/**
 * DefaultPhaseDispatchManager Tests
 *
 * Tests for the PhaseDispatchManager adapter with dependency checking
 * and queue management.
 */

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { DefaultPhaseDispatchManager } from './phase-manager.js';
import { DrizzlePhaseRepository } from '../db/repositories/drizzle/phase.js';
import { DrizzleTaskRepository } from '../db/repositories/drizzle/task.js';
import { DrizzleInitiativeRepository } from '../db/repositories/drizzle/initiative.js';
import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js';
import type { DrizzleDatabase } from '../db/index.js';
import type { EventBus, DomainEvent } from '../events/types.js';
import type { DispatchManager } from './types.js';

// =============================================================================
// Test Helpers
// =============================================================================

/**
 * Create a mock EventBus that captures emitted events.
 */
function createMockEventBus(): EventBus & { emittedEvents: DomainEvent[] } {
  const emittedEvents: DomainEvent[] = [];

  return {
    emittedEvents,
    emit<T extends DomainEvent>(event: T): void {
      emittedEvents.push(event);
    },
    on: vi.fn(),
    off: vi.fn(),
    once: vi.fn(),
  };
}

// =============================================================================
// Tests
// =============================================================================

/**
 * Create a mock DispatchManager (stub, not used in phase dispatch tests).
 */
function createMockDispatchManager(): DispatchManager {
  return {
    queue: vi.fn(),
    getNextDispatchable: vi.fn().mockResolvedValue(null),
    dispatchNext: vi.fn().mockResolvedValue({ success: false, reason: 'mock' }),
    completeTask: vi.fn(),
    blockTask: vi.fn(),
    retryBlockedTask: vi.fn(),
    getQueueState: vi.fn().mockResolvedValue({ queued: [], ready: [], blocked: [] }),
  };
}

