/**
 * ExecutionOrchestrator Tests
 *
 * Tests phase completion transitions, especially when initiative has no branch.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExecutionOrchestrator } from './orchestrator.js';
import { ensureProjectClone } from '../git/project-clones.js';
import type { BranchManager } from '../git/branch-manager.js';
import type { AgentManager } from '../agent/types.js';
import type { AgentRepository } from '../db/repositories/agent-repository.js';

vi.mock('../git/project-clones.js', () => ({
  ensureProjectClone: vi.fn().mockResolvedValue('/tmp/test-workspace/clones/test'),
}));

vi.mock('./quality-review.js', () => ({
  shouldRunQualityReview: vi.fn().mockResolvedValue({ run: false, qualifyingFiles: [] }),
  runQualityReview: vi.fn().mockResolvedValue(undefined),
  computeQualifyingFiles: vi.fn().mockResolvedValue([]),
}));
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { TaskRepository } from '../db/repositories/task-repository.js';
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
import type { ProjectRepository } from '../db/repositories/project-repository.js';
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
import type { ConflictResolutionService } from '../coordination/conflict-resolution-service.js';
import type { EventBus, TaskCompletedEvent, AgentStoppedEvent, DomainEvent } from '../events/types.js';
import { shouldRunQualityReview, runQualityReview } from './quality-review.js';

function createMockEventBus(): EventBus & { handlers: Map<string, Function[]>; emitted: DomainEvent[] } {
  const handlers = new Map<string, Function[]>();
  const emitted: DomainEvent[] = [];
  return {
    handlers,
    emitted,
    emit: vi.fn((event: DomainEvent) => {
      emitted.push(event);
      const fns = handlers.get(event.type) ?? [];
      for (const fn of fns) fn(event);
    }),
    on: vi.fn((type: string, handler: Function) => {
      const fns = handlers.get(type) ?? [];
      fns.push(handler);
      handlers.set(type, fns);
    }),
    off: vi.fn(),
    once: vi.fn(),
  };
}

function createMocks() {
  const branchManager: BranchManager = {
    ensureBranch: vi.fn(),
    mergeBranch: vi.fn().mockResolvedValue({ success: true, message: 'merged', previousRef: 'abc000' }),
    diffBranches: vi.fn().mockResolvedValue(''),
    diffBranchesStat: vi.fn().mockResolvedValue([]),
    diffFileSingle: vi.fn().mockResolvedValue(''),
    getHeadCommitHash: vi.fn().mockResolvedValue('deadbeef00000000000000000000000000000000'),
    deleteBranch: vi.fn(),
    branchExists: vi.fn().mockResolvedValue(true),
    remoteBranchExists: vi.fn().mockResolvedValue(false),
    listCommits: vi.fn().mockResolvedValue([]),
    diffCommit: vi.fn().mockResolvedValue(''),
    getMergeBase: vi.fn().mockResolvedValue('abc123'),
    pushBranch: vi.fn(),
    checkMergeability: vi.fn().mockResolvedValue({ mergeable: true }),
    fetchRemote: vi.fn(),
    fastForwardBranch: vi.fn(),
    updateRef: vi.fn(),
  };

  const phaseRepository = {
    findById: vi.fn(),
    findByInitiativeId: vi.fn().mockResolvedValue([]),
    update: vi.fn().mockImplementation(async (id: string, data: any) => ({ id, ...data })),
    create: vi.fn(),
  } as unknown as PhaseRepository;

  const taskRepository = {
    findById: vi.fn(),
    findByPhaseId: vi.fn().mockResolvedValue([]),
    findByInitiativeId: vi.fn().mockResolvedValue([]),
  } as unknown as TaskRepository;

  const initiativeRepository = {
    findById: vi.fn(),
    findByStatus: vi.fn().mockResolvedValue([]),
    update: vi.fn(),
  } as unknown as InitiativeRepository;

  const projectRepository = {
    findProjectsByInitiativeId: vi.fn().mockResolvedValue([]),
  } as unknown as ProjectRepository;

  const phaseDispatchManager: PhaseDispatchManager = {
    queuePhase: vi.fn(),
    getNextDispatchablePhase: vi.fn().mockResolvedValue(null),
    dispatchNextPhase: vi.fn().mockResolvedValue({ success: false, phaseId: '', reason: 'none' }),
    completePhase: vi.fn(),
    blockPhase: vi.fn(),
    getPhaseQueueState: vi.fn().mockResolvedValue({ queued: [], ready: [], blocked: [] }),
  };

  const dispatchManager = {
    queue: vi.fn(),
    getNextDispatchable: vi.fn().mockResolvedValue(null),
    dispatchNext: vi.fn().mockResolvedValue({ success: false, taskId: '' }),
    completeTask: vi.fn(),
    blockTask: vi.fn(),
    retryBlockedTask: vi.fn(),
    getQueueState: vi.fn().mockResolvedValue({ queued: [], ready: [], blocked: [] }),
  } as unknown as DispatchManager;

  const conflictResolutionService: ConflictResolutionService = {
    handleConflict: vi.fn(),
  };

  const agentRepository = {
    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(),
  } as unknown as AgentRepository;

  const agentManager = {
    spawn: vi.fn().mockResolvedValue({ id: 'review-agent-1', name: 'review-agent' }),
    stop: vi.fn(),
    list: vi.fn().mockResolvedValue([]),
    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(),
  } as unknown as AgentManager;

  const eventBus = createMockEventBus();

  return {
    branchManager,
    phaseRepository,
    taskRepository,
    initiativeRepository,
    projectRepository,
    phaseDispatchManager,
    dispatchManager,
    conflictResolutionService,
    agentRepository,
    agentManager,
    eventBus,
  };
}

function createOrchestrator(mocks: ReturnType<typeof createMocks>, opts: { withAgentManager?: boolean; withAgentRepository?: boolean } = {}) {
  const orchestrator = new ExecutionOrchestrator(
    mocks.branchManager,
    mocks.phaseRepository,
    mocks.taskRepository,
    mocks.initiativeRepository,
    mocks.projectRepository,
    mocks.phaseDispatchManager,
    mocks.dispatchManager,
    mocks.conflictResolutionService,
    mocks.eventBus,
    '/tmp/test-workspace',
    opts.withAgentRepository !== false ? mocks.agentRepository : undefined,
    opts.withAgentManager !== false ? mocks.agentManager : undefined,
  );
  orchestrator.start();
  return orchestrator;
}

function emitTaskCompleted(eventBus: ReturnType<typeof createMockEventBus>, taskId: string) {
  const event: TaskCompletedEvent = {
    type: 'task:completed',
    timestamp: new Date(),
    payload: { taskId, agentId: 'agent-1', success: true, message: 'done' },
  };
  eventBus.emit(event);
}

describe('ExecutionOrchestrator', () => {
  let mocks: ReturnType<typeof createMocks>;

  beforeEach(() => {
    mocks = createMocks();
  });

  describe('phase completion when initiative has no branch', () => {
    it('should transition phase to pending_review in review mode even without a branch', async () => {
      const task = {
        id: 'task-1',
        phaseId: 'phase-1',
        initiativeId: 'init-1',
        category: 'execute',
        status: 'completed',
      };
      const phase = { id: 'phase-1', initiativeId: 'init-1', name: 'Phase 1', status: 'in_progress' };
      const initiative = { id: 'init-1', branch: null, executionMode: 'review_per_phase' };

      vi.mocked(mocks.taskRepository.findById).mockResolvedValue(task as any);
      vi.mocked(mocks.phaseRepository.findById).mockResolvedValue(phase as any);
      vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue(initiative as any);
      vi.mocked(mocks.taskRepository.findByPhaseId).mockResolvedValue([task] as any);

      createOrchestrator(mocks);

      emitTaskCompleted(mocks.eventBus, 'task-1');

      // Allow async handler to complete
      await vi.waitFor(() => {
        expect(mocks.phaseRepository.update).toHaveBeenCalledWith('phase-1', { status: 'pending_review' });
      });
    });

    it('should complete phase in yolo mode even without a branch', async () => {
      const task = {
        id: 'task-1',
        phaseId: 'phase-1',
        initiativeId: 'init-1',
        category: 'execute',
        status: 'completed',
      };
      const phase = { id: 'phase-1', initiativeId: 'init-1', name: 'Phase 1', status: 'in_progress' };
      const initiative = { id: 'init-1', branch: null, executionMode: 'yolo' };

      vi.mocked(mocks.taskRepository.findById).mockResolvedValue(task as any);
      vi.mocked(mocks.phaseRepository.findById).mockResolvedValue(phase as any);
      vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue(initiative as any);
      vi.mocked(mocks.initiativeRepository.findByStatus).mockResolvedValue([]);
      vi.mocked(mocks.taskRepository.findByPhaseId).mockResolvedValue([task] as any);
      vi.mocked(mocks.phaseRepository.findByInitiativeId).mockResolvedValue([phase] as any);

      createOrchestrator(mocks);

      emitTaskCompleted(mocks.eventBus, 'task-1');

      await vi.waitFor(() => {
        expect(mocks.phaseDispatchManager.completePhase).toHaveBeenCalledWith('phase-1');
      });

      // Should NOT have attempted any branch merges
      expect(mocks.branchManager.mergeBranch).not.toHaveBeenCalled();
    });
  });

  describe('phase completion when merge fails', () => {
    it('should still check phase completion even if task merge throws', async () => {
      const task = {
        id: 'task-1',
        phaseId: 'phase-1',
        initiativeId: 'init-1',
        category: 'execute',
        status: 'completed',
      };
      const phase = { id: 'phase-1', initiativeId: 'init-1', name: 'Phase 1', status: 'in_progress' };
      const initiative = { id: 'init-1', branch: 'cw/test', executionMode: 'review_per_phase' };
      const project = { id: 'proj-1', name: 'test', url: 'https://example.com', defaultBranch: 'main' };

      vi.mocked(mocks.taskRepository.findById).mockResolvedValue(task as any);
      vi.mocked(mocks.phaseRepository.findById).mockResolvedValue(phase as any);
      vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue(initiative as any);
      vi.mocked(mocks.taskRepository.findByPhaseId).mockResolvedValue([task] as any);
      vi.mocked(mocks.projectRepository.findProjectsByInitiativeId).mockResolvedValue([project] as any);
      // Merge fails
      vi.mocked(mocks.branchManager.mergeBranch).mockResolvedValue({
        success: false,
        message: 'conflict',
        conflicts: ['file.ts'],
      });

      createOrchestrator(mocks);

      emitTaskCompleted(mocks.eventBus, 'task-1');

      // Phase should still transition despite merge failure
      await vi.waitFor(() => {
        expect(mocks.phaseRepository.update).toHaveBeenCalledWith('phase-1', { status: 'pending_review' });
      });
    });
  });

  describe('phase completion with branch (normal flow)', () => {
    it('should merge task branch and transition phase when all tasks done', async () => {
      const task = {
        id: 'task-1',
        phaseId: 'phase-1',
        initiativeId: 'init-1',
        category: 'execute',
        status: 'completed',
      };
      const phase = { id: 'phase-1', initiativeId: 'init-1', name: 'Phase 1', status: 'in_progress' };
      const initiative = { id: 'init-1', branch: 'cw/test', executionMode: 'review_per_phase' };
      const project = { id: 'proj-1', name: 'test', url: 'https://example.com', defaultBranch: 'main' };

      vi.mocked(mocks.taskRepository.findById).mockResolvedValue(task as any);
      vi.mocked(mocks.phaseRepository.findById).mockResolvedValue(phase as any);
      vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue(initiative as any);
      vi.mocked(mocks.taskRepository.findByPhaseId).mockResolvedValue([task] as any);
      vi.mocked(mocks.projectRepository.findProjectsByInitiativeId).mockResolvedValue([project] as any);
      vi.mocked(mocks.branchManager.branchExists).mockResolvedValue(true);
      vi.mocked(mocks.branchManager.mergeBranch).mockResolvedValue({ success: true, message: 'ok' });

      createOrchestrator(mocks);

      emitTaskCompleted(mocks.eventBus, 'task-1');

      await vi.waitFor(() => {
        expect(mocks.phaseRepository.update).toHaveBeenCalledWith('phase-1', { status: 'pending_review' });
      });
    });

    it('should not transition phase when some tasks are still pending', async () => {
      const task1 = {
        id: 'task-1',
        phaseId: 'phase-1',
        initiativeId: 'init-1',
        category: 'execute',
        status: 'completed',
      };
      const task2 = {
        id: 'task-2',
        phaseId: 'phase-1',
        initiativeId: 'init-1',
        category: 'execute',
        status: 'pending',
      };
      const phase = { id: 'phase-1', initiativeId: 'init-1', name: 'Phase 1', status: 'in_progress' };
      const initiative = { id: 'init-1', branch: 'cw/test', executionMode: 'review_per_phase' };

      vi.mocked(mocks.taskRepository.findById).mockResolvedValue(task1 as any);
      vi.mocked(mocks.phaseRepository.findById).mockResolvedValue(phase as any);
      vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue(initiative as any);
      vi.mocked(mocks.taskRepository.findByPhaseId).mockResolvedValue([task1, task2] as any);
      vi.mocked(mocks.projectRepository.findProjectsByInitiativeId).mockResolvedValue([]);

      createOrchestrator(mocks);

      emitTaskCompleted(mocks.eventBus, 'task-1');

      // Give the async handler time to run
      await new Promise((r) => setTimeout(r, 50));

      expect(mocks.phaseRepository.update).not.toHaveBeenCalled();
      expect(mocks.phaseDispatchManager.completePhase).not.toHaveBeenCalled();
    });
  });

  describe('approveInitiative', () => {
    function setupApproveTest(mocks: ReturnType<typeof createMocks>) {
      const initiative = { id: 'init-1', branch: 'cw/test', status: 'pending_review' };
      const project = { id: 'proj-1', name: 'test', url: 'https://example.com', defaultBranch: 'main' };
      vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue(initiative as any);
      vi.mocked(mocks.projectRepository.findProjectsByInitiativeId).mockResolvedValue([project] as any);
      vi.mocked(mocks.branchManager.branchExists).mockResolvedValue(true);
      vi.mocked(mocks.branchManager.mergeBranch).mockResolvedValue({ success: true, message: 'ok', previousRef: 'abc000' });
      return { initiative, project };
    }

    it('should roll back merge when push fails', async () => {
      setupApproveTest(mocks);
      vi.mocked(mocks.branchManager.pushBranch).mockRejectedValue(new Error('non-fast-forward'));

      const orchestrator = createOrchestrator(mocks);

      await expect(orchestrator.approveInitiative('init-1', 'merge_and_push')).rejects.toThrow('non-fast-forward');

      // Should have rolled back the merge by restoring the previous ref
      expect(mocks.branchManager.updateRef).toHaveBeenCalledWith(
        expect.any(String),
        'main',
        'abc000',
      );

      // Should NOT have marked initiative as completed
      expect(mocks.initiativeRepository.update).not.toHaveBeenCalled();
    });

    it('should complete initiative when push succeeds', async () => {
      setupApproveTest(mocks);

      const orchestrator = createOrchestrator(mocks);

      await orchestrator.approveInitiative('init-1', 'merge_and_push');

      expect(mocks.branchManager.updateRef).not.toHaveBeenCalled();
      expect(mocks.initiativeRepository.update).toHaveBeenCalledWith('init-1', { status: 'completed' });
    });

    it('should not attempt rollback for push_branch strategy', async () => {
      setupApproveTest(mocks);
      vi.mocked(mocks.branchManager.pushBranch).mockRejectedValue(new Error('auth failed'));

      const orchestrator = createOrchestrator(mocks);

      await expect(orchestrator.approveInitiative('init-1', 'push_branch')).rejects.toThrow('auth failed');

      // No merge happened, so no rollback needed
      expect(mocks.branchManager.updateRef).not.toHaveBeenCalled();
    });
  });

  describe('handleAgentStopped quality review hook', () => {
    function emitAgentStopped(eventBus: ReturnType<typeof createMockEventBus>, payload: { taskId?: string; agentId: string; reason: AgentStoppedEvent['payload']['reason'] }) {
      const event: AgentStoppedEvent = {
        type: 'agent:stopped',
        timestamp: new Date(),
        payload: { taskId: payload.taskId ?? null, agentId: payload.agentId, name: 'test-agent', reason: payload.reason },
      };
      eventBus.emit(event);
    }

    function setupQualityReviewMocks() {
      const task = { id: 'task-1', phaseId: 'phase-1', initiativeId: 'init-1', category: 'execute', status: 'in_progress' };
      const initiative = { id: 'init-1', branch: 'cw/test-initiative', executionMode: 'yolo', qualityReview: true };
      const phase = { id: 'phase-1', initiativeId: 'init-1', name: 'Phase 1', status: 'in_progress' };
      const project = { id: 'proj-1', name: 'test', url: 'https://example.com', defaultBranch: 'main' };

      vi.mocked(mocks.taskRepository.findById).mockResolvedValue(task as any);
      vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue(initiative as any);
      vi.mocked(mocks.phaseRepository.findById).mockResolvedValue(phase as any);
      vi.mocked(mocks.projectRepository.findProjectsByInitiativeId).mockResolvedValue([project] as any);
      vi.mocked(ensureProjectClone).mockResolvedValue('/tmp/test-workspace/clones/test');
    }

    beforeEach(() => {
      vi.mocked(shouldRunQualityReview).mockClear().mockResolvedValue({ run: false, qualifyingFiles: [] });
      vi.mocked(runQualityReview).mockClear().mockResolvedValue(undefined);
    });

    it('should not call shouldRunQualityReview when reason is user_requested', async () => {
      createOrchestrator(mocks);

      emitAgentStopped(mocks.eventBus, { taskId: 'task-1', agentId: 'agent-1', reason: 'user_requested' });

      await new Promise((r) => setTimeout(r, 50));

      expect(shouldRunQualityReview).not.toHaveBeenCalled();
      expect(mocks.dispatchManager.completeTask).not.toHaveBeenCalled();
    });

    it('should call dispatchManager.completeTask when shouldRunQualityReview returns run: false', async () => {
      setupQualityReviewMocks();
      vi.mocked(shouldRunQualityReview).mockResolvedValue({ run: false, qualifyingFiles: [] });

      createOrchestrator(mocks);

      emitAgentStopped(mocks.eventBus, { taskId: 'task-1', agentId: 'agent-1', reason: 'task_complete' });

      await vi.waitFor(() => {
        expect(mocks.dispatchManager.completeTask).toHaveBeenCalledWith('task-1', 'agent-1');
      });
      expect(runQualityReview).not.toHaveBeenCalled();
    });

    it('should call runQualityReview and NOT call completeTask when shouldRunQualityReview returns run: true', async () => {
      setupQualityReviewMocks();
      vi.mocked(shouldRunQualityReview).mockResolvedValue({ run: true, qualifyingFiles: ['src/foo.ts'] });

      createOrchestrator(mocks);

      emitAgentStopped(mocks.eventBus, { taskId: 'task-1', agentId: 'agent-1', reason: 'task_complete' });

      await vi.waitFor(() => {
        expect(runQualityReview).toHaveBeenCalledWith(
          expect.objectContaining({
            taskId: 'task-1',
            qualifyingFiles: ['src/foo.ts'],
            taskRepository: mocks.taskRepository,
          }),
        );
      });
      expect(mocks.dispatchManager.completeTask).not.toHaveBeenCalled();
    });

    it('should fall back to completeTask when agentRepository is not available', async () => {
      createOrchestrator(mocks, { withAgentRepository: false, withAgentManager: false });

      emitAgentStopped(mocks.eventBus, { taskId: 'task-1', agentId: 'agent-1', reason: 'task_complete' });

      await vi.waitFor(() => {
        expect(mocks.dispatchManager.completeTask).toHaveBeenCalledWith('task-1', 'agent-1');
      });
      expect(shouldRunQualityReview).not.toHaveBeenCalled();
    });
  });
});

describe('handleAgentStopped — quality review integration', () => {
  let mocks: ReturnType<typeof createMocks>;

  beforeEach(() => {
    mocks = createMocks();
    vi.mocked(shouldRunQualityReview).mockReset();
    vi.mocked(runQualityReview).mockReset();
  });

  it('calls runQualityReview and skips completeTask when shouldRunQualityReview returns run:true', async () => {
    vi.mocked(shouldRunQualityReview).mockResolvedValue({
      run: true,
      qualifyingFiles: ['src/foo.ts'],
    });
    vi.mocked(runQualityReview).mockResolvedValue(undefined);

    // Provide task data for re-fetch inside runQualityReview branch
    vi.mocked(mocks.taskRepository.findById).mockResolvedValue({
      id: 't1',
      status: 'in_progress',
      initiativeId: 'i1',
      phaseId: 'p1',
    } as any);
    vi.mocked(mocks.initiativeRepository.findById).mockResolvedValue({
      id: 'i1',
      branch: 'cw/test',
      qualityReview: true,
    } as any);
    vi.mocked(mocks.phaseRepository.findById).mockResolvedValue({
      id: 'p1',
      name: 'impl',
      initiativeId: 'i1',
    } as any);
    vi.mocked(mocks.projectRepository.findProjectsByInitiativeId).mockResolvedValue([
      { id: 'proj-1', name: 'test', url: 'https://example.com', defaultBranch: 'main' },
    ] as any);

    createOrchestrator(mocks);

    mocks.eventBus.emit({
      type: 'agent:stopped',
      timestamp: new Date(),
      payload: { taskId: 't1', reason: 'task_complete', agentId: 'a1' },
    });

    await vi.waitFor(() => {
      expect(runQualityReview).toHaveBeenCalledWith(
        expect.objectContaining({ taskId: 't1', qualifyingFiles: ['src/foo.ts'] }),
      );
    });
    expect(mocks.dispatchManager.completeTask).not.toHaveBeenCalled();
  });

  it('calls completeTask and skips runQualityReview when shouldRunQualityReview returns run:false', async () => {
    vi.mocked(shouldRunQualityReview).mockResolvedValue({ run: false, qualifyingFiles: [] });
    vi.mocked(runQualityReview).mockResolvedValue(undefined);

    createOrchestrator(mocks);

    mocks.eventBus.emit({
      type: 'agent:stopped',
      timestamp: new Date(),
      payload: { taskId: 't1', reason: 'task_complete', agentId: 'a1' },
    });

    await vi.waitFor(() => {
      expect(mocks.dispatchManager.completeTask).toHaveBeenCalledWith('t1', 'a1');
    });
    expect(runQualityReview).not.toHaveBeenCalled();
  });

  it('skips both paths for user_requested reason', async () => {
    createOrchestrator(mocks);

    mocks.eventBus.emit({
      type: 'agent:stopped',
      timestamp: new Date(),
      payload: { taskId: 't1', reason: 'user_requested', agentId: 'a1' },
    });

    // Wait for scheduleDispatch to be triggered (dispatchNext is called in the cycle)
    await vi.waitFor(() => expect(mocks.dispatchManager.dispatchNext).toHaveBeenCalled());
    expect(shouldRunQualityReview).not.toHaveBeenCalled();
    expect(mocks.dispatchManager.completeTask).not.toHaveBeenCalled();
  });
});
