/**
 * DefaultCoordinationManager Tests
 *
 * Tests for the CoordinationManager adapter with dependency-ordered
 * merging and conflict handling.
 */

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { DefaultCoordinationManager } from './manager.js';
import { DrizzleTaskRepository } from '../db/repositories/drizzle/task.js';
import { DrizzleAgentRepository } from '../db/repositories/drizzle/agent.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 { WorktreeManager, MergeResult as GitMergeResult } from '../git/types.js';
import type { TaskRepository } from '../db/repositories/task-repository.js';
import type { AgentRepository } from '../db/repositories/agent-repository.js';
import type { MessageRepository } from '../db/repositories/message-repository.js';
import type { ConflictResolutionService } from './conflict-resolution-service.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 WorktreeManager with configurable merge results.
 */
function createMockWorktreeManager(
  mergeResults: Map<string, GitMergeResult> = new Map()
): WorktreeManager {
  return {
    create: vi.fn(),
    remove: vi.fn(),
    list: vi.fn().mockResolvedValue([]),
    get: vi.fn(),
    diff: vi.fn(),
    merge: vi.fn().mockImplementation(async (worktreeId: string) => {
      const result = mergeResults.get(worktreeId);
      if (result) return result;
      // Default: successful merge
      return { success: true, message: 'Merged successfully' };
    }),
  };
}

/**
 * Create a mock ConflictResolutionService.
 */
function createMockConflictResolutionService(): ConflictResolutionService {
  return {
    handleConflict: vi.fn(),
  };
}

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

