/**
 * Test Harness for E2E Testing
 *
 * Wires up the full system with mocks for E2E testing.
 * Uses real managers (DispatchManager, CoordinationManager) with
 * MockAgentManager and MockWorktreeManager for isolation.
 */

import { randomUUID } from 'crypto';
import { vi } from 'vitest';
import type { DrizzleDatabase } from '../db/index.js';
import type { EventBus, DomainEvent } from '../events/types.js';
import { EventEmitterBus } from '../events/bus.js';
import type { AgentManager } from '../agent/types.js';
import { MockAgentManager, type MockAgentScenario } from '../agent/mock-manager.js';
import type { PendingQuestions, QuestionItem } from '../agent/types.js';
import type { WorktreeManager, Worktree, WorktreeDiff, MergeResult } from '../git/types.js';
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
import { DefaultDispatchManager } from '../dispatch/manager.js';
import { DefaultPhaseDispatchManager } from '../dispatch/phase-manager.js';
import type { CoordinationManager } from '../coordination/types.js';
import { DefaultCoordinationManager } from '../coordination/manager.js';
import type { TaskRepository } from '../db/repositories/task-repository.js';
import type { MessageRepository } from '../db/repositories/message-repository.js';
import type { AgentRepository } from '../db/repositories/agent-repository.js';
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { ErrandRepository } from '../db/repositories/errand-repository.js';
import type { Initiative, Phase, Task } from '../db/schema.js';
import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js';
import { createRepositories } from '../container.js';
import {
  seedFixture,
  type InitiativeFixture,
  type SeededFixture,
} from './fixtures.js';
import { appRouter, createCallerFactory } from '../trpc/router.js';
import { createContext, type TRPCContext } from '../trpc/context.js';

// =============================================================================
// MockWorktreeManager
// =============================================================================

/**
 * Simple in-memory WorktreeManager for testing.
 * Creates fake worktrees without actual git operations.
 */
export class MockWorktreeManager implements WorktreeManager {
  private worktrees: Map<string, Worktree> = new Map();
  private mergeResults: Map<string, MergeResult> = new Map();

  /**
   * Set a custom merge result for a specific worktree.
   * Used to test conflict scenarios.
   */
  setMergeResult(worktreeId: string, result: MergeResult): void {
    this.mergeResults.set(worktreeId, result);
  }

  async create(id: string, branch: string, baseBranch?: string): Promise<Worktree> {
    const worktree: Worktree = {
      id,
      branch,
      path: `/tmp/test-worktrees/${id}`,
      isMainWorktree: false,
    };
    this.worktrees.set(id, worktree);
    return worktree;
  }

  async remove(id: string): Promise<void> {
    if (!this.worktrees.has(id)) {
      throw new Error(`Worktree not found: ${id}`);
    }
    this.worktrees.delete(id);
    this.mergeResults.delete(id);
  }

  async list(): Promise<Worktree[]> {
    return Array.from(this.worktrees.values());
  }

  async get(id: string): Promise<Worktree | null> {
    return this.worktrees.get(id) ?? null;
  }

  async diff(id: string): Promise<WorktreeDiff> {
    if (!this.worktrees.has(id)) {
      throw new Error(`Worktree not found: ${id}`);
    }
    return {
      files: [],
      summary: 'No changes (mock)',
    };
  }

  async merge(id: string, targetBranch: string): Promise<MergeResult> {
    if (!this.worktrees.has(id)) {
      throw new Error(`Worktree not found: ${id}`);
    }

    // Return custom result if set, otherwise success
    const customResult = this.mergeResults.get(id);
    if (customResult) {
      return customResult;
    }

    return {
      success: true,
      message: `Merged ${id} into ${targetBranch} (mock)`,
    };
  }

  /**
   * Clear all worktrees.
   * Useful for test cleanup.
   */
  clear(): void {
    this.worktrees.clear();
    this.mergeResults.clear();
  }
}

// =============================================================================
// CapturingEventBus
// =============================================================================

/**
 * EventBus wrapper that captures all emitted events.
 * Extends EventEmitterBus with event capture functionality.
 */
export class CapturingEventBus extends EventEmitterBus {
  /** All emitted events */
  emittedEvents: DomainEvent[] = [];

