/**
 * DefaultDispatchManager Tests
 *
 * Tests for the DispatchManager adapter with dependency checking
 * and queue management.
 */

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { DefaultDispatchManager } from './manager.js';
import { DrizzleTaskRepository } from '../db/repositories/drizzle/task.js';
import { DrizzleMessageRepository } from '../db/repositories/drizzle/message.js';

import { DrizzlePhaseRepository } from '../db/repositories/drizzle/phase.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 { AgentManager, AgentInfo } from '../agent/types.js';
import type { TaskRepository } from '../db/repositories/task-repository.js';
import type { MessageRepository } from '../db/repositories/message-repository.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(),
  };
}

/**
 * Create a mock AgentManager.
 */
function createMockAgentManager(
  agents: AgentInfo[] = []
): AgentManager {
  const mockAgents = [...agents];

  return {
    list: vi.fn().mockResolvedValue(mockAgents),
    get: vi.fn().mockImplementation(async (id: string) => {
      return mockAgents.find((a) => a.id === id) || null;
    }),
    getByName: vi.fn().mockImplementation(async (name: string) => {
      return mockAgents.find((a) => a.name === name) || null;
    }),
    spawn: vi.fn().mockImplementation(async (options) => {
      const newAgent: AgentInfo = {
        id: `agent-${Date.now()}`,
        name: options.name ?? `mock-agent-${Date.now()}`,
        taskId: options.taskId,
        initiativeId: options.initiativeId ?? null,
        sessionId: null,
        worktreeId: 'worktree-test',
        status: 'running',
        mode: options.mode ?? 'execute',
        provider: options.provider ?? 'claude',
        accountId: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };
      mockAgents.push(newAgent);
      return newAgent;
    }),
    stop: vi.fn().mockResolvedValue(undefined),
    delete: vi.fn().mockResolvedValue(undefined),
    dismiss: vi.fn().mockResolvedValue(undefined),
    resume: vi.fn().mockResolvedValue(undefined),
    getResult: vi.fn().mockResolvedValue(null),
    getPendingQuestions: vi.fn().mockResolvedValue(null),
    resumeForConversation: vi.fn().mockResolvedValue(false),
    sendUserMessage: vi.fn().mockResolvedValue(undefined),
  };
}

/**
 * Create an idle agent for testing.
 */
function createIdleAgent(id: string, name: string): AgentInfo {
  return {
    id,
    name,
    taskId: 'task-123',
    initiativeId: null,
    sessionId: 'session-abc',
    worktreeId: 'worktree-xyz',
    status: 'idle',
    mode: 'execute',
    provider: 'claude',
    accountId: null,
    createdAt: new Date(),
    updatedAt: new Date(),
  };
}

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