describe('DefaultCoordinationManager', () => {
  let db: DrizzleDatabase;
  let taskRepository: TaskRepository;
  let agentRepository: AgentRepository;
  let messageRepository: MessageRepository;
  let eventBus: EventBus & { emittedEvents: DomainEvent[] };
  let worktreeManager: WorktreeManager;
  let conflictResolutionService: ConflictResolutionService;
  let manager: DefaultCoordinationManager;
  let testPhaseId: string;

  beforeEach(async () => {
    // Set up test database
    db = createTestDatabase();
    taskRepository = new DrizzleTaskRepository(db);
    agentRepository = new DrizzleAgentRepository(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();
    worktreeManager = createMockWorktreeManager();

    // Create coordination manager
    manager = new DefaultCoordinationManager(
      worktreeManager,
      taskRepository,
      agentRepository,
      messageRepository,
      eventBus
    );
  });

  // ===========================================================================
  // queueMerge() Tests
  // ===========================================================================

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

      // Create agent for task
      const agent = await agentRepository.create({
        name: 'agent-test',
        taskId: task.id,
        worktreeId: 'worktree-123',
      });

      await manager.queueMerge(task.id);

      const state = await manager.getQueueState();
      expect(state.queued.length).toBe(1);
      expect(state.queued[0].taskId).toBe(task.id);
      expect(state.queued[0].agentId).toBe(agent.id);
      expect(state.queued[0].worktreeId).toBe('worktree-123');
      expect(state.queued[0].priority).toBe('high');

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

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

    it('should throw error when no agent assigned to task', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Orphan Task',
        order: 1,
      });

      await expect(manager.queueMerge(task.id)).rejects.toThrow(
        'No agent found for task'
      );
    });
  });

  // ===========================================================================
  // getNextMergeable() Tests
  // ===========================================================================

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

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

      await agentRepository.create({
        name: 'agent-merge',
        taskId: task.id,
        worktreeId: 'worktree-merge',
      });

      await manager.queueMerge(task.id);

      const next = await manager.getNextMergeable();
      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,
      });

      // Create agents for all tasks
      await agentRepository.create({
        name: 'agent-low',
        taskId: lowTask.id,
        worktreeId: 'wt-low',
      });
      await agentRepository.create({
        name: 'agent-high',
        taskId: highTask.id,
        worktreeId: 'wt-high',
      });
      await agentRepository.create({
        name: 'agent-medium',
        taskId: mediumTask.id,
        worktreeId: 'wt-medium',
      });

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

      // Should get high priority first
      const next = await manager.getNextMergeable();
      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,
      });

      await agentRepository.create({
        name: 'agent-1',
        taskId: task1.id,
        worktreeId: 'wt-1',
      });
      await agentRepository.create({
        name: 'agent-2',
        taskId: task2.id,
        worktreeId: 'wt-2',
      });

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

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

  // ===========================================================================
  // processMerges() Tests - Success Path
  // ===========================================================================

  describe('processMerges - success path', () => {
    it('should complete clean merges and emit MergeCompletedEvent', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Mergeable Task',
        priority: 'high',
        order: 1,
      });

      const agent = await agentRepository.create({
        name: 'agent-merge',
        taskId: task.id,
        worktreeId: 'worktree-success',
      });

      await manager.queueMerge(task.id);

      const results = await manager.processMerges('main');

      expect(results.length).toBe(1);
      expect(results[0].taskId).toBe(task.id);
      expect(results[0].success).toBe(true);

      // Check state updated
      const state = await manager.getQueueState();
      expect(state.queued.length).toBe(0);
      expect(state.merged).toContain(task.id);

      // Check events emitted: queued, started, completed
      const eventTypes = eventBus.emittedEvents.map((e) => e.type);
      expect(eventTypes).toContain('merge:queued');
      expect(eventTypes).toContain('merge:started');
      expect(eventTypes).toContain('merge:completed');
    });

    it('should process multiple tasks in priority order', async () => {
      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,
      });

      await agentRepository.create({
        name: 'agent-low',
        taskId: lowTask.id,
        worktreeId: 'wt-low',
      });
      await agentRepository.create({
        name: 'agent-high',
        taskId: highTask.id,
        worktreeId: 'wt-high',
      });

      await manager.queueMerge(lowTask.id);
      await manager.queueMerge(highTask.id);

      const results = await manager.processMerges('main');

      // Should process high priority first
      expect(results.length).toBe(2);
      expect(results[0].taskId).toBe(highTask.id);
      expect(results[1].taskId).toBe(lowTask.id);
    });
  });

  // ===========================================================================
  // processMerges() Tests - Conflict Handling
  // ===========================================================================

  describe('processMerges - conflict handling', () => {
    it('should detect conflicts and emit MergeConflictedEvent', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Conflicting Task',
        priority: 'high',
        order: 1,
      });

      await agentRepository.create({
        name: 'agent-conflict',
        taskId: task.id,
        worktreeId: 'wt-conflict',
      });

      // Configure merge to fail with conflicts
      const mergeResults = new Map<string, GitMergeResult>();
      mergeResults.set('wt-conflict', {
        success: false,
        conflicts: ['file1.ts', 'file2.ts'],
        message: 'Merge conflicts detected',
      });

      worktreeManager = createMockWorktreeManager(mergeResults);
      manager = new DefaultCoordinationManager(
        worktreeManager,
        taskRepository,
        agentRepository,
        messageRepository,
        eventBus
      );

      await manager.queueMerge(task.id);
      const results = await manager.processMerges('main');

      expect(results.length).toBe(1);
      expect(results[0].taskId).toBe(task.id);
      expect(results[0].success).toBe(false);
      expect(results[0].conflicts).toEqual(['file1.ts', 'file2.ts']);

      // Check state updated
      const state = await manager.getQueueState();
      expect(state.conflicted.length).toBe(1);
      expect(state.conflicted[0].taskId).toBe(task.id);
      expect(state.conflicted[0].conflicts).toEqual(['file1.ts', 'file2.ts']);

      // Check MergeConflictedEvent emitted
      const conflictEvent = eventBus.emittedEvents.find(
        (e) => e.type === 'merge:conflicted'
      );
      expect(conflictEvent).toBeDefined();
      expect((conflictEvent as any).payload.conflictingFiles).toEqual([
        'file1.ts',
        'file2.ts',
      ]);
    });

    it('should create resolution task on conflict', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Original Task',
        priority: 'medium',
        order: 1,
      });

      await agentRepository.create({
        name: 'agent-conflict',
        taskId: task.id,
        worktreeId: 'wt-conflict',
      });

      // Configure merge to fail
      const mergeResults = new Map<string, GitMergeResult>();
      mergeResults.set('wt-conflict', {
        success: false,
        conflicts: ['src/index.ts'],
        message: 'Merge conflicts detected',
      });

      worktreeManager = createMockWorktreeManager(mergeResults);
      manager = new DefaultCoordinationManager(
        worktreeManager,
        taskRepository,
        agentRepository,
        messageRepository,
        eventBus
      );

      await manager.queueMerge(task.id);
      await manager.processMerges('main');

      // Check new task was created
      const tasks = await taskRepository.findByPhaseId(testPhaseId);
      const conflictTask = tasks.find((t) =>
        t.name.startsWith('Resolve conflicts:')
      );

      expect(conflictTask).toBeDefined();
      expect(conflictTask!.name).toBe('Resolve conflicts: Original Task');
      expect(conflictTask!.priority).toBe('high');
      expect(conflictTask!.description).toContain('src/index.ts');

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

      // Check TaskQueuedEvent emitted for conflict task
      const queuedEvent = eventBus.emittedEvents.find(
        (e) =>
          e.type === 'task:queued' &&
          (e as any).payload.taskId === conflictTask!.id
      );
      expect(queuedEvent).toBeDefined();
    });

    it('should create message to agent on conflict', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Task with Message',
        priority: 'medium',
        order: 1,
      });

      const agent = await agentRepository.create({
        name: 'agent-msg',
        taskId: task.id,
        worktreeId: 'wt-msg',
      });

      // Configure merge to fail
      const mergeResults = new Map<string, GitMergeResult>();
      mergeResults.set('wt-msg', {
        success: false,
        conflicts: ['conflict.ts'],
        message: 'Merge conflicts detected',
      });

      worktreeManager = createMockWorktreeManager(mergeResults);
      manager = new DefaultCoordinationManager(
        worktreeManager,
        taskRepository,
        agentRepository,
        messageRepository,
        eventBus
      );

      await manager.queueMerge(task.id);
      await manager.processMerges('main');

      // Check message was created
      const messages = await messageRepository.findByRecipient('agent', agent.id);
      expect(messages.length).toBe(1);
      expect(messages[0].recipientType).toBe('agent');
      expect(messages[0].recipientId).toBe(agent.id);
      expect(messages[0].senderType).toBe('user');
      expect(messages[0].content).toContain('Merge conflict detected');
      expect(messages[0].content).toContain('conflict.ts');
    });
  });

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

  describe('getQueueState', () => {
    it('should return correct counts for all states', async () => {
      // Create tasks
      const task1 = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Queued Task',
        priority: 'high',
        order: 1,
      });
      const task2 = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Conflict Task',
        priority: 'medium',
        order: 2,
      });

      await agentRepository.create({
        name: 'agent-1',
        taskId: task1.id,
        worktreeId: 'wt-1',
      });
      await agentRepository.create({
        name: 'agent-2',
        taskId: task2.id,
        worktreeId: 'wt-2',
      });

      // Configure task2 to conflict
      const mergeResults = new Map<string, GitMergeResult>();
      mergeResults.set('wt-2', {
        success: false,
        conflicts: ['test.ts'],
        message: 'Conflict',
      });

      worktreeManager = createMockWorktreeManager(mergeResults);
      manager = new DefaultCoordinationManager(
        worktreeManager,
        taskRepository,
        agentRepository,
        messageRepository,
        eventBus
      );

      // Queue both, process task2 (will conflict), leave task1 queued
      await manager.queueMerge(task2.id);
      await manager.processMerges('main');
      await manager.queueMerge(task1.id);

      const state = await manager.getQueueState();

      // task1 should be queued, task2 should be conflicted
      expect(state.queued.length).toBe(1);
      expect(state.queued[0].taskId).toBe(task1.id);
      expect(state.inProgress.length).toBe(0);
      expect(state.merged.length).toBe(0);
      expect(state.conflicted.length).toBe(1);
      expect(state.conflicted[0].taskId).toBe(task2.id);
    });
  });

  // ===========================================================================
  // handleConflict() Tests
  // ===========================================================================

  describe('handleConflict', () => {
    it('should throw error when task not found', async () => {
      await expect(
        manager.handleConflict('non-existent', ['file.ts'])
      ).rejects.toThrow('Original task not found');
    });

    it('should throw error when no agent for task', async () => {
      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Orphan Task',
        order: 1,
      });

      await expect(manager.handleConflict(task.id, ['file.ts'])).rejects.toThrow(
        'No agent found for task'
      );
    });
  });

  // ===========================================================================
  // Error Handling Tests
  // ===========================================================================

  describe('error handling', () => {
    it('should throw when TaskRepository not configured', async () => {
      const managerNoRepo = new DefaultCoordinationManager(
        worktreeManager,
        undefined, // No task repo
        agentRepository,
        messageRepository,
        eventBus
      );

      await expect(managerNoRepo.queueMerge('task-id')).rejects.toThrow(
        'TaskRepository not configured'
      );
    });

    it('should throw when AgentRepository not configured', async () => {
      const managerNoAgentRepo = new DefaultCoordinationManager(
        worktreeManager,
        taskRepository,
        undefined, // No agent repo
        messageRepository,
        eventBus
      );

      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Test',
        order: 1,
      });

      await expect(managerNoAgentRepo.queueMerge(task.id)).rejects.toThrow(
        'AgentRepository not configured'
      );
    });

    it('should throw when WorktreeManager not configured', async () => {
      const managerNoWorktree = new DefaultCoordinationManager(
        undefined, // No worktree manager
        taskRepository,
        agentRepository,
        messageRepository,
        eventBus
      );

      const task = await taskRepository.create({
        phaseId: testPhaseId,
        name: 'Test',
        order: 1,
      });

      await agentRepository.create({
        name: 'agent-test',
        taskId: task.id,
        worktreeId: 'wt-test',
      });

      await managerNoWorktree.queueMerge(task.id);
      await expect(managerNoWorktree.processMerges('main')).rejects.toThrow(
        'WorktreeManager not configured'
      );
    });
  });
});