  emit<T extends DomainEvent>(event: T): void {
    this.emittedEvents.push(event);
    super.emit(event);
  }

  /**
   * Get events by type.
   */
  getEventsByType(type: string): DomainEvent[] {
    return this.emittedEvents.filter((e) => e.type === type);
  }

  /**
   * Clear captured events.
   */
  clearEvents(): void {
    this.emittedEvents = [];
  }
}

// =============================================================================
// tRPC Caller Type
// =============================================================================

/**
 * Create caller factory for the app router.
 */
const createCaller = createCallerFactory(appRouter);

/**
 * Type for the tRPC caller.
 */
export type TRPCCaller = ReturnType<typeof createCaller>;

// =============================================================================
// TestHarness Interface
// =============================================================================

/**
 * Test harness for E2E testing.
 * Provides access to all system components and helper methods.
 */
export interface TestHarness {
  // Core components
  /** In-memory SQLite database */
  db: DrizzleDatabase;
  /** Event bus with event capture */
  eventBus: CapturingEventBus;
  /** Mock agent manager */
  agentManager: MockAgentManager;
  /** Alias for agentManager - used in tests for clarity */
  mockAgentManager: MockAgentManager;
  /** Mock worktree manager */
  worktreeManager: MockWorktreeManager;
  /** Real dispatch manager wired to mocks */
  dispatchManager: DispatchManager;
  /** Real phase dispatch manager wired to phaseRepository */
  phaseDispatchManager: PhaseDispatchManager;
  /** Real coordination manager wired to mocks */
  coordinationManager: CoordinationManager;

  // Repositories
  /** Task repository */
  taskRepository: TaskRepository;
  /** Message repository */
  messageRepository: MessageRepository;
  /** Agent repository */
  agentRepository: AgentRepository;
  /** Initiative repository */
  initiativeRepository: InitiativeRepository;
  /** Phase repository */
  phaseRepository: PhaseRepository;
  /** Errand repository */
  errandRepository: ErrandRepository;

  // tRPC Caller
  /** tRPC caller for direct procedure calls */
  caller: TRPCCaller;

  // Helpers
  /**
   * Seed a fixture into the database.
   */
  seedFixture(fixture: InitiativeFixture): Promise<SeededFixture>;

  /**
   * Set scenario for a specific agent name.
   */
  setAgentScenario(agentName: string, scenario: MockAgentScenario): void;

  /**
   * Convenience: Set agent to complete with done status.
   */
  setAgentDone(agentName: string, result?: string): void;

  /**
   * Convenience: Set agent to ask questions (array form).
   */
  setAgentQuestions(
    agentName: string,
    questions: QuestionItem[]
  ): void;

  /**
   * Convenience: Set agent to ask a single question.
   * Wraps the question in an array internally.
   */
  setAgentQuestion(
    agentName: string,
    questionId: string,
    question: string,
    options?: Array<{ label: string; description?: string }>
  ): void;

  /**
   * Convenience: Set agent to fail with unrecoverable error.
   */
  setAgentError(agentName: string, error: string): void;

  /**
   * Get pending questions for an agent.
   */
  getPendingQuestions(agentId: string): Promise<PendingQuestions | null>;

  /**
   * Resume an idle agent to answer a conversation (mock: always returns false).
   */
  resumeForConversation(agentId: string, conversationId: string, question: string, fromAgentId: string): Promise<boolean>;

  /**
   * Get events by type.
   */
  getEventsByType(type: string): DomainEvent[];

  /**
   * Get emitted events by type (alias for getEventsByType).
   */
  getEmittedEvents(type: string): DomainEvent[];

  /**
   * Clear all captured events.
   */
  clearEvents(): void;

  /**
   * Clean up all resources.
   */
  cleanup(): void;

  /**
   * Advance fake timers (wrapper for vi.runAllTimersAsync).
   * Only works when vi.useFakeTimers() is active.
   */
  advanceTimers(): Promise<void>;

  /**
   * Run a test body with fake timers enabled.
   * Activates fake timers before the callback and restores real timers after,
   * even if the callback throws.
   */
  withFakeTimers(fn: () => Promise<void>): Promise<void>;

