import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('node:child_process', () => ({
  execFileSync: vi.fn(),
}));

import { execFileSync } from 'node:child_process';
import {
  createBranchBaseResolutionCache,
  findFirstTaktCommit,
  resolveBranchBaseCommit,
} from '../infra/task/branchGitResolver.js';

const mockExecFileSync = vi.mocked(execFileSync);

describe('branchGitResolver performance', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should skip full ref scan when default branch candidate resolves', () => {
    mockExecFileSync.mockImplementation((cmd, args) => {
      if (cmd !== 'git') {
        throw new Error('unexpected command');
      }

      if (args[0] === 'reflog') {
        throw new Error('reflog unavailable');
      }

      if (args[0] === 'merge-base' && args[1] === 'main') {
        return 'base-main';
      }

      if (args[0] === 'merge-base' && args[1] === 'origin/main') {
        throw new Error('origin/main not available');
      }

      if (args[0] === 'rev-list') {
        return '1';
      }

      if (args[0] === 'log' && args[1] === '--format=%s') {
        return 'takt: first';
      }

      if (args[0] === 'for-each-ref') {
        throw new Error('for-each-ref should not be called');
      }

      throw new Error(`unexpected git args: ${args.join(' ')}`);
    });

    const baseCommit = resolveBranchBaseCommit('/project', 'main', 'takt/feature-a');

    expect(baseCommit).toBe('base-main');
    expect(mockExecFileSync).not.toHaveBeenCalledWith(
      'git',
      ['for-each-ref', '--format=%(refname:short)', 'refs/heads', 'refs/remotes'],
      expect.anything(),
    );
  });

  it('should skip full ref scan when default branch candidate resolves without takt prefix', () => {
    mockExecFileSync.mockImplementation((cmd, args) => {
      if (cmd !== 'git') {
        throw new Error('unexpected command');
      }

      if (args[0] === 'reflog') {
        throw new Error('reflog unavailable');
      }

      if (args[0] === 'merge-base' && args[1] === 'main') {
        return 'base-main';
      }

      if (args[0] === 'merge-base' && args[1] === 'origin/main') {
        throw new Error('origin/main not available');
      }

      if (args[0] === 'rev-list') {
        return '3';
      }

      if (args[0] === 'log' && args[1] === '--format=%s') {
        return 'feat: add new feature';
      }

      if (args[0] === 'for-each-ref') {
        throw new Error('for-each-ref should not be called');
      }

      throw new Error(`unexpected git args: ${args.join(' ')}`);
    });

    const baseCommit = resolveBranchBaseCommit('/project', 'main', 'takt/feature-a');

    expect(baseCommit).toBe('base-main');
    expect(mockExecFileSync).not.toHaveBeenCalledWith(
      'git',
      ['for-each-ref', '--format=%(refname:short)', 'refs/heads', 'refs/remotes'],
      expect.anything(),
    );
  });

  it('should reuse ref list cache across branch resolutions', () => {
    mockExecFileSync.mockImplementation((cmd, args) => {
      if (cmd !== 'git') {
        throw new Error('unexpected command');
      }

      if (args[0] === 'reflog') {
        throw new Error('reflog unavailable');
      }

      if (args[0] === 'merge-base') {
        const baseRef = args[1];
        const branch = args[2];
        if (baseRef === 'main' || baseRef === 'origin/main') {
          throw new Error('priority refs unavailable');
        }
        if (baseRef === 'develop' && branch === 'takt/feature-a') {
          return 'base-a';
        }
        if (baseRef === 'origin/develop' && branch === 'takt/feature-a') {
          return 'base-a-remote';
        }
        if (baseRef === 'develop' && branch === 'takt/feature-b') {
          return 'base-b';
        }
        if (baseRef === 'origin/develop' && branch === 'takt/feature-b') {
          return 'base-b-remote';
        }
        throw new Error(`unexpected merge-base args: ${args.join(' ')}`);
      }

      if (args[0] === 'for-each-ref') {
        return 'develop\norigin/develop\n';
      }

      if (args[0] === 'rev-parse' && args[1] === '--git-common-dir') {
        return '/project/.git';
      }

      if (args[0] === 'rev-list') {
        const range = args[3];
        if (range === 'base-a..takt/feature-a') {
          return '1';
        }
        if (range === 'base-a-remote..takt/feature-a') {
          return '5';
        }
        if (range === 'base-b..takt/feature-b') {
          return '1';
        }
        if (range === 'base-b-remote..takt/feature-b') {
          return '6';
        }
        throw new Error(`unexpected rev-list args: ${args.join(' ')}`);
      }

      if (args[0] === 'log' && args[1] === '--format=%s') {
        return 'takt: first';
      }

      throw new Error(`unexpected git args: ${args.join(' ')}`);
    });

    const cache = createBranchBaseResolutionCache();
    const baseA = resolveBranchBaseCommit('/project', 'main', 'takt/feature-a', cache);
    const baseB = resolveBranchBaseCommit('/project', 'main', 'takt/feature-b', cache);

    expect(baseA).toBe('base-a');
    expect(baseB).toBe('base-b');

    const forEachRefCalls = mockExecFileSync.mock.calls.filter(([, args]) =>
      args[0] === 'for-each-ref',
    );
    expect(forEachRefCalls).toHaveLength(1);
  });

  it('should skip reflog lookup when baseCommit is provided to findFirstTaktCommit', () => {
    mockExecFileSync.mockImplementation((cmd, args) => {
      if (cmd !== 'git') {
        throw new Error('unexpected command');
      }

      if (args[0] === 'reflog') {
        throw new Error('reflog should not be called');
      }

      if (args[0] === 'log' && args[1] === '--format=%H\t%s') {
        return 'abc123\ttakt: first instruction\n';
      }

      throw new Error(`unexpected git args: ${args.join(' ')}`);
    });

    const first = findFirstTaktCommit('/project', 'main', 'takt/feature-a', { baseCommit: 'base-a' });

    expect(first).toEqual({ subject: 'takt: first instruction' });
    expect(mockExecFileSync).not.toHaveBeenCalledWith(
      'git',
      ['reflog', 'show', '--format=%H', 'takt/feature-a'],
      expect.anything(),
    );
  });

  it('should reuse ref list cache across worktrees in the same repository', () => {
    mockExecFileSync.mockImplementation((cmd, args, options) => {
      if (cmd !== 'git') {
        throw new Error('unexpected command');
      }

      if (args[0] === 'reflog') {
        throw new Error('reflog unavailable');
      }

      if (args[0] === 'rev-parse' && args[1] === '--git-common-dir') {
        if (options?.cwd === '/repo/worktrees/a' || options?.cwd === '/repo/worktrees/b') {
          return '/repo/.git';
        }
        throw new Error(`unexpected rev-parse cwd: ${String(options?.cwd)}`);
      }

      if (args[0] === 'merge-base') {
        const baseRef = args[1];
        const branch = args[2];
        if (baseRef === 'main' || baseRef === 'origin/main') {
          throw new Error('priority refs unavailable');
        }
        if (baseRef === 'develop' && branch === 'takt/feature-a') {
          return 'base-a';
        }
        if (baseRef === 'origin/develop' && branch === 'takt/feature-a') {
          return 'base-a-remote';
        }
        if (baseRef === 'develop' && branch === 'takt/feature-b') {
          return 'base-b';
        }
        if (baseRef === 'origin/develop' && branch === 'takt/feature-b') {
          return 'base-b-remote';
        }
        throw new Error(`unexpected merge-base args: ${args.join(' ')}`);
      }

      if (args[0] === 'for-each-ref') {
        return 'develop\norigin/develop\n';
      }

      if (args[0] === 'rev-list') {
        const range = args[3];
        if (range === 'base-a..takt/feature-a') {
          return '1';
        }
        if (range === 'base-a-remote..takt/feature-a') {
          return '5';
        }
        if (range === 'base-b..takt/feature-b') {
          return '1';
        }
        if (range === 'base-b-remote..takt/feature-b') {
          return '6';
        }
        throw new Error(`unexpected rev-list args: ${args.join(' ')}`);
      }

      if (args[0] === 'log' && args[1] === '--format=%s') {
        return 'takt: first';
      }

      throw new Error(`unexpected git args: ${args.join(' ')}`);
    });

    const cache = createBranchBaseResolutionCache();
    const baseA = resolveBranchBaseCommit('/repo/worktrees/a', 'main', 'takt/feature-a', cache);
    const baseB = resolveBranchBaseCommit('/repo/worktrees/b', 'main', 'takt/feature-b', cache);

    expect(baseA).toBe('base-a');
    expect(baseB).toBe('base-b');

    const forEachRefCalls = mockExecFileSync.mock.calls.filter(([, args]) =>
      args[0] === 'for-each-ref',
    );
    expect(forEachRefCalls).toHaveLength(1);
  });
});
