/**
 * Quality Review Service Tests
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { computeQualifyingFiles, shouldRunQualityReview, runQualityReview } from './quality-review.js';
import type { BranchManager } from '../git/branch-manager.js';
import type { AgentRepository } from '../db/repositories/agent-repository.js';
import type { TaskRepository } from '../db/repositories/task-repository.js';
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
import type { AgentManager } from '../agent/types.js';

function makeBranchManager(overrides: Partial<BranchManager> = {}): BranchManager {
  return {
    ensureBranch: vi.fn(),
    mergeBranch: vi.fn(),
    diffBranches: vi.fn(),
    diffBranchesStat: vi.fn().mockResolvedValue([]),
    diffFileSingle: vi.fn(),
    getHeadCommitHash: vi.fn(),
    deleteBranch: vi.fn(),
    branchExists: vi.fn(),
    remoteBranchExists: vi.fn(),
    listCommits: vi.fn(),
    diffCommit: vi.fn(),
    getMergeBase: vi.fn(),
    pushBranch: vi.fn(),
    checkMergeability: vi.fn(),
    fetchRemote: vi.fn(),
    fastForwardBranch: vi.fn(),
    updateRef: vi.fn(),
    ...overrides,
  };
}

function makeAgentRepository(overrides: Partial<AgentRepository> = {}): AgentRepository {
  return {
    create: vi.fn(),
    findById: vi.fn().mockResolvedValue(null),
    findByName: vi.fn(),
    findByTaskId: vi.fn(),
    findBySessionId: vi.fn(),
    findAll: vi.fn(),
    findByStatus: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
    ...overrides,
  } as unknown as AgentRepository;
}

function makeTaskRepository(overrides: Partial<TaskRepository> = {}): TaskRepository {
  return {
    create: vi.fn(),
    findById: vi.fn().mockResolvedValue(null),
    findByParentTaskId: vi.fn(),
    findByInitiativeId: vi.fn(),
    findByPhaseId: vi.fn(),
    update: vi.fn().mockResolvedValue({}),
    delete: vi.fn(),
    createDependency: vi.fn(),
    getDependencies: vi.fn(),
    ...overrides,
  } as unknown as TaskRepository;
}

function makeInitiativeRepository(overrides: Partial<InitiativeRepository> = {}): InitiativeRepository {
  return {
    create: vi.fn(),
    findById: vi.fn().mockResolvedValue(null),
    findAll: vi.fn(),
    findByStatus: vi.fn(),
    update: vi.fn(),
    findByProjectId: vi.fn(),
    delete: vi.fn(),
    ...overrides,
  } as unknown as InitiativeRepository;
}

function makeAgentManager(overrides: Partial<AgentManager> = {}): AgentManager {
  return {
    spawn: vi.fn().mockResolvedValue({ id: 'review-agent-1' }),
    stop: vi.fn(),
    list: vi.fn(),
    get: vi.fn(),
    getByName: vi.fn(),
    resume: vi.fn(),
    getResult: vi.fn(),
    getPendingQuestions: vi.fn(),
    delete: vi.fn(),
    dismiss: vi.fn(),
    resumeForConversation: vi.fn(),
    sendUserMessage: vi.fn(),
    ...overrides,
  } as unknown as AgentManager;
}

// ---------------------------------------------------------------------------
// computeQualifyingFiles
// ---------------------------------------------------------------------------

describe('computeQualifyingFiles', () => {
  it('includes .ts files', async () => {
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockResolvedValue([
        { path: 'src/foo.ts', status: 'modified', additions: 5, deletions: 2 },
      ]),
    });
    const result = await computeQualifyingFiles('/repo', 'task-branch', 'main', branchManager);
    expect(result).toEqual(['src/foo.ts']);
  });

  it('includes .tsx, .js, .css, .json and other non-excluded types', async () => {
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockResolvedValue([
        { path: 'src/App.tsx', status: 'modified', additions: 1, deletions: 0 },
        { path: 'src/utils.js', status: 'added', additions: 10, deletions: 0 },
        { path: 'src/style.css', status: 'modified', additions: 3, deletions: 1 },
        { path: 'config.json', status: 'modified', additions: 2, deletions: 2 },
      ]),
    });
    const result = await computeQualifyingFiles('/repo', 'task-branch', 'main', branchManager);
    expect(result).toEqual(['src/App.tsx', 'src/utils.js', 'src/style.css', 'config.json']);
  });

  it('excludes files ending with .gen.ts', async () => {
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockResolvedValue([
        { path: 'src/foo.ts', status: 'modified', additions: 5, deletions: 2 },
        { path: 'src/routes.gen.ts', status: 'modified', additions: 100, deletions: 0 },
        { path: 'types.gen.ts', status: 'added', additions: 50, deletions: 0 },
      ]),
    });
    const result = await computeQualifyingFiles('/repo', 'task-branch', 'main', branchManager);
    expect(result).toEqual(['src/foo.ts']);
  });

  it('excludes files under dist/', async () => {
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockResolvedValue([
        { path: 'src/foo.ts', status: 'modified', additions: 5, deletions: 2 },
        { path: 'dist/bundle.js', status: 'modified', additions: 1, deletions: 0 },
        { path: 'apps/server/dist/index.js', status: 'added', additions: 10, deletions: 0 },
        { path: 'packages/foo/dist/foo.js', status: 'modified', additions: 3, deletions: 0 },
      ]),
    });
    const result = await computeQualifyingFiles('/repo', 'task-branch', 'main', branchManager);
    expect(result).toEqual(['src/foo.ts']);
  });

  it('returns empty array when diff throws', async () => {
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockRejectedValue(new Error('branch not found')),
    });
    const result = await computeQualifyingFiles('/repo', 'task-branch', 'main', branchManager);
    expect(result).toEqual([]);
  });

  it('passes baseBranch as first branch arg and taskBranch as second to diffBranchesStat', async () => {
    const diffSpy = vi.fn().mockResolvedValue([]);
    const branchManager = makeBranchManager({ diffBranchesStat: diffSpy });
    await computeQualifyingFiles('/repo', 'task-branch', 'main', branchManager);
    expect(diffSpy).toHaveBeenCalledWith('/repo', 'main', 'task-branch');
  });
});

// ---------------------------------------------------------------------------
// shouldRunQualityReview
// ---------------------------------------------------------------------------

describe('shouldRunQualityReview', () => {
  const BASE_PARAMS = {
    agentId: 'agent-1',
    taskId: 'task-1',
    stopReason: 'task_complete',
    repoPath: '/repo',
    taskBranch: 'cw/init-task-task-1',
    baseBranch: 'main',
  };

  it('returns false when stopReason is not task_complete', async () => {
    const agentRepository = makeAgentRepository();
    const taskRepository = makeTaskRepository();
    const initiativeRepository = makeInitiativeRepository();
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      stopReason: 'error',
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
    // Should short-circuit: no repo lookups
    expect(agentRepository.findById).not.toHaveBeenCalled();
  });

  it('returns false when agent mode is errand', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'errand' }),
    });
    const taskRepository = makeTaskRepository();
    const initiativeRepository = makeInitiativeRepository();
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
    expect(taskRepository.findById).not.toHaveBeenCalled();
  });

  it('returns false when agent is not found', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue(null),
    });
    const taskRepository = makeTaskRepository();
    const initiativeRepository = makeInitiativeRepository();
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
  });

  it('returns false when task status is quality_review (recursion guard)', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue({
        id: 'task-1',
        status: 'quality_review',
        initiativeId: 'init-1',
      }),
    });
    const initiativeRepository = makeInitiativeRepository();
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
    expect(initiativeRepository.findById).not.toHaveBeenCalled();
  });

  it('returns false when task status is not in_progress and not quality_review', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue({
        id: 'task-1',
        status: 'completed',
        initiativeId: 'init-1',
      }),
    });
    const initiativeRepository = makeInitiativeRepository();
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
  });

  it('returns false when task is not found', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue(null),
    });
    const initiativeRepository = makeInitiativeRepository();
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
  });

  it('returns false when task has no initiativeId', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue({
        id: 'task-1',
        status: 'in_progress',
        initiativeId: null,
      }),
    });
    const initiativeRepository = makeInitiativeRepository();
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
    expect(initiativeRepository.findById).not.toHaveBeenCalled();
  });

  it('returns false when initiative.qualityReview is false', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue({
        id: 'task-1',
        status: 'in_progress',
        initiativeId: 'init-1',
      }),
    });
    const initiativeRepository = makeInitiativeRepository({
      findById: vi.fn().mockResolvedValue({ id: 'init-1', qualityReview: false }),
    });
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockResolvedValue([
        { path: 'src/foo.ts', status: 'modified', additions: 5, deletions: 2 },
      ]),
    });

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
    expect(branchManager.diffBranchesStat).not.toHaveBeenCalled();
  });

  it('returns false when initiative is not found', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue({
        id: 'task-1',
        status: 'in_progress',
        initiativeId: 'init-1',
      }),
    });
    const initiativeRepository = makeInitiativeRepository({
      findById: vi.fn().mockResolvedValue(null),
    });
    const branchManager = makeBranchManager();

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
  });

  it('returns false when no qualifying files in changeset', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue({
        id: 'task-1',
        status: 'in_progress',
        initiativeId: 'init-1',
      }),
    });
    const initiativeRepository = makeInitiativeRepository({
      findById: vi.fn().mockResolvedValue({ id: 'init-1', qualityReview: true }),
    });
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockResolvedValue([
        { path: 'dist/bundle.js', status: 'modified', additions: 10, deletions: 0 },
        { path: 'src/routes.gen.ts', status: 'added', additions: 50, deletions: 0 },
      ]),
    });

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: false, qualifyingFiles: [] });
  });

  it('returns true with qualifying files when all conditions pass', async () => {
    const agentRepository = makeAgentRepository({
      findById: vi.fn().mockResolvedValue({ id: 'agent-1', mode: 'execute' }),
    });
    const taskRepository = makeTaskRepository({
      findById: vi.fn().mockResolvedValue({
        id: 'task-1',
        status: 'in_progress',
        initiativeId: 'init-1',
      }),
    });
    const initiativeRepository = makeInitiativeRepository({
      findById: vi.fn().mockResolvedValue({ id: 'init-1', qualityReview: true }),
    });
    const branchManager = makeBranchManager({
      diffBranchesStat: vi.fn().mockResolvedValue([
        { path: 'src/foo.ts', status: 'modified', additions: 5, deletions: 2 },
        { path: 'src/bar.ts', status: 'added', additions: 20, deletions: 0 },
      ]),
    });

    const result = await shouldRunQualityReview({
      ...BASE_PARAMS,
      agentRepository,
      taskRepository,
      initiativeRepository,
      branchManager,
    });

    expect(result).toEqual({ run: true, qualifyingFiles: ['src/foo.ts', 'src/bar.ts'] });
  });
});

// ---------------------------------------------------------------------------
// runQualityReview
// ---------------------------------------------------------------------------

describe('runQualityReview', () => {
  const BASE_RUN_PARAMS = {
    taskId: 'task-1',
    taskBranch: 'cw/init-task-task-1',
    baseBranch: 'main',
    initiativeId: 'init-1',
    qualifyingFiles: ['src/foo.ts', 'src/bar.ts'],
  };

  const makeLog = () => ({
    info: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
    trace: vi.fn(),
    fatal: vi.fn(),
    child: vi.fn(),
  });

  it('transitions task status to quality_review before spawning', async () => {
    const taskRepository = makeTaskRepository();
    const agentManager = makeAgentManager();
    const log = makeLog();

    await runQualityReview({
      ...BASE_RUN_PARAMS,
      taskRepository,
      agentManager,
      log: log as any,
    });

    const updateCalls = vi.mocked(taskRepository.update).mock.calls;
    expect(updateCalls[0]).toEqual(['task-1', { status: 'quality_review' }]);
    // spawn should come after the update
    expect(agentManager.spawn).toHaveBeenCalled();
  });

  it('calls agentManager.spawn with mode execute and correct branchName', async () => {
    const taskRepository = makeTaskRepository();
    const agentManager = makeAgentManager();
    const log = makeLog();

    await runQualityReview({
      ...BASE_RUN_PARAMS,
      taskRepository,
      agentManager,
      log: log as any,
    });

    expect(agentManager.spawn).toHaveBeenCalledWith(
      expect.objectContaining({
        taskId: 'task-1',
        initiativeId: 'init-1',
        mode: 'execute',
        baseBranch: 'main',
        branchName: 'cw/init-task-task-1',
      }),
    );
  });

  it('prompt includes /simplify instruction and qualifying files', async () => {
    const taskRepository = makeTaskRepository();
    const agentManager = makeAgentManager();
    const log = makeLog();

    await runQualityReview({
      ...BASE_RUN_PARAMS,
      taskRepository,
      agentManager,
      log: log as any,
    });

    const spawnCall = vi.mocked(agentManager.spawn).mock.calls[0][0];
    expect(spawnCall.prompt).toContain('/simplify');
    expect(spawnCall.prompt).toContain('src/foo.ts');
    expect(spawnCall.prompt).toContain('src/bar.ts');
  });

  it('logs reviewAgentId at info level after spawn', async () => {
    const taskRepository = makeTaskRepository();
    const agentManager = makeAgentManager({
      spawn: vi.fn().mockResolvedValue({ id: 'review-agent-42' }),
    });
    const log = makeLog();

    await runQualityReview({
      ...BASE_RUN_PARAMS,
      taskRepository,
      agentManager,
      log: log as any,
    });

    expect(log.info).toHaveBeenCalledWith(
      expect.objectContaining({ taskId: 'task-1', reviewAgentId: 'review-agent-42' }),
      expect.any(String),
    );
  });

  it('on spawn failure: transitions task to completed and does not throw', async () => {
    const taskRepository = makeTaskRepository();
    const agentManager = makeAgentManager({
      spawn: vi.fn().mockRejectedValue(new Error('spawn failed')),
    });
    const log = makeLog();

    await expect(
      runQualityReview({
        ...BASE_RUN_PARAMS,
        taskRepository,
        agentManager,
        log: log as any,
      }),
    ).resolves.toBeUndefined();

    expect(log.error).toHaveBeenCalled();
    expect(taskRepository.update).toHaveBeenCalledWith('task-1', { status: 'completed' });
  });
});
