/**
 * Cassette Test Harness
 *
 * Wraps RealProviderHarness with the CassetteProcessManager so tests run
 * against recorded cassettes instead of real AI APIs.
 *
 * Usage:
 *
 *   let harness: RealProviderHarness;
 *
 *   beforeAll(async () => {
 *     harness = await createCassetteHarness({ provider: 'claude' });
 *   });
 *
 *   afterAll(() => harness.cleanup());
 *
 *   it('completes a task', async () => {
 *     const agent = await harness.agentManager.spawn({ prompt: MINIMAL_PROMPTS.done, ... });
 *     const result = await harness.waitForAgentCompletion(agent.id);
 *     expect(result?.success).toBe(true);
 *   });
 *
 * Mode control via env vars:
 *   (default)                 → replay mode: cassette must exist, throws if missing
 *   CW_CASSETTE_RECORD=1      → auto mode: replay if exists, record if missing
 *   CW_CASSETTE_FORCE_RECORD=1→ record mode: always run real agent, overwrite cassette
 */

import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
import { createTestDatabase } from '../../db/repositories/drizzle/test-helpers.js';
import {
  DrizzleAgentRepository,
  DrizzleProjectRepository,
  DrizzleAccountRepository,
  DrizzleInitiativeRepository,
} from '../../db/repositories/drizzle/index.js';
import { MultiProviderAgentManager } from '../../agent/manager.js';
import { CapturingEventBus, sleep, type RealProviderHarness } from '../integration/real-providers/harness.js';
import { CassetteStore } from './store.js';
import { CassetteProcessManager, type CassetteMode } from './process-manager.js';

export interface CassetteHarnessOptions {
  /** Which provider the agent runs as (default: 'claude'). */
  provider?: 'claude' | 'codex';
  /**
   * Directory where cassette JSON files are stored and read from.
   * Defaults to CW_CASSETTE_DIR env var, then src/test/cassettes/.
   */
  cassetteDir?: string;
  /**
   * Override cassette mode. Normally derived from env vars:
   * - CW_CASSETTE_FORCE_RECORD=1  → 'record'
   * - CW_CASSETTE_RECORD=1        → 'auto'
   * - (default)                   → 'replay'
   */
  mode?: CassetteMode;
}

const DEFAULT_CASSETTE_DIR = new URL('../cassettes', import.meta.url).pathname;

/**
 * Resolve cassette mode from env vars (highest priority) or options.
 */
function resolveCassetteMode(options: CassetteHarnessOptions): CassetteMode {
  if (process.env.CW_CASSETTE_FORCE_RECORD === '1') return 'record';
  if (process.env.CW_CASSETTE_RECORD === '1') return 'auto';
  return options.mode ?? 'replay';
}

/**
 * Create a test harness backed by the cassette system.
 *
 * The harness exposes the same interface as RealProviderHarness so tests
 * written for real providers work unchanged with cassettes.
 *
 * Replay is much faster than real API calls (typically < 500ms) and
 * exercises the full pipeline: ProcessManager → FileTailer → OutputHandler
 * → SignalManager → event emission.
 */
export async function createCassetteHarness(options: CassetteHarnessOptions = {}): Promise<RealProviderHarness> {
  const cassetteDir = options.cassetteDir ?? process.env.CW_CASSETTE_DIR ?? DEFAULT_CASSETTE_DIR;
  const cassetteMode = resolveCassetteMode(options);

  // Create a temporary git workspace (required for worktree operations).
  const workspaceRoot = await mkdtemp(join(tmpdir(), 'cw-cassette-'));
  execSync('git init', { cwd: workspaceRoot, stdio: 'ignore' });
  execSync('git config user.email "test@test.com"', { cwd: workspaceRoot, stdio: 'ignore' });
  execSync('git config user.name "Test"', { cwd: workspaceRoot, stdio: 'ignore' });
  execSync('touch .gitkeep && git add .gitkeep && git commit -m "init"', { cwd: workspaceRoot, stdio: 'ignore' });

  const db = createTestDatabase();
  const agentRepository = new DrizzleAgentRepository(db);
  const projectRepository = new DrizzleProjectRepository(db);
  const accountRepository = new DrizzleAccountRepository(db);
  const initiativeRepository = new DrizzleInitiativeRepository(db);
  const eventBus = new CapturingEventBus();

  const store = new CassetteStore(cassetteDir);
  const cassetteProcessManager = new CassetteProcessManager(
    workspaceRoot,
    projectRepository,
    store,
    cassetteMode,
  );

  const agentManager = new MultiProviderAgentManager(
    agentRepository,
    workspaceRoot,
    projectRepository,
    accountRepository,
    eventBus,
    undefined, // credentialManager
    undefined, // changeSetRepository
    undefined, // phaseRepository
    undefined, // taskRepository
    undefined, // pageRepository
    undefined, // logChunkRepository
    false,     // debug
    cassetteProcessManager,
  );

  const harness: RealProviderHarness = {
    db,
    eventBus,
    agentManager,
    workspaceRoot,
    agentRepository,
    projectRepository,
    accountRepository,
    initiativeRepository,

    // Cassette replays are fast — use a short poll interval and default timeout.
    async waitForAgentCompletion(agentId, timeoutMs = 30_000) {
      const deadline = Date.now() + timeoutMs;
      while (Date.now() < deadline) {
        const agent = await agentRepository.findById(agentId);
        if (!agent) return null;
        if (agent.status === 'idle' || agent.status === 'stopped') {
          return agentManager.getResult(agentId);
        }
        if (agent.status === 'crashed') {
          return agentManager.getResult(agentId);
        }
        if (agent.status === 'waiting_for_input') return null;
        await sleep(100);
      }
      throw new Error(`[cassette] Timeout waiting for agent ${agentId} to complete after ${timeoutMs}ms`);
    },

    async waitForAgentWaiting(agentId, timeoutMs = 30_000) {
      const deadline = Date.now() + timeoutMs;
      while (Date.now() < deadline) {
        const agent = await agentRepository.findById(agentId);
        if (!agent) return null;
        if (agent.status === 'waiting_for_input') return agentManager.getPendingQuestions(agentId);
        if (['idle', 'stopped', 'crashed'].includes(agent.status)) return null;
        await sleep(100);
      }
      throw new Error(`[cassette] Timeout waiting for agent ${agentId} to enter waiting state after ${timeoutMs}ms`);
    },

    async waitForAgentStatus(agentId, status, timeoutMs = 30_000) {
      const deadline = Date.now() + timeoutMs;
      while (Date.now() < deadline) {
        const agent = await agentRepository.findById(agentId);
        if (!agent) throw new Error(`Agent ${agentId} not found`);
        if (agent.status === status) return;
        await sleep(100);
      }
      throw new Error(`[cassette] Timeout waiting for agent ${agentId} to reach status '${status}' after ${timeoutMs}ms`);
    },

    getEventsByType(type) {
      return eventBus.getEventsByType(type);
    },

    clearEvents() {
      eventBus.clearEvents();
    },

    async killAllAgents() {
      const agents = await agentRepository.findAll();
      for (const agent of agents) {
        if (agent.status === 'running') {
          try { await agentManager.stop(agent.id); } catch { /* ignore */ }
        }
      }
    },

    async cleanup() {
      await harness.killAllAgents();
      try { await rm(workspaceRoot, { recursive: true, force: true }); } catch { /* ignore */ }
    },
  };

  return harness;
}