  // ==========================================================================
  // Architect Mode Helpers
  // ==========================================================================

  /**
   * Set up scenario where architect completes discussion.
   */
  setArchitectDiscussComplete(
    agentName: string,
    _decisions: unknown[],
    summary: string
  ): void;

  /**
   * Set up scenario where architect needs more questions in discuss mode.
   */
  setArchitectDiscussQuestions(
    agentName: string,
    questions: QuestionItem[]
  ): void;

  /**
   * Set up scenario where architect completes plan.
   */
  setArchitectPlanComplete(
    agentName: string,
    _phases: unknown[]
  ): void;

  /**
   * Set up scenario where architect completes detail.
   */
  setArchitectDetailComplete(
    agentName: string,
    _tasks: unknown[]
  ): void;

  /**
   * Set up scenario where architect needs questions in detail mode.
   */
  setArchitectDetailQuestions(
    agentName: string,
    questions: QuestionItem[]
  ): void;

  // ==========================================================================
  // Initiative/Phase/Plan Convenience Helpers
  // ==========================================================================

  /**
   * Get initiative by ID through tRPC.
   */
  getInitiative(id: string): Promise<Initiative | null>;

  /**
   * Get phases for initiative through tRPC.
   */
  getPhases(initiativeId: string): Promise<Phase[]>;

  /**
   * Create initiative through tRPC.
   */
  createInitiative(name: string): Promise<Initiative>;

  /**
   * Create phases from plan output through tRPC.
   */
  createPhasesFromPlan(
    initiativeId: string,
    phases: Array<{ name: string }>
  ): Promise<Phase[]>;

  /**
   * Create a detail task through tRPC (replaces createPlan).
   */
  createDetailTask(
    phaseId: string,
    name: string,
    description?: string
  ): Promise<Task>;

  /**
   * Get child tasks of a parent task through tRPC.
   */
  getChildTasks(parentTaskId: string): Promise<Task[]>;
}

// =============================================================================
// createTestHarness Factory
// =============================================================================

/**
 * Create a fully wired test harness for E2E testing.
 *
 * Wires:
 * - In-memory SQLite database
 * - CapturingEventBus (captures all events)
 * - MockAgentManager (simulates agent behavior)
 * - MockWorktreeManager (fake worktrees)
 * - Real DefaultDispatchManager (with mock agent manager)
 * - Real DefaultCoordinationManager (with mock worktree manager)
 * - All repositories (Drizzle implementations)
 * - tRPC caller with full context
 */
