import { beforeEach, describe, expect, it, vi } from 'bun:test';
import type { Logger } from '@repo/shared';
import { ErrorCode, type ValidationFailedContext } from '@repo/shared/errors';
import type {
  CloneOptions,
  ServiceResult
} from '@sandbox-container/core/types';
import { DEFAULT_GIT_CLONE_TIMEOUT_MS } from '@sandbox-container/managers/git-manager';
import type { ExecutionService } from '@sandbox-container/services/execution-service';
import {
  GitService,
  type SecurityService
} from '@sandbox-container/services/git-service';
import type { SessionManager } from '@sandbox-container/services/session-manager';
import type { RawExecResult } from '@sandbox-container/session';
import { mocked } from '../test-utils';

// Properly typed mock dependencies
const mockSecurityService: SecurityService = {
  validateGitUrl: vi.fn(),
  validatePath: vi.fn()
};

const mockLogger = {
  info: vi.fn(),
  error: vi.fn(),
  warn: vi.fn(),
  debug: vi.fn(),
  child: vi.fn()
} as Logger;
mockLogger.child = vi.fn(() => mockLogger);

// Properly typed mock SessionManager
const mockSessionManager = {
  executeInSession: vi.fn(),
  executeStreamInSession: vi.fn(),
  killCommand: vi.fn(),
  setEnvVars: vi.fn(),
  getSession: vi.fn(),
  createSession: vi.fn(),
  deleteSession: vi.fn(),
  listSessions: vi.fn(),
  destroy: vi.fn(),
  withSession: vi.fn()
} as unknown as SessionManager;

const mockExecutionService = {
  execute: vi.fn(),
  executeStream: vi.fn(),
  withExecution: vi.fn(),
  kill: vi.fn()
} as unknown as ExecutionService;

