/**
 * Unit tests for GitLab Client Service
 */

import {
  fetchGitLabFileContent,
  fetchGitLabFileContentSafe,
  isValidGitLabUrl,
  GitLabError,
  GitLabAuthError,
  validateAuthenticationConfig
} from '../services/gitlabClient';
import { config } from '../config';

// Mock the global fetch function
const mockFetch = jest.fn();
global.fetch = mockFetch as jest.MockedFunction<typeof fetch>;

describe('GitLab Client Service', () => {
  // Store original token value
  const originalToken = config.auth.gitlabToken;

  beforeEach(() => {
    // Reset all mocks before each test
    jest.clearAllMocks();
    // Clear console.log and console.error mocks
    jest.spyOn(console, 'log').mockImplementation(() => {});
    jest.spyOn(console, 'error').mockImplementation(() => {});
    // Reset token to original value
    (config.auth as any).gitlabToken = originalToken;
  });

  afterEach(() => {
    // Restore console methods
    jest.restoreAllMocks();
    // Reset token to original value
    (config.auth as any).gitlabToken = originalToken;
  });

  describe('fetchGitLabFileContent', () => {
    const testUrl = 'https://gitlab.example.com/repo/-/raw/main/llms.txt';
    const testContent = 'This is test content from GitLab';

    it('should successfully fetch content from GitLab without token', async () => {
      // Ensure no token is set
      (config.auth as any).gitlabToken = undefined;

      // Mock successful response
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        statusText: 'OK',
        text: jest.fn().mockResolvedValueOnce(testContent)
      } as any);

      const result = await fetchGitLabFileContent(testUrl);

      expect(result).toBe(testContent);
      expect(mockFetch).toHaveBeenCalledWith(testUrl, {
        headers: {
          'User-Agent': 'awesome-components-mcp/1.0.0'
        }
      });
      expect(mockFetch).toHaveBeenCalledTimes(1);
    });

    it('should successfully fetch content from GitLab with token', async () => {
      // Set a test token
      (config.auth as any).gitlabToken = 'test-token-123';

      // Mock successful response
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        statusText: 'OK',
        text: jest.fn().mockResolvedValueOnce(testContent)
      } as any);

      const result = await fetchGitLabFileContent(testUrl);

      expect(result).toBe(testContent);
      expect(mockFetch).toHaveBeenCalledWith(testUrl, {
        headers: {
          'User-Agent': 'awesome-components-mcp/1.0.0',
          'Authorization': 'Bearer test-token-123'
        }
      });
      expect(mockFetch).toHaveBeenCalledTimes(1);
    });

    it('should handle 401 Unauthorized errors', async () => {
      // Mock 401 response
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 401,
        statusText: 'Unauthorized',
        text: jest.fn()
      } as any);

      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabAuthError);
      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Authentication failed (status: 401)');
    });

    it('should handle 404 Not Found errors', async () => {
      // Mock 404 response
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
        statusText: 'Not Found',
        text: jest.fn()
      } as any);

      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('File not found (status: 404)');
    });

    it('should handle 403 Forbidden errors without token', async () => {
      // Ensure no token is set
      (config.auth as any).gitlabToken = undefined;

      // Mock 403 response
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 403,
        statusText: 'Forbidden',
        text: jest.fn()
      } as any);

      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('This repository may be private and require authentication');
    });

    it('should handle 403 Forbidden errors with token', async () => {
      // Set a test token
      (config.auth as any).gitlabToken = 'test-token-123';

      // Mock 403 response
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 403,
        statusText: 'Forbidden',
        text: jest.fn()
      } as any);

      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Your token may not have sufficient permissions');
    });

    it('should handle 5xx server errors', async () => {
      // Mock 500 response
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        statusText: 'Internal Server Error',
        text: jest.fn()
      } as any);

      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('GitLab server error (status: 500)');
    });

    it('should handle other HTTP errors', async () => {
      // Mock 400 response
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 400,
        statusText: 'Bad Request',
        text: jest.fn()
      } as any);

      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Failed to fetch content from GitLab (status: 400)');
    });

    it('should handle network failures', async () => {
      // Mock network error
      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Network error while fetching from GitLab');
    });

    it('should re-throw GitLabError without wrapping', async () => {
      // Mock a response that will trigger a GitLabError
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
        statusText: 'Not Found',
        text: jest.fn()
      } as any);

      try {
        await fetchGitLabFileContent(testUrl);
      } catch (error) {
        expect(error).toBeInstanceOf(GitLabError);
        expect((error as GitLabError).statusCode).toBe(404);
        expect((error as GitLabError).url).toBe(testUrl);
      }
    });

    it('should log successful fetches', async () => {
      const consoleSpy = jest.spyOn(console, 'log');
      
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        statusText: 'OK',
        text: jest.fn().mockResolvedValueOnce(testContent)
      } as any);

      await fetchGitLabFileContent(testUrl);

      expect(consoleSpy).toHaveBeenCalledWith(`Fetching content from GitLab URL: ${testUrl}`);
      expect(consoleSpy).toHaveBeenCalledWith(`Successfully fetched ${testContent.length} characters from ${testUrl}`);
    });

    it('should log errors for failed requests', async () => {
      const consoleSpy = jest.spyOn(console, 'error');

      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
        statusText: 'Not Found',
        text: jest.fn()
      } as any);

      try {
        await fetchGitLabFileContent(testUrl);
      } catch {
        // Expected to throw
      }

      expect(consoleSpy).toHaveBeenCalledWith(`GitLab request to ${testUrl} failed: 404 Not Found`);
    });
  });

  describe('isValidGitLabUrl', () => {
    it('should validate correct GitLab URLs', () => {
      const validUrls = [
        'https://gitlab.com/user/repo/-/raw/main/file.txt',
        'http://gitlab.example.com/project/-/raw/master/llms.txt',
        'https://gitlab.yeepay.com/awesome/awesome-components/-/raw/main/llms.txt'
      ];

      validUrls.forEach(url => {
        expect(isValidGitLabUrl(url)).toBe(true);
      });
    });

    it('should reject invalid URLs', () => {
      const invalidUrls = [
        'not-a-url',
        'ftp://gitlab.com/file.txt',
        'https://github.com/user/repo/raw/main/file.txt', // GitHub, not GitLab
        'https://example.com/file.txt', // No GitLab indicators
        'https://gitlab.com/file.txt', // No raw path
        'https://notgitlab.com/-/raw/main/file.txt', // No GitLab in hostname
        'https://git.company.com/team/project/-/raw/develop/docs.txt' // git but not gitlab
      ];

      invalidUrls.forEach(url => {
        expect(isValidGitLabUrl(url)).toBe(false);
      });
    });

    it('should handle malformed URLs gracefully', () => {
      const malformedUrls = [
        '',
        'http://',
        'https://',
        'not a url at all'
      ];

      malformedUrls.forEach(url => {
        expect(isValidGitLabUrl(url)).toBe(false);
      });
    });
  });

  describe('validateAuthenticationConfig', () => {
    const testUrl = 'http://gitlab.yeepay.com/awesome/awesome-components/-/raw/main/test.txt';

    it('should return valid for no token (public access)', () => {
      (config.auth as any).gitlabToken = undefined;

      const result = validateAuthenticationConfig(testUrl);

      expect(result.isValid).toBe(true);
      expect(result.message).toContain('public repositories only');
    });

    it('should return valid for proper token', () => {
      (config.auth as any).gitlabToken = 'valid-token-123456';

      const result = validateAuthenticationConfig(testUrl);

      expect(result.isValid).toBe(true);
      expect(result.message).toContain('Authentication token is configured');
    });

    it('should return invalid for short token', () => {
      (config.auth as any).gitlabToken = 'short';

      const result = validateAuthenticationConfig(testUrl);

      expect(result.isValid).toBe(false);
      expect(result.message).toContain('too short');
    });

    it('should return invalid for empty token', () => {
      (config.auth as any).gitlabToken = '   ';

      const result = validateAuthenticationConfig(testUrl);

      expect(result.isValid).toBe(false);
      expect(result.message).toContain('too short');
    });
  });

  describe('fetchGitLabFileContentSafe', () => {
    const validUrl = 'https://gitlab.example.com/repo/-/raw/main/llms.txt';
    const invalidUrl = 'https://github.com/user/repo/raw/main/file.txt';
    const testContent = 'Safe fetch test content';

    it('should fetch content for valid GitLab URLs', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        statusText: 'OK',
        text: jest.fn().mockResolvedValueOnce(testContent)
      } as any);

      const result = await fetchGitLabFileContentSafe(validUrl);

      expect(result).toBe(testContent);
      expect(mockFetch).toHaveBeenCalledWith(validUrl);
    });

    it('should reject invalid GitLab URLs', async () => {
      await expect(fetchGitLabFileContentSafe(invalidUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContentSafe(invalidUrl)).rejects.toThrow('Invalid GitLab URL format');

      // Should not make any fetch calls for invalid URLs
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it('should propagate fetch errors for valid URLs', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 404,
        statusText: 'Not Found',
        text: jest.fn()
      } as any);

      await expect(fetchGitLabFileContentSafe(validUrl)).rejects.toThrow(GitLabError);
      await expect(fetchGitLabFileContentSafe(validUrl)).rejects.toThrow('File not found at GitLab URL');
    });
  });

  describe('GitLabError', () => {
    it('should create error with all properties', () => {
      const error = new GitLabError('Test error', 404, 'https://example.com');

      expect(error.message).toBe('Test error');
      expect(error.statusCode).toBe(404);
      expect(error.url).toBe('https://example.com');
      expect(error.name).toBe('GitLabError');
      expect(error).toBeInstanceOf(Error);
    });

    it('should create error with minimal properties', () => {
      const error = new GitLabError('Simple error');

      expect(error.message).toBe('Simple error');
      expect(error.statusCode).toBeUndefined();
      expect(error.url).toBeUndefined();
      expect(error.name).toBe('GitLabError');
    });
  });
});