describe('DefaultDispatchManager', () => {
  let db: DrizzleDatabase;
  let taskRepository: TaskRepository;
  let messageRepository: MessageRepository;
  let eventBus: EventBus & { emittedEvents: DomainEvent[] };
  let agentManager: AgentManager;
  let dispatchManager: DefaultDispatchManager;
  let testPhaseId: string;

  beforeEach(async () => {
    // Set up test database
    db = createTestDatabase();
    taskRepository = new DrizzleTaskRepository(db);
    messageRepository = new DrizzleMessageRepository(db);

    // Create required hierarchy for tasks
    const initiativeRepo = new DrizzleInitiativeRepository(db);
    const phaseRepo = new DrizzlePhaseRepository(db);
    

    const initiative = await initiativeRepo.create({
      name: 'Test Initiative',
    });
    const phase = await phaseRepo.create({
      initiativeId: initiative.id,
      name: 'Test Phase',
    });
    testPhaseId = phase.id;

    // Create mocks
    eventBus = createMockEventBus();
    agentManager = createMockAgentManager();

    // Create dispatch manager
    dispatchManager = new DefaultDispatchManager(
      taskRepository,
      messageRepository,
      agentManager,
      eventBus
    );
  });

  // ===========================================================================
  // queue() Tests
  // ===========================================================================

  describe('queue', () => {
    it('should add task to queue and emit TaskQueuedEvent', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Test Task',
        priority: 'high',
        order: 1,
      });

      await dispatchManager.queue(task.id);

      const state = await dispatchManager.getQueueState();
      expect(state.queued.length).toBe(1);
      expect(state.queued[0].taskId).toBe(task.id);
      expect(state.queued[0].priority).toBe('high');

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

    it('should throw error when task not found', async () => {
      await expect(dispatchManager.queue('non-existent-id')).rejects.toThrow(
        'Task not found'
      );
    });
  });

  // ===========================================================================
  // getNextDispatchable() Tests
  // ===========================================================================

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

    it('should return task when dependencies are complete', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Dispatchable Task',
        priority: 'medium',
        order: 1,
      });

      await dispatchManager.queue(task.id);

      const next = await dispatchManager.getNextDispatchable();
      expect(next).not.toBeNull();
      expect(next!.taskId).toBe(task.id);
    });

    it('should respect priority ordering (high > medium > low)', async () => {
      // Create tasks in different priority order
      const lowTask = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Low Priority',
        priority: 'low',
        order: 1,
      });
      const highTask = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'High Priority',
        priority: 'high',
        order: 2,
      });
      const mediumTask = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Medium Priority',
        priority: 'medium',
        order: 3,
      });

      // Queue in wrong order (low, high, medium)
      await dispatchManager.queue(lowTask.id);
      await dispatchManager.queue(highTask.id);
      await dispatchManager.queue(mediumTask.id);

      // Should get high priority first
      const next = await dispatchManager.getNextDispatchable();
      expect(next).not.toBeNull();
      expect(next!.taskId).toBe(highTask.id);
      expect(next!.priority).toBe('high');
    });

    it('should order by queuedAt within same priority (oldest first)', async () => {
      const task1 = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'First Task',
        priority: 'medium',
        order: 1,
      });
      const task2 = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Second Task',
        priority: 'medium',
        order: 2,
      });

      // Queue first task, wait, then queue second
      await dispatchManager.queue(task1.id);
      await new Promise((resolve) => setTimeout(resolve, 10));
      await dispatchManager.queue(task2.id);

      // Should get the first queued task
      const next = await dispatchManager.getNextDispatchable();
      expect(next).not.toBeNull();
      expect(next!.taskId).toBe(task1.id);
    });
  });

  // ===========================================================================
  // completeTask() Tests
  // ===========================================================================

  describe('completeTask', () => {
    it('should update task status and emit TaskCompletedEvent', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task to Complete',
        priority: 'medium',
        order: 1,
      });

      await dispatchManager.queue(task.id);
      await dispatchManager.completeTask(task.id);

      // Check task status updated
      const updatedTask = await taskRepository.findById(task.id);
      expect(updatedTask!.status).toBe('completed');

      // Check removed from queue
      const state = await dispatchManager.getQueueState();
      expect(state.queued.length).toBe(0);

      // Check event was emitted (2 events: queued + completed)
      expect(eventBus.emittedEvents.length).toBe(2);
      expect(eventBus.emittedEvents[1].type).toBe('task:completed');
    });
  });

  // ===========================================================================
  // blockTask() Tests
  // ===========================================================================

  describe('blockTask', () => {
    it('should update task status and emit TaskBlockedEvent', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task to Block',
        priority: 'medium',
        order: 1,
      });

      await dispatchManager.queue(task.id);
      await dispatchManager.blockTask(task.id, 'Waiting for user input');

      // Check task status updated
      const updatedTask = await taskRepository.findById(task.id);
      expect(updatedTask!.status).toBe('blocked');

      // Check moved to blocked list
      const state = await dispatchManager.getQueueState();
      expect(state.queued.length).toBe(0);
      expect(state.blocked.length).toBe(1);
      expect(state.blocked[0].taskId).toBe(task.id);
      expect(state.blocked[0].reason).toBe('Waiting for user input');

      // Check event was emitted (2 events: queued + blocked)
      expect(eventBus.emittedEvents.length).toBe(2);
      expect(eventBus.emittedEvents[1].type).toBe('task:blocked');
      expect((eventBus.emittedEvents[1] as any).payload.reason).toBe(
        'Waiting for user input'
      );
    });
  });

  // ===========================================================================
  // dispatchNext() Tests
  // ===========================================================================

  describe('dispatchNext', () => {
    it('should return failure when no tasks ready', async () => {
      const result = await dispatchManager.dispatchNext();

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

    it('should return failure when no agents available', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task needing agent',
        priority: 'high',
        order: 1,
      });

      await dispatchManager.queue(task.id);

      // Agent manager returns empty list (no idle agents)
      const result = await dispatchManager.dispatchNext();

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

    it('should dispatch task to available agent', async () => {
      // Create task
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task for dispatch',
        description: 'Do the thing',
        priority: 'high',
        order: 1,
      });

      await dispatchManager.queue(task.id);

      // Set up agent manager with an idle agent
      const idleAgent = createIdleAgent('agent-1', 'gastown');
      agentManager = createMockAgentManager([idleAgent]);
      dispatchManager = new DefaultDispatchManager(
        taskRepository,
        messageRepository,
        agentManager,
        eventBus
      );

      // Re-queue since we created a new dispatch manager
      await dispatchManager.queue(task.id);

      const result = await dispatchManager.dispatchNext();

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

      // Check task status updated to in_progress
      const updatedTask = await taskRepository.findById(task.id);
      expect(updatedTask!.status).toBe('in_progress');

      // Check spawn was called with correct params (prompt is wrapped by buildExecutePrompt)
      expect(agentManager.spawn).toHaveBeenCalledWith(
        expect.objectContaining({
          taskId: task.id,
          prompt: expect.stringContaining('Do the thing'),
        })
      );
    });

    it('should emit TaskDispatchedEvent on successful dispatch', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Dispatch event test',
        priority: 'medium',
        order: 1,
      });

      const idleAgent = createIdleAgent('agent-1', 'yaletown');
      agentManager = createMockAgentManager([idleAgent]);
      dispatchManager = new DefaultDispatchManager(
        taskRepository,
        messageRepository,
        agentManager,
        eventBus
      );

      await dispatchManager.queue(task.id);
      await dispatchManager.dispatchNext();

      // Find TaskDispatchedEvent
      const dispatchedEvent = eventBus.emittedEvents.find(
        (e) => e.type === 'task:dispatched'
      );
      expect(dispatchedEvent).toBeDefined();
      expect((dispatchedEvent as any).payload.taskId).toBe(task.id);
      expect((dispatchedEvent as any).payload.agentId).toBeDefined();
    });
  });

  // ===========================================================================
  // getQueueState() Tests
  // ===========================================================================

  describe('getQueueState', () => {
    it('should return correct state', async () => {
      // Create and queue tasks
      const task1 = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Ready Task',
        priority: 'high',
        order: 1,
      });
      const task2 = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Another Ready Task',
        priority: 'low',
        order: 2,
      });
      const task3 = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Blocked Task',
        priority: 'medium',
        order: 3,
      });

      await dispatchManager.queue(task1.id);
      await dispatchManager.queue(task2.id);
      await dispatchManager.queue(task3.id);
      await dispatchManager.blockTask(task3.id, 'Manual block');

      const state = await dispatchManager.getQueueState();

      // Queued should have 2 (task3 was moved to blocked)
      expect(state.queued.length).toBe(2);
      expect(state.queued.map((t) => t.taskId)).toContain(task1.id);
      expect(state.queued.map((t) => t.taskId)).toContain(task2.id);

      // Ready should have same 2 tasks (no dependencies)
      expect(state.ready.length).toBe(2);

      // Blocked should have 1
      expect(state.blocked.length).toBe(1);
      expect(state.blocked[0].taskId).toBe(task3.id);
      expect(state.blocked[0].reason).toBe('Manual block');
    });
  });

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

  describe('dependency scenario', () => {
    it('should handle task dependencies correctly', async () => {
      // This test verifies the basic flow described in the plan:
      // - Task A (no deps) - should be dispatchable
      // - Task B (depends on A) - not dispatchable until A completes
      // - Task C (depends on A) - not dispatchable until A completes

      // For v1, dependencies are empty (we'd need to enhance queue() to fetch from task_dependencies table)
      // This test verifies the priority and queue ordering work correctly

      const taskA = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task A - Foundation',
        priority: 'high',
        order: 1,
      });
      const taskB = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task B - Build on A',
        priority: 'medium',
        order: 2,
      });
      const taskC = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task C - Also build on A',
        priority: 'medium',
        order: 3,
      });

      // Queue all three
      await dispatchManager.queue(taskA.id);
      await dispatchManager.queue(taskB.id);
      await dispatchManager.queue(taskC.id);

      const state = await dispatchManager.getQueueState();
      expect(state.queued.length).toBe(3);

      // With no dependencies set, all should be ready
      // (In a full implementation, B and C would have dependsOn: [taskA.id])
      expect(state.ready.length).toBe(3);

      // A should come first (highest priority)
      const next = await dispatchManager.getNextDispatchable();
      expect(next!.taskId).toBe(taskA.id);

      // Complete A
      await dispatchManager.completeTask(taskA.id);

      // Now B and C should be next (both medium priority, B queued first)
      const stateAfterA = await dispatchManager.getQueueState();
      expect(stateAfterA.queued.length).toBe(2);

      // Get next - should be B (queued before C)
      const nextAfterA = await dispatchManager.getNextDispatchable();
      expect(nextAfterA!.taskId).toBe(taskB.id);
    });
  });
});