describe('DefaultPhaseDispatchManager', () => {
  let db: DrizzleDatabase;
  let phaseRepository: DrizzlePhaseRepository;
  let taskRepository: DrizzleTaskRepository;
  let initiativeRepository: DrizzleInitiativeRepository;
  let eventBus: EventBus & { emittedEvents: DomainEvent[] };
  let dispatchManager: DispatchManager;
  let phaseDispatchManager: DefaultPhaseDispatchManager;
  let testInitiativeId: string;

  beforeEach(async () => {
    // Set up test database
    db = createTestDatabase();
    phaseRepository = new DrizzlePhaseRepository(db);
    taskRepository = new DrizzleTaskRepository(db);
    initiativeRepository = new DrizzleInitiativeRepository(db);

    // Create required initiative for phases
    const initiative = await initiativeRepository.create({
      name: 'Test Initiative',
    });
    testInitiativeId = initiative.id;

    // Create mock event bus and dispatch manager
    eventBus = createMockEventBus();
    dispatchManager = createMockDispatchManager();

    // Create phase dispatch manager
    phaseDispatchManager = new DefaultPhaseDispatchManager(
      phaseRepository,
      taskRepository,
      dispatchManager,
      eventBus
    );
  });

  // ===========================================================================
  // queuePhase() Tests
  // ===========================================================================

  describe('queuePhase', () => {
    it('should add phase to queue', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);

      const state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.queued.length).toBe(1);
      expect(state.queued[0].phaseId).toBe(phase.id);
      expect(state.queued[0].initiativeId).toBe(testInitiativeId);
    });

    it('should emit PhaseQueuedEvent', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);

      // Check event was emitted
      expect(eventBus.emittedEvents.length).toBe(1);
      expect(eventBus.emittedEvents[0].type).toBe('phase:queued');
      expect((eventBus.emittedEvents[0] as any).payload.phaseId).toBe(phase.id);
      expect((eventBus.emittedEvents[0] as any).payload.initiativeId).toBe(testInitiativeId);
    });

    it('should include dependencies in queued phase', async () => {
      const phase1 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      await phaseRepository.update(phase2.id, { status: 'approved' as const });

      // Phase 2 depends on Phase 1
      await phaseRepository.createDependency(phase2.id, phase1.id);

      await phaseDispatchManager.queuePhase(phase2.id);

      const state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.queued[0].dependsOn).toContain(phase1.id);

      // Event should also include dependencies
      expect((eventBus.emittedEvents[0] as any).payload.dependsOn).toContain(phase1.id);
    });

    it('should throw error when phase not found', async () => {
      await expect(phaseDispatchManager.queuePhase('non-existent-id')).rejects.toThrow(
        'Phase not found'
      );
    });

    it('should reject non-approved phase', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Pending Phase',
        status: 'pending',
      });

      await expect(phaseDispatchManager.queuePhase(phase.id)).rejects.toThrow(
        'must be approved before queuing'
      );
    });

    it('should reject in_progress phase', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'In Progress Phase',
        status: 'in_progress',
      });

      await expect(phaseDispatchManager.queuePhase(phase.id)).rejects.toThrow(
        'must be approved before queuing'
      );
    });
  });

  // ===========================================================================
  // getNextDispatchablePhase() Tests
  // ===========================================================================

  describe('getNextDispatchablePhase', () => {
    it('should return null when queue empty', async () => {
      const next = await phaseDispatchManager.getNextDispatchablePhase();
      expect(next).toBeNull();
    });

    it('should return phase with no dependencies first', async () => {
      const phase1 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1 (no deps)',
      });
      const phase2 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2 (depends on 1)',
      });
      await phaseRepository.update(phase1.id, { status: 'approved' as const });
      await phaseRepository.update(phase2.id, { status: 'approved' as const });

      // Phase 2 depends on Phase 1
      await phaseRepository.createDependency(phase2.id, phase1.id);

      // Queue both phases (phase 2 first, then phase 1)
      await phaseDispatchManager.queuePhase(phase2.id);
      await phaseDispatchManager.queuePhase(phase1.id);

      // Should return phase 1 since phase 2 has incomplete dependency
      const next = await phaseDispatchManager.getNextDispatchablePhase();
      expect(next).not.toBeNull();
      expect(next!.phaseId).toBe(phase1.id);
    });

    it('should skip phases with incomplete dependencies', async () => {
      const phase1 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
        status: 'pending', // Not completed
      });
      const phase2 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      await phaseRepository.update(phase2.id, { status: 'approved' as const });

      // Phase 2 depends on Phase 1
      await phaseRepository.createDependency(phase2.id, phase1.id);

      // Queue only phase 2 (phase 1 is not queued or completed)
      await phaseDispatchManager.queuePhase(phase2.id);

      // Should return null since phase 2's dependency (phase 1) is not complete
      const next = await phaseDispatchManager.getNextDispatchablePhase();
      expect(next).toBeNull();
    });

    it('should return oldest phase when multiple ready', async () => {
      const phase1 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 1',
      });
      const phase2 = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase 2',
      });
      await phaseRepository.update(phase1.id, { status: 'approved' as const });
      await phaseRepository.update(phase2.id, { status: 'approved' as const });

      // Queue phase1 first, then phase2
      await phaseDispatchManager.queuePhase(phase1.id);
      await new Promise((resolve) => setTimeout(resolve, 10)); // Small delay
      await phaseDispatchManager.queuePhase(phase2.id);

      // Should return phase 1 (queued first)
      const next = await phaseDispatchManager.getNextDispatchablePhase();
      expect(next).not.toBeNull();
      expect(next!.phaseId).toBe(phase1.id);
    });
  });

  // ===========================================================================
  // dispatchNextPhase() Tests
  // ===========================================================================

  describe('dispatchNextPhase', () => {
    it('should update phase status to in_progress', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      const result = await phaseDispatchManager.dispatchNextPhase();

      expect(result.success).toBe(true);
      expect(result.phaseId).toBe(phase.id);

      // Check phase status updated
      const updatedPhase = await phaseRepository.findById(phase.id);
      expect(updatedPhase!.status).toBe('in_progress');
    });

    it('should emit PhaseStartedEvent', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      await phaseDispatchManager.dispatchNextPhase();

      // Find PhaseStartedEvent (events include queued + started)
      const startedEvent = eventBus.emittedEvents.find(
        (e) => e.type === 'phase:started'
      );
      expect(startedEvent).toBeDefined();
      expect((startedEvent as any).payload.phaseId).toBe(phase.id);
      expect((startedEvent as any).payload.initiativeId).toBe(testInitiativeId);
    });

    it('should return failure when no dispatchable phases', async () => {
      const result = await phaseDispatchManager.dispatchNextPhase();

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

    it('should remove phase from queue after dispatch', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      await phaseDispatchManager.dispatchNextPhase();

      const state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.queued.length).toBe(0);
    });
  });

  // ===========================================================================
  // completePhase() Tests
  // ===========================================================================

  describe('completePhase', () => {
    it('should update phase status to completed', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
        status: 'in_progress',
      });

      await phaseDispatchManager.completePhase(phase.id);

      const updatedPhase = await phaseRepository.findById(phase.id);
      expect(updatedPhase!.status).toBe('completed');
    });

    it('should remove from queue', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      await phaseDispatchManager.completePhase(phase.id);

      const state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.queued.length).toBe(0);
    });

    it('should emit PhaseCompletedEvent', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
        status: 'in_progress',
      });

      await phaseDispatchManager.completePhase(phase.id);

      // Find PhaseCompletedEvent
      const completedEvent = eventBus.emittedEvents.find(
        (e) => e.type === 'phase:completed'
      );
      expect(completedEvent).toBeDefined();
      expect((completedEvent as any).payload.phaseId).toBe(phase.id);
      expect((completedEvent as any).payload.initiativeId).toBe(testInitiativeId);
      expect((completedEvent as any).payload.success).toBe(true);
    });

    it('should throw error when phase not found', async () => {
      await expect(phaseDispatchManager.completePhase('non-existent-id')).rejects.toThrow(
        'Phase not found'
      );
    });
  });

  // ===========================================================================
  // blockPhase() Tests
  // ===========================================================================

  describe('blockPhase', () => {
    it('should update phase status', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      await phaseDispatchManager.blockPhase(phase.id, 'Waiting for user input');

      const updatedPhase = await phaseRepository.findById(phase.id);
      expect(updatedPhase!.status).toBe('blocked');
    });

    it('should add to blocked list', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      await phaseDispatchManager.blockPhase(phase.id, 'Waiting for user input');

      const state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.blocked.length).toBe(1);
      expect(state.blocked[0].phaseId).toBe(phase.id);
      expect(state.blocked[0].reason).toBe('Waiting for user input');
    });

    it('should emit PhaseBlockedEvent', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      await phaseDispatchManager.blockPhase(phase.id, 'External dependency');

      // Find PhaseBlockedEvent (events include queued + blocked)
      const blockedEvent = eventBus.emittedEvents.find(
        (e) => e.type === 'phase:blocked'
      );
      expect(blockedEvent).toBeDefined();
      expect((blockedEvent as any).payload.phaseId).toBe(phase.id);
      expect((blockedEvent as any).payload.reason).toBe('External dependency');
    });

    it('should remove from queue when blocked', async () => {
      const phase = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Test Phase',
      });
      await phaseRepository.update(phase.id, { status: 'approved' as const });

      await phaseDispatchManager.queuePhase(phase.id);
      await phaseDispatchManager.blockPhase(phase.id, 'Some reason');

      const state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.queued.length).toBe(0);
    });
  });

  // ===========================================================================
  // Dependency Scenario Test
  // ===========================================================================

  describe('dependency scenario', () => {
    it('should dispatch phases in correct dependency order', async () => {
      // Create a diamond dependency:
      // Phase A (no deps) -> Phase B & C -> Phase D (depends on B and C)
      const phaseA = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase A - Foundation',
      });
      const phaseB = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase B - Build on A',
      });
      const phaseC = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase C - Also build on A',
      });
      const phaseD = await phaseRepository.create({
        initiativeId: testInitiativeId,
        name: 'Phase D - Needs B and C',
      });
      await phaseRepository.update(phaseA.id, { status: 'approved' as const });
      await phaseRepository.update(phaseB.id, { status: 'approved' as const });
      await phaseRepository.update(phaseC.id, { status: 'approved' as const });
      await phaseRepository.update(phaseD.id, { status: 'approved' as const });

      // Set up dependencies
      await phaseRepository.createDependency(phaseB.id, phaseA.id);
      await phaseRepository.createDependency(phaseC.id, phaseA.id);
      await phaseRepository.createDependency(phaseD.id, phaseB.id);
      await phaseRepository.createDependency(phaseD.id, phaseC.id);

      // Queue all four phases
      await phaseDispatchManager.queuePhase(phaseA.id);
      await phaseDispatchManager.queuePhase(phaseB.id);
      await phaseDispatchManager.queuePhase(phaseC.id);
      await phaseDispatchManager.queuePhase(phaseD.id);

      // Initially only A should be dispatchable
      let state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.ready.length).toBe(1);
      expect(state.ready[0].phaseId).toBe(phaseA.id);

      // Dispatch A
      let result = await phaseDispatchManager.dispatchNextPhase();
      expect(result.success).toBe(true);
      expect(result.phaseId).toBe(phaseA.id);

      // Complete A
      await phaseDispatchManager.completePhase(phaseA.id);

      // Now B and C should be dispatchable (both have A completed)
      state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.ready.length).toBe(2);
      const readyIds = state.ready.map((p) => p.phaseId);
      expect(readyIds).toContain(phaseB.id);
      expect(readyIds).toContain(phaseC.id);

      // D should still not be ready (needs both B and C completed)
      expect(readyIds).not.toContain(phaseD.id);

      // Dispatch and complete B
      result = await phaseDispatchManager.dispatchNextPhase();
      expect(result.success).toBe(true);
      await phaseDispatchManager.completePhase(result.phaseId!);

      // D still not ready (needs C completed too)
      state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.ready.length).toBe(1); // Only C is ready now

      // Dispatch and complete C
      result = await phaseDispatchManager.dispatchNextPhase();
      expect(result.success).toBe(true);
      await phaseDispatchManager.completePhase(result.phaseId!);

      // Now D should be ready
      state = await phaseDispatchManager.getPhaseQueueState();
      expect(state.ready.length).toBe(1);
      expect(state.ready[0].phaseId).toBe(phaseD.id);
    });
  });
});
