/**
 * Tests for Test Harness
 *
 * Proves that the test harness enables E2E testing scenarios.
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
  createTestHarness,
  SIMPLE_FIXTURE,
  PARALLEL_FIXTURE,
  COMPLEX_FIXTURE,
  type TestHarness,
} from './index.js';
import { taskDependencies } from '../db/schema.js';
import { eq } from 'drizzle-orm';

describe('TestHarness', () => {
  let harness: TestHarness;

  beforeEach(() => {
    harness = createTestHarness();
  });

  afterEach(() => {
    harness.cleanup();
    vi.useRealTimers();
  });

  describe('createTestHarness', () => {
    it('returns all components', () => {
      expect(harness.db).toBeDefined();
      expect(harness.eventBus).toBeDefined();
      expect(harness.agentManager).toBeDefined();
      expect(harness.worktreeManager).toBeDefined();
      expect(harness.dispatchManager).toBeDefined();
      expect(harness.coordinationManager).toBeDefined();
      expect(harness.taskRepository).toBeDefined();
      expect(harness.messageRepository).toBeDefined();
      expect(harness.agentRepository).toBeDefined();
    });

    it('provides helper methods', () => {
      expect(typeof harness.seedFixture).toBe('function');
      expect(typeof harness.setAgentScenario).toBe('function');
      expect(typeof harness.setAgentQuestion).toBe('function');
      expect(typeof harness.setAgentQuestions).toBe('function');
      expect(typeof harness.getEventsByType).toBe('function');
      expect(typeof harness.clearEvents).toBe('function');
      expect(typeof harness.cleanup).toBe('function');
    });
  });

  describe('setAgentQuestion convenience helper', () => {
    it('wraps single question in array format', async () => {
      vi.useFakeTimers();

      // Set single question using convenience method
      harness.setAgentQuestion('test-agent', 'q1', 'Which option?', [
        { label: 'Option A', description: 'First option' },
        { label: 'Option B', description: 'Second option' },
      ]);

      // Spawn agent with that scenario
      const agent = await harness.agentManager.spawn({
        name: 'test-agent',
        taskId: 'task-1',
        prompt: 'test',
      });

      await harness.advanceTimers();

      // Verify questions array format
      const pending = await harness.getPendingQuestions(agent.id);
      expect(pending).not.toBeNull();
      expect(pending?.questions).toHaveLength(1);
      expect(pending?.questions[0].id).toBe('q1');
      expect(pending?.questions[0].question).toBe('Which option?');
      expect(pending?.questions[0].options).toHaveLength(2);
    });
  });

  describe('seedFixture', () => {
    it('creates task hierarchy from SIMPLE_FIXTURE', async () => {
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);

      // Check initiative created
      expect(seeded.initiativeId).toBeDefined();

      // Check phases created
      expect(seeded.phases.size).toBe(1);
      expect(seeded.phases.has('Phase 1')).toBe(true);

      // Check task groups created
      expect(seeded.taskGroups.size).toBe(1);
      expect(seeded.taskGroups.has('Task Group 1')).toBe(true);

      // Check tasks created
      expect(seeded.tasks.size).toBe(3);
      expect(seeded.tasks.has('Task A')).toBe(true);
      expect(seeded.tasks.has('Task B')).toBe(true);
      expect(seeded.tasks.has('Task C')).toBe(true);
    });

    it('returns correct IDs that exist in database', async () => {
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);

      // Verify task exists in database
      const taskAId = seeded.tasks.get('Task A')!;
      const taskA = await harness.taskRepository.findById(taskAId);
      expect(taskA).not.toBeNull();
      expect(taskA?.name).toBe('Task A');
    });

    it('creates PARALLEL_FIXTURE correctly', async () => {
      const seeded = await harness.seedFixture(PARALLEL_FIXTURE);

      expect(seeded.phases.size).toBe(1);
      expect(seeded.taskGroups.size).toBe(2);
      expect(seeded.tasks.size).toBe(4);
      expect(seeded.tasks.has('Task X')).toBe(true);
      expect(seeded.tasks.has('Task Q')).toBe(true);
    });

    it('creates COMPLEX_FIXTURE correctly', async () => {
      const seeded = await harness.seedFixture(COMPLEX_FIXTURE);

      expect(seeded.phases.size).toBe(2);
      expect(seeded.taskGroups.size).toBe(4);
      expect(seeded.tasks.size).toBe(5);
    });
  });

  describe('task dependencies', () => {
    it('resolves dependencies correctly (dependsOn contains actual task IDs)', async () => {
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);

      const taskAId = seeded.tasks.get('Task A')!;
      const taskBId = seeded.tasks.get('Task B')!;

      // Query task_dependencies table directly
      const deps = await harness.db
        .select()
        .from(taskDependencies)
        .where(eq(taskDependencies.taskId, taskBId));

      expect(deps.length).toBe(1);
      expect(deps[0].dependsOnTaskId).toBe(taskAId);
    });

    it('creates multiple dependencies for a task', async () => {
      const seeded = await harness.seedFixture(COMPLEX_FIXTURE);

      // Task 4A depends on both Task 2A and Task 3A
      const task4AId = seeded.tasks.get('Task 4A')!;
      const task2AId = seeded.tasks.get('Task 2A')!;
      const task3AId = seeded.tasks.get('Task 3A')!;

      const deps = await harness.db
        .select()
        .from(taskDependencies)
        .where(eq(taskDependencies.taskId, task4AId));

      expect(deps.length).toBe(2);
      const depIds = deps.map((d) => d.dependsOnTaskId);
      expect(depIds).toContain(task2AId);
      expect(depIds).toContain(task3AId);
    });
  });

  describe('event capture', () => {
    it('captures events via getEventsByType', async () => {
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
      const taskAId = seeded.tasks.get('Task A')!;

      // Queue a task (emits task:queued event)
      await harness.dispatchManager.queue(taskAId);

      const events = harness.getEventsByType('task:queued');
      expect(events.length).toBe(1);
      expect((events[0].payload as { taskId: string }).taskId).toBe(taskAId);
    });

    it('clears events via clearEvents', async () => {
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
      const taskAId = seeded.tasks.get('Task A')!;

      await harness.dispatchManager.queue(taskAId);
      expect(harness.getEventsByType('task:queued').length).toBe(1);

      harness.clearEvents();
      expect(harness.getEventsByType('task:queued').length).toBe(0);
    });
  });

  describe('dispatch flow', () => {
    it('dispatchManager.queue() + dispatchNext() uses MockAgentManager', async () => {
      vi.useFakeTimers();
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
      const taskAId = seeded.tasks.get('Task A')!;

      // Note: DispatchManager.dispatchNext() requires an idle agent in the pool
      // before it will spawn a new agent. Pre-seed an idle agent.
      await harness.agentManager.spawn({
        name: 'pool-agent',
        taskId: 'placeholder',
        prompt: 'placeholder',
      });
      // Wait for agent to complete and become idle
      await harness.advanceTimers();

      // Queue the task
      await harness.dispatchManager.queue(taskAId);

      // Clear events from queue and agent spawn
      harness.clearEvents();

      // Dispatch the task
      const result = await harness.dispatchManager.dispatchNext();

      // Advance timers to trigger mock agent completion
      await harness.advanceTimers();

      expect(result.success).toBe(true);
      expect(result.taskId).toBe(taskAId);
      expect(result.agentId).toBeDefined();

      // Should have emitted task:dispatched
      const dispatchedEvents = harness.getEventsByType('task:dispatched');
      expect(dispatchedEvents.length).toBe(1);
    });

    it('returns failure when no tasks are queued', async () => {
      const result = await harness.dispatchManager.dispatchNext();

      expect(result.success).toBe(false);
      expect(result.reason).toBe('No dispatchable tasks');
    });

    it('returns failure when no idle agents available', async () => {
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
      const taskAId = seeded.tasks.get('Task A')!;

      // Queue the task but don't pre-seed any agents
      await harness.dispatchManager.queue(taskAId);

      // Dispatch without any agents in pool
      const result = await harness.dispatchManager.dispatchNext();

      expect(result.success).toBe(false);
      expect(result.reason).toBe('No available agents');
    });
  });

  describe('agent completion triggers events', () => {
    it('agent completion emits agent:stopped event', async () => {
      vi.useFakeTimers();
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
      const taskAId = seeded.tasks.get('Task A')!;

      // Pre-seed an idle agent (required by DispatchManager)
      await harness.agentManager.spawn({
        name: 'pool-agent',
        taskId: 'placeholder',
        prompt: 'placeholder',
      });
      await harness.advanceTimers();
      harness.clearEvents();

      // Queue and dispatch
      await harness.dispatchManager.queue(taskAId);
      harness.clearEvents();
      await harness.dispatchManager.dispatchNext();

      // Should have agent:spawned
      const spawnedEvents = harness.getEventsByType('agent:spawned');
      expect(spawnedEvents.length).toBe(1);

      // Advance timers to trigger completion
      await harness.advanceTimers();

      // Should have agent:stopped
      const stoppedEvents = harness.getEventsByType('agent:stopped');
      expect(stoppedEvents.length).toBe(1);
    });

    it('custom scenario affects agent behavior', async () => {
      vi.useFakeTimers();
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
      const taskAId = seeded.tasks.get('Task A')!;

      // Pre-seed an idle agent (required by DispatchManager)
      await harness.agentManager.spawn({
        name: 'pool-agent',
        taskId: 'placeholder',
        prompt: 'placeholder',
      });
      await harness.advanceTimers();
      harness.clearEvents();

      // Set error scenario for the agent that will be spawned
      harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, {
        status: 'error',
        delay: 0,
        error: 'Test crash',
      });

      // Queue and dispatch
      await harness.dispatchManager.queue(taskAId);
      harness.clearEvents();
      await harness.dispatchManager.dispatchNext();

      // Advance timers
      await harness.advanceTimers();

      // Should have agent:crashed
      const crashedEvents = harness.getEventsByType('agent:crashed');
      expect(crashedEvents.length).toBe(1);
    });
  });

  describe('full dispatch -> complete -> merge flow', () => {
    it('works end-to-end', async () => {
      vi.useFakeTimers();
      const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
      const taskAId = seeded.tasks.get('Task A')!;

      // Pre-seed an idle agent (required by DispatchManager)
      await harness.agentManager.spawn({
        name: 'pool-agent',
        taskId: 'placeholder',
        prompt: 'placeholder',
      });
      await harness.advanceTimers();
      harness.clearEvents();

      // Step 1: Queue task
      await harness.dispatchManager.queue(taskAId);

      // Step 2: Dispatch task
      const dispatchResult = await harness.dispatchManager.dispatchNext();
      expect(dispatchResult.success).toBe(true);

      // Advance timers for agent completion
      await harness.advanceTimers();

      // Clear events for cleaner verification
      harness.clearEvents();

      // Step 3: Complete task
      await harness.dispatchManager.completeTask(taskAId);

      // Verify task:completed event
      const completedEvents = harness.getEventsByType('task:completed');
      expect(completedEvents.length).toBe(1);
      expect((completedEvents[0].payload as { taskId: string }).taskId).toBe(taskAId);

      // Step 4: Verify task status in database
      const task = await harness.taskRepository.findById(taskAId);
      expect(task?.status).toBe('completed');
    });
  });

  describe('MockWorktreeManager', () => {
    it('creates fake worktrees', async () => {
      const worktree = await harness.worktreeManager.create('wt-1', 'feature-1');

      expect(worktree.id).toBe('wt-1');
      expect(worktree.branch).toBe('feature-1');
      expect(worktree.path).toContain('wt-1');
    });

    it('merge returns success by default', async () => {
      await harness.worktreeManager.create('wt-1', 'feature-1');
      const result = await harness.worktreeManager.merge('wt-1', 'main');

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

    it('allows setting custom merge results', async () => {
      await harness.worktreeManager.create('wt-1', 'feature-1');
      harness.worktreeManager.setMergeResult('wt-1', {
        success: false,
        conflicts: ['file1.ts', 'file2.ts'],
        message: 'Merge conflict',
      });

      const result = await harness.worktreeManager.merge('wt-1', 'main');

      expect(result.success).toBe(false);
      expect(result.conflicts).toEqual(['file1.ts', 'file2.ts']);
    });
  });
});