export function createTestHarness(): TestHarness {
  // Create database
  const db = createTestDatabase();

  // Create event bus with capture
  const eventBus = new CapturingEventBus();

  // Create mock managers
  const agentManager = new MockAgentManager({ eventBus });
  const worktreeManager = new MockWorktreeManager();

  // Create repositories
  const repos = createRepositories(db);
  const { taskRepository, messageRepository, agentRepository, initiativeRepository, phaseRepository, errandRepository } = repos;

  // Create real managers wired to mocks
  const dispatchManager = new DefaultDispatchManager(
    taskRepository,
    messageRepository,
    agentManager,
    eventBus
  );

  const phaseDispatchManager = new DefaultPhaseDispatchManager(
    phaseRepository,
    taskRepository,
    dispatchManager,
    eventBus
  );

  const coordinationManager = new DefaultCoordinationManager(
    worktreeManager,
    taskRepository,
    agentRepository,
    messageRepository,
    eventBus
  );

  // Create tRPC context with all dependencies
  const ctx: TRPCContext = createContext({
    eventBus,
    serverStartedAt: new Date(),
    processCount: 0,
    agentManager,
    taskRepository,
    messageRepository,
    dispatchManager,
    phaseDispatchManager,
    coordinationManager,
    initiativeRepository,
    phaseRepository,
    errandRepository,
  });

  // Create tRPC caller
  const caller = createCaller(ctx);

  // Build harness
  const harness: TestHarness = {
    // Core components
    db,
    eventBus,
    agentManager,
    mockAgentManager: agentManager, // Alias for clarity in tests
    worktreeManager,
    dispatchManager,
    phaseDispatchManager,
    coordinationManager,

    // Repositories
    taskRepository,
    messageRepository,
    agentRepository,
    initiativeRepository,
    phaseRepository,
    errandRepository,

    // tRPC Caller
    caller,

    // Helpers
    seedFixture: (fixture: InitiativeFixture) => seedFixture(db, fixture),

    setAgentScenario: (agentName: string, scenario: MockAgentScenario) => {
      agentManager.setScenario(agentName, scenario);
    },

    setAgentDone: (agentName: string, result?: string) => {
      agentManager.setScenario(agentName, { status: 'done', result });
    },

    setAgentQuestions: (
      agentName: string,
      questions: QuestionItem[]
    ) => {
      agentManager.setScenario(agentName, { status: 'questions', questions });
    },

    setAgentQuestion: (
      agentName: string,
      questionId: string,
      question: string,
      options?: Array<{ label: string; description?: string }>
    ) => {
      agentManager.setScenario(agentName, {
        status: 'questions',
        questions: [{ id: questionId, question, options }],
      });
    },

    setAgentError: (agentName: string, error: string) => {
      agentManager.setScenario(agentName, { status: 'error', error });
    },

    getPendingQuestions: (agentId: string) => agentManager.getPendingQuestions(agentId),

    resumeForConversation: (agentId: string, conversationId: string, question: string, fromAgentId: string) =>
      agentManager.resumeForConversation(agentId, conversationId, question, fromAgentId),

    getEventsByType: (type: string) => eventBus.getEventsByType(type),

    getEmittedEvents: (type: string) => eventBus.getEventsByType(type),

    clearEvents: () => eventBus.clearEvents(),

    cleanup: () => {
      agentManager.clear();
      worktreeManager.clear();
      eventBus.clearEvents();
    },

    // Timer helper - requires vi.useFakeTimers() to be active
    advanceTimers: async () => { await vi.runAllTimersAsync(); },

    withFakeTimers: async (fn: () => Promise<void>) => {
      vi.useFakeTimers();
      try {
        await fn();
      } finally {
        vi.useRealTimers();
      }
    },

    // ========================================================================
    // Architect Mode Helpers
    // ========================================================================

    setArchitectDiscussComplete: (
      agentName: string,
      _decisions: unknown[],
      summary: string
    ) => {
      agentManager.setScenario(agentName, {
        status: 'done',
        result: summary,
        delay: 0,
      });
    },

    setArchitectDiscussQuestions: (
      agentName: string,
      questions: QuestionItem[]
    ) => {
      agentManager.setScenario(agentName, {
        status: 'questions',
        questions,
        delay: 0,
      });
    },

    setArchitectPlanComplete: (
      agentName: string,
      _phases: unknown[]
    ) => {
      agentManager.setScenario(agentName, {
        status: 'done',
        result: 'Plan complete',
        delay: 0,
      });
    },

    setArchitectDetailComplete: (
      agentName: string,
      _tasks: unknown[]
    ) => {
      agentManager.setScenario(agentName, {
        status: 'done',
        result: 'Detail complete',
        delay: 0,
      });
    },

    setArchitectDetailQuestions: (
      agentName: string,
      questions: QuestionItem[]
    ) => {
      agentManager.setScenario(agentName, {
        status: 'questions',
        questions,
        delay: 0,
      });
    },

    // ========================================================================
    // Initiative/Phase/Plan Convenience Helpers
    // ========================================================================

    getInitiative: async (id: string) => {
      try {
        return await caller.getInitiative({ id });
      } catch {
        return null;
      }
    },

    getPhases: (initiativeId: string) => {
      return caller.listPhases({ initiativeId });
    },

    createInitiative: (name: string) => {
      return caller.createInitiative({ name });
    },

    createPhasesFromPlan: (
      initiativeId: string,
      phases: Array<{ name: string }>
    ) => {
      return caller.createPhasesFromPlan({ initiativeId, phases });
    },

    createDetailTask: async (phaseId: string, name: string, description?: string) => {
      return caller.createPhaseTask({
        phaseId,
        name,
        description,
        category: 'detail',
        type: 'auto',
      });
    },

    getChildTasks: (parentTaskId: string) => {
      return caller.listTasks({ parentTaskId });
    },
  };

  return harness;
}