describe('GitService', () => {
  let gitService: GitService;

  beforeEach(async () => {
    // Reset all mocks before each test
    vi.clearAllMocks();

    // Set up default successful security validations
    mocked(mockSecurityService.validateGitUrl).mockReturnValue({
      isValid: true,
      errors: []
    });
    mocked(mockSecurityService.validatePath).mockReturnValue({
      isValid: true,
      errors: []
    });

    mocked(mockExecutionService.execute).mockImplementation(
      async (command, options = {}) => {
        const sessionId = options.sessionId ?? 'default';
        const forwardedOptions =
          options.cwd !== undefined ||
          options.env !== undefined ||
          options.origin !== undefined
            ? {
                ...(options.cwd !== undefined && { cwd: options.cwd }),
                ...(options.env !== undefined && { env: options.env }),
                ...(options.origin !== undefined && { origin: options.origin })
              }
            : undefined;

        return forwardedOptions !== undefined
          ? await mockSessionManager.executeInSession(
              sessionId,
              command,
              forwardedOptions
            )
          : await mockSessionManager.executeInSession(sessionId, command);
      }
    );

    mocked(mockExecutionService.withExecution).mockImplementation(
      async ({ sessionId }, callback) => {
        try {
          const mockExec = async (
            cmd: string,
            options?: {
              cwd?: string;
              env?: Record<string, string | undefined>;
              timeoutMs?: number;
              origin?: 'user' | 'internal';
            }
          ) => {
            const result = await mockExecutionService.execute(cmd, {
              sessionId,
              cwd: options?.cwd,
              env: options?.env,
              timeoutMs: options?.timeoutMs,
              origin: options?.origin
            });
            if (result.success) {
              return result.data;
            }
            throw result.error;
          };
          const data = await callback(mockExec);
          return { success: true, data } as any;
        } catch (error: any) {
          // If error has code/message/details, return it as-is
          if (error && typeof error === 'object' && 'code' in error) {
            return { success: false, error } as any;
          }
          // Otherwise wrap as generic error
          return {
            success: false,
            error: {
              code: 'INTERNAL_ERROR',
              message: error instanceof Error ? error.message : 'Unknown error',
              details: {}
            }
          } as any;
        }
      }
    );

    gitService = new GitService(
      mockSecurityService,
      mockExecutionService,
      mockLogger
    );
  });

  describe('cloneRepository', () => {
    it('should clone repository successfully with default options', async () => {
      // Mock successful git clone
      mocked(mockSessionManager.executeInSession)
        .mockResolvedValueOnce({
          success: true,
          data: {
            exitCode: 0,
            stdout: 'Cloning into target-dir...',
            stderr: ''
          }
        } as ServiceResult<RawExecResult>)
        .mockResolvedValueOnce({
          success: true,
          data: {
            exitCode: 0,
            stdout: 'main\n',
            stderr: ''
          }
        } as ServiceResult<RawExecResult>);

      const result = await gitService.cloneRepository(
        'https://github.com/user/repo.git'
      );

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.path).toBe('/workspace/repo');
        expect(result.data.branch).toBe('main');
      }

      // Verify security validations were called
      expect(mockSecurityService.validateGitUrl).toHaveBeenCalledWith(
        'https://github.com/user/repo.git'
      );
      expect(mockSecurityService.validatePath).toHaveBeenCalledWith(
        '/workspace/repo'
      );

      // Verify SessionManager was called for git clone (cwd is undefined)
      expect(mockSessionManager.executeInSession).toHaveBeenNthCalledWith(
        1,
        'default',
        "'timeout' '-k' '5' '600' 'git' '-c' 'http.lowSpeedLimit=1024' '-c' 'http.lowSpeedTime=30' 'clone' '--filter=blob:none' 'https://github.com/user/repo.git' '/workspace/repo'",
        { origin: 'internal' }
      );

      // Verify SessionManager was called for getting current branch
      expect(mockSessionManager.executeInSession).toHaveBeenNthCalledWith(
        2,
        'default',
        "'git' 'branch' '--show-current'",
        { cwd: '/workspace/repo', origin: 'internal' }
      );
    });

    it('should clone repository with custom branch and target directory', async () => {
      mocked(mockSessionManager.executeInSession)
        .mockResolvedValueOnce({
          success: true,
          data: {
            exitCode: 0,
            stdout: 'Cloning...',
            stderr: ''
          }
        } as ServiceResult<RawExecResult>)
        .mockResolvedValueOnce({
          success: true,
          data: {
            exitCode: 0,
            stdout: 'develop\n',
            stderr: ''
          }
        } as ServiceResult<RawExecResult>);

      const options: CloneOptions = {
        branch: 'develop',
        targetDir: '/tmp/custom-target',
        sessionId: 'session-123'
      };

      const result = await gitService.cloneRepository(
        'https://github.com/user/repo.git',
        options
      );

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.path).toBe('/tmp/custom-target');
        expect(result.data.branch).toBe('develop');
      }

      // Verify git clone command includes branch option (cwd is undefined)
      expect(mockSessionManager.executeInSession).toHaveBeenNthCalledWith(
        1,
        'session-123',
        "'timeout' '-k' '5' '600' 'git' '-c' 'http.lowSpeedLimit=1024' '-c' 'http.lowSpeedTime=30' 'clone' '--filter=blob:none' '--branch' 'develop' 'https://github.com/user/repo.git' '/tmp/custom-target'",
        { origin: 'internal' }
      );
    });

    it('should clone repository with custom timeout', async () => {
      mocked(mockSessionManager.executeInSession)
        .mockResolvedValueOnce({
          success: true,
          data: {
            exitCode: 0,
            stdout: 'Cloning...',
            stderr: ''
          }
        } as ServiceResult<RawExecResult>)
        .mockResolvedValueOnce({
          success: true,
          data: {
            exitCode: 0,
            stdout: 'main\n',
            stderr: ''
          }
        } as ServiceResult<RawExecResult>);

      const result = await gitService.cloneRepository(
        'https://github.com/user/repo.git',
        { timeoutMs: 90_000 }
      );

      expect(result.success).toBe(true);
      expect(mockSessionManager.executeInSession).toHaveBeenNthCalledWith(
        1,
        'default',
        "'timeout' '-k' '5' '90' 'git' '-c' 'http.lowSpeedLimit=1024' '-c' 'http.lowSpeedTime=30' 'clone' '--filter=blob:none' 'https://github.com/user/repo.git' '/workspace/repo'",
        { origin: 'internal' }
      );
    });

    it('should return error when git URL validation fails', async () => {
      mocked(mockSecurityService.validateGitUrl).mockReturnValue({
        isValid: false,
        errors: ['Invalid URL scheme', 'URL not in allowlist']
      });

      const result = await gitService.cloneRepository(
        'ftp://malicious.com/repo.git'
      );

      expect(result.success).toBe(false);
      if (result.success) throw new Error('Expected failure');
      expect(result.error.code).toBe('INVALID_GIT_URL');
      expect(result.error.message).toContain('Invalid URL scheme');
      const details = result.error
        .details as unknown as ValidationFailedContext;
      expect(details.validationErrors).toBeDefined();
      expect(details.validationErrors[0]?.message).toContain(
        'Invalid URL scheme'
      );

      // Should not attempt git clone
      expect(mockSessionManager.executeInSession).not.toHaveBeenCalled();
    });

    it('should return error when target directory validation fails', async () => {
      mocked(mockSecurityService.validatePath).mockReturnValue({
        isValid: false,
        errors: ['Path outside sandbox', 'Path contains invalid characters']
      });

      const result = await gitService.cloneRepository(
        'https://github.com/user/repo.git',
        { targetDir: '/malicious/../path' }
      );

      expect(result.success).toBe(false);
      if (result.success) throw new Error('Expected failure');
      expect(result.error.code).toBe('VALIDATION_FAILED');
      const details = result.error
        .details as unknown as ValidationFailedContext;
      expect(details.validationErrors).toBeDefined();
      expect(details.validationErrors[0]?.message).toContain(
        'Path outside sandbox'
      );

      // Should not attempt git clone
      expect(mockSessionManager.executeInSession).not.toHaveBeenCalled();
    });

    it('should return error when git clone command fails', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 128,
          stdout: '',
          stderr: 'fatal: repository not found'
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.cloneRepository(
        'https://github.com/user/nonexistent.git'
      );

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe(ErrorCode.GIT_REPOSITORY_NOT_FOUND);
        expect(result.error.details?.exitCode).toBe(128);
        expect(result.error.details?.stderr).toContain('repository not found');
      }
    });

    it('should return timeout error when clone exits with code 124', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 124,
          stdout: '',
          stderr: ''
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.cloneRepository(
        'https://github.com/user/large-repo.git'
      );

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe(ErrorCode.GIT_NETWORK_ERROR);
        expect(result.error.message).toContain('timed out');
        expect(result.error.message).toContain(
          `${DEFAULT_GIT_CLONE_TIMEOUT_MS / 1000} seconds`
        );
        expect(result.error.details?.exitCode).toBe(124);
      }
    });

    it('should return validation error when timeout is invalid', async () => {
      const result = await gitService.cloneRepository(
        'https://github.com/user/repo.git',
        { timeoutMs: 0 }
      );

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe(ErrorCode.VALIDATION_FAILED);
        expect(result.error.message).toContain('Invalid clone timeout');
      }

      expect(mockSessionManager.executeInSession).not.toHaveBeenCalled();
    });

    it('should sanitize credentials in timeout error messages', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 124,
          stdout: '',
          stderr: ''
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.cloneRepository(
        'https://user:secret-token@github.com/user/repo.git'
      );

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe(ErrorCode.GIT_NETWORK_ERROR);
        expect(result.error.message).not.toContain('secret-token');
        expect(JSON.stringify(result.error.details)).not.toContain(
          'secret-token'
        );
      }
    });

    it('should handle execution errors gracefully', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: false,
        error: {
          message: 'Session execution failed',
          code: 'SESSION_ERROR'
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.cloneRepository(
        'https://github.com/user/repo.git'
      );

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe('SESSION_ERROR');
      }
    });
  });

  describe('checkoutBranch', () => {
    it('should checkout branch successfully', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 0,
          stdout: 'Switched to branch develop',
          stderr: ''
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.checkoutBranch(
        '/tmp/repo',
        'develop',
        'session-123'
      );

      expect(result.success).toBe(true);

      // Verify SessionManager was called with correct parameters
      expect(mockSessionManager.executeInSession).toHaveBeenCalledWith(
        'session-123',
        "'git' 'checkout' 'develop'",
        { cwd: '/tmp/repo', origin: 'internal' }
      );
    });

    it('should return error when branch name is empty', async () => {
      const result = await gitService.checkoutBranch('/tmp/repo', '');

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe('VALIDATION_FAILED');
        expect(result.error.message).toContain('Invalid branch name');
      }

      expect(mockSessionManager.executeInSession).not.toHaveBeenCalled();
    });

    it('should return error when git checkout fails', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 1,
          stdout: '',
          stderr: "error: pathspec 'nonexistent' did not match"
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.checkoutBranch(
        '/tmp/repo',
        'nonexistent'
      );

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe(ErrorCode.GIT_BRANCH_NOT_FOUND);
        expect(result.error.details?.stderr).toContain('did not match');
      }
    });
  });

  describe('getCurrentBranch', () => {
    it('should return current branch successfully', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 0,
          stdout: 'main\n',
          stderr: ''
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.getCurrentBranch(
        '/tmp/repo',
        'session-123'
      );

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data).toBe('main');
      }

      expect(mockSessionManager.executeInSession).toHaveBeenCalledWith(
        'session-123',
        "'git' 'branch' '--show-current'",
        { cwd: '/tmp/repo', origin: 'internal' }
      );
    });
  });

  describe('listBranches', () => {
    it('should list branches successfully and parse output correctly', async () => {
      const branchOutput = `  develop
* main
  feature/auth
  remotes/origin/HEAD -> origin/main
  remotes/origin/develop
  remotes/origin/main
  remotes/origin/feature/auth`;

      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 0,
          stdout: branchOutput,
          stderr: ''
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.listBranches('/tmp/repo', 'session-123');

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data).toEqual([
          'develop',
          'main',
          'feature/auth',
          'HEAD -> origin/main'
        ]);

        // Should not include duplicates or HEAD references
        expect(result.data).not.toContain('HEAD');
        expect(result.data.filter((b) => b === 'main')).toHaveLength(1);
      }

      expect(mockSessionManager.executeInSession).toHaveBeenCalledWith(
        'session-123',
        "'git' 'branch' '-a'",
        { cwd: '/tmp/repo', origin: 'internal' }
      );
    });

    it('should return error when git branch command fails', async () => {
      mocked(mockSessionManager.executeInSession).mockResolvedValue({
        success: true,
        data: {
          exitCode: 128,
          stdout: '',
          stderr: 'fatal: not a git repository'
        }
      } as ServiceResult<RawExecResult>);

      const result = await gitService.listBranches('/tmp/not-a-repo');

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe(ErrorCode.GIT_OPERATION_FAILED);
        expect(result.error.details?.exitCode).toBe(128);
      }
    });
  });
});
