import { beforeEach, describe, expect, it } from 'bun:test';
import { ErrorCode } from '@repo/shared/errors';
import {
  DEFAULT_GIT_CLONE_TIMEOUT_MS,
  GitManager
} from '@sandbox-container/managers/git-manager';

describe('GitManager', () => {
  let manager: GitManager;

  beforeEach(() => {
    manager = new GitManager();
  });

  describe('generateTargetDirectory', () => {
    it('should generate directory in /workspace with repo name', () => {
      const dir = manager.generateTargetDirectory(
        'https://github.com/user/repo.git'
      );

      expect(dir).toBe('/workspace/repo');
    });

    it('should generate consistent directories for same URL', () => {
      const dir1 = manager.generateTargetDirectory(
        'https://github.com/user/repo.git'
      );
      const dir2 = manager.generateTargetDirectory(
        'https://github.com/user/repo.git'
      );

      expect(dir1).toBe(dir2);
    });

    it('should handle invalid URLs with fallback name', () => {
      const dir = manager.generateTargetDirectory('invalid-url');

      expect(dir).toBe('/workspace/repository');
    });
  });

  describe('buildCloneArgs', () => {
    const timeoutPrefix = [
      'timeout',
      '-k',
      '5',
      String(DEFAULT_GIT_CLONE_TIMEOUT_MS / 1000)
    ];
    const gitConfig = [
      'git',
      '-c',
      'http.lowSpeedLimit=1024',
      '-c',
      'http.lowSpeedTime=30'
    ];

    it('should build basic clone args with timeout and stall detection', () => {
      const args = manager.buildCloneArgs(
        'https://github.com/user/repo.git',
        '/tmp/target',
        {}
      );
      expect(args).toEqual([
        ...timeoutPrefix,
        ...gitConfig,
        'clone',
        '--filter=blob:none',
        'https://github.com/user/repo.git',
        '/tmp/target'
      ]);
    });

    it('should build clone args with branch option', () => {
      const args = manager.buildCloneArgs(
        'https://github.com/user/repo.git',
        '/tmp/target',
        { branch: 'develop' }
      );
      expect(args).toEqual([
        ...timeoutPrefix,
        ...gitConfig,
        'clone',
        '--filter=blob:none',
        '--branch',
        'develop',
        'https://github.com/user/repo.git',
        '/tmp/target'
      ]);
    });

    it('should build clone args with depth option for shallow clone', () => {
      const args = manager.buildCloneArgs(
        'https://github.com/user/repo.git',
        '/tmp/target',
        { depth: 1 }
      );
      expect(args).toEqual([
        ...timeoutPrefix,
        ...gitConfig,
        'clone',
        '--filter=blob:none',
        '--depth',
        '1',
        'https://github.com/user/repo.git',
        '/tmp/target'
      ]);
    });

    it('should build clone args with both branch and depth options', () => {
      const args = manager.buildCloneArgs(
        'https://github.com/user/repo.git',
        '/tmp/target',
        { branch: 'main', depth: 10 }
      );
      expect(args).toEqual([
        ...timeoutPrefix,
        ...gitConfig,
        'clone',
        '--filter=blob:none',
        '--branch',
        'main',
        '--depth',
        '10',
        'https://github.com/user/repo.git',
        '/tmp/target'
      ]);
    });

    it('should pass through depth value to git command', () => {
      const args = manager.buildCloneArgs(
        'https://github.com/user/repo.git',
        '/tmp/target',
        { depth: 5 }
      );
      expect(args).toEqual([
        ...timeoutPrefix,
        ...gitConfig,
        'clone',
        '--filter=blob:none',
        '--depth',
        '5',
        'https://github.com/user/repo.git',
        '/tmp/target'
      ]);
    });

    it('should build clone args with custom timeout', () => {
      const args = manager.buildCloneArgs(
        'https://github.com/user/repo.git',
        '/tmp/target',
        { timeoutMs: 90_000 }
      );
      expect(args).toEqual([
        'timeout',
        '-k',
        '5',
        '90',
        ...gitConfig,
        'clone',
        '--filter=blob:none',
        'https://github.com/user/repo.git',
        '/tmp/target'
      ]);
    });

    it('should preserve millisecond precision for sub-second clone timeouts', () => {
      const args = manager.buildCloneArgs(
        'https://github.com/user/repo.git',
        '/tmp/target',
        { timeoutMs: 1_500 }
      );

      expect(args).toEqual([
        'timeout',
        '-k',
        '5',
        '1.5',
        ...gitConfig,
        'clone',
        '--filter=blob:none',
        'https://github.com/user/repo.git',
        '/tmp/target'
      ]);
    });
  });

  describe('buildCheckoutArgs', () => {
    it('should build checkout args with branch names', () => {
      expect(manager.buildCheckoutArgs('develop')).toEqual([
        'git',
        'checkout',
        'develop'
      ]);
      expect(manager.buildCheckoutArgs('feature/new-feature')).toEqual([
        'git',
        'checkout',
        'feature/new-feature'
      ]);
    });
  });

  describe('buildGetCurrentBranchArgs', () => {
    it('should build get current branch args', () => {
      expect(manager.buildGetCurrentBranchArgs()).toEqual([
        'git',
        'branch',
        '--show-current'
      ]);
    });
  });

  describe('buildListBranchesArgs', () => {
    it('should build list branches args', () => {
      expect(manager.buildListBranchesArgs()).toEqual(['git', 'branch', '-a']);
    });
  });

  describe('parseBranchList', () => {
    it('should parse and deduplicate branch list with remote branches', () => {
      const output = `  develop
* main
  remotes/origin/develop
  remotes/origin/main
  remotes/origin/feature/auth`;
      expect(manager.parseBranchList(output)).toEqual([
        'develop',
        'main',
        'feature/auth'
      ]);
    });

    it('should filter out HEAD references', () => {
      const output = `  develop
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main`;
      const branches = manager.parseBranchList(output);
      expect(branches).not.toContain('HEAD');
      expect(branches).toContain('HEAD -> origin/main');
    });

    it('should handle empty and single branch lists', () => {
      expect(manager.parseBranchList('\n\n  \n')).toEqual([]);
      expect(manager.parseBranchList('* main')).toEqual(['main']);
    });
  });

  describe('validateBranchName', () => {
    it('should validate non-empty branch names', () => {
      expect(manager.validateBranchName('main').isValid).toBe(true);
      expect(manager.validateBranchName('feature/new-feature').isValid).toBe(
        true
      );
    });

    it('should reject empty or whitespace-only branch names', () => {
      const emptyResult = manager.validateBranchName('');
      expect(emptyResult.isValid).toBe(false);
      expect(emptyResult.error).toBe('Branch name cannot be empty');

      const whitespaceResult = manager.validateBranchName('   ');
      expect(whitespaceResult.isValid).toBe(false);
      expect(whitespaceResult.error).toBe('Branch name cannot be empty');
    });
  });

  describe('determineErrorCode', () => {
    it('should return GIT_NETWORK_ERROR for exit code 124 (timeout)', () => {
      expect(manager.determineErrorCode('clone', new Error(''), 124)).toBe(
        ErrorCode.GIT_NETWORK_ERROR
      );
    });

    it('should return GIT_OPERATION_FAILED for exit code 128 with not a git repository message', () => {
      const error = new Error('fatal: not a git repository');

      expect(manager.determineErrorCode('getCurrentBranch', error, 128)).toBe(
        ErrorCode.GIT_OPERATION_FAILED
      );
    });

    it('should return GIT_REPOSITORY_NOT_FOUND for exit code 128 with repository not found message', () => {
      const error = new Error('fatal: repository not found');

      expect(manager.determineErrorCode('clone', error, 128)).toBe(
        ErrorCode.GIT_REPOSITORY_NOT_FOUND
      );
    });

    it('should return GIT_AUTH_FAILED for permission errors', () => {
      expect(
        manager.determineErrorCode('clone', new Error('Permission denied'))
      ).toBe(ErrorCode.GIT_AUTH_FAILED);
    });

    it('should return GIT_REPOSITORY_NOT_FOUND for not found errors', () => {
      expect(
        manager.determineErrorCode('checkout', new Error('Branch not found'))
      ).toBe(ErrorCode.GIT_REPOSITORY_NOT_FOUND);
    });

    it('should return GIT_BRANCH_NOT_FOUND for pathspec errors', () => {
      expect(
        manager.determineErrorCode(
          'checkout',
          new Error("pathspec 'branch' did not match")
        )
      ).toBe(ErrorCode.GIT_BRANCH_NOT_FOUND);
    });

    it('should return GIT_AUTH_FAILED for authentication errors', () => {
      expect(
        manager.determineErrorCode('clone', new Error('Authentication failed'))
      ).toBe(ErrorCode.GIT_AUTH_FAILED);
    });

    it('should return operation-specific error codes as fallback', () => {
      expect(
        manager.determineErrorCode('clone', new Error('Unknown error'))
      ).toBe(ErrorCode.GIT_CLONE_FAILED);
      expect(
        manager.determineErrorCode('checkout', new Error('Unknown error'))
      ).toBe(ErrorCode.GIT_CHECKOUT_FAILED);
      expect(
        manager.determineErrorCode(
          'getCurrentBranch',
          new Error('Unknown error')
        )
      ).toBe(ErrorCode.GIT_OPERATION_FAILED);
      expect(
        manager.determineErrorCode('listBranches', new Error('Unknown error'))
      ).toBe(ErrorCode.GIT_OPERATION_FAILED);
    });

    it('should handle string errors', () => {
      expect(manager.determineErrorCode('clone', 'repository not found')).toBe(
        ErrorCode.GIT_REPOSITORY_NOT_FOUND
      );
    });

    it('should handle case-insensitive error matching', () => {
      expect(
        manager.determineErrorCode('clone', new Error('PERMISSION DENIED'))
      ).toBe(ErrorCode.GIT_AUTH_FAILED);
    });
  });

  describe('createErrorMessage', () => {
    it('should create error messages with operation context', () => {
      const cloneMsg = manager.createErrorMessage(
        'clone',
        { repoUrl: 'https://github.com/user/repo.git', targetDir: '/tmp/repo' },
        'Repository not found'
      );
      expect(cloneMsg).toContain('clone repository');
      expect(cloneMsg).toContain('repoUrl=https://github.com/user/repo.git');
      expect(cloneMsg).toContain('Repository not found');

      const checkoutMsg = manager.createErrorMessage(
        'checkout',
        { repoPath: '/tmp/repo', branch: 'develop' },
        'Branch not found'
      );
      expect(checkoutMsg).toContain('checkout branch');
      expect(checkoutMsg).toContain('branch=develop');
    });
  });

  describe('isSshUrl', () => {
    it('should return true for SSH URLs', () => {
      expect(manager.isSshUrl('git@github.com:user/repo.git')).toBe(true);
      expect(manager.isSshUrl('ssh://git@github.com:22/user/repo.git')).toBe(
        true
      );
    });

    it('should return false for HTTPS URLs', () => {
      expect(manager.isSshUrl('https://github.com/user/repo.git')).toBe(false);
    });
  });

  describe('isHttpsUrl', () => {
    it('should return true for HTTPS URLs', () => {
      expect(manager.isHttpsUrl('https://github.com/user/repo.git')).toBe(true);
    });

    it('should return false for SSH URLs', () => {
      expect(manager.isHttpsUrl('git@github.com:user/repo.git')).toBe(false);
    });

    it('should return false for invalid URLs', () => {
      expect(manager.isHttpsUrl('not-a-url')).toBe(false);
    });
  });
});
