/**
 * Integration test — proves PhaseRunner state machine works against real gsd-tools.cjs.
 *
 * Creates a temp `.planning/` directory structure, instantiates real GSDTools,
 * and exercises the state machine. Sessions will fail (no Claude CLI in CI) but
 * the state machine's control flow, event emission, and error capture are proven.
 */

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

import { GSDTools, resolveGsdToolsPath } from './gsd-tools.js';
import { PhaseRunner } from './phase-runner.js';
import type { PhaseRunnerDeps } from './phase-runner.js';
import { ContextEngine } from './context-engine.js';
import { PromptFactory } from './phase-prompt.js';
import { GSDEventStream } from './event-stream.js';
import { loadConfig } from './config.js';
import type { GSDEvent } from './types.js';
import { GSDEventType, PhaseStepType } from './types.js';

// ─── Helpers ─────────────────────────────────────────────────────────────────

const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);

async function createTempPlanningDir(): Promise<string> {
  const tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-phase-int-'));

  // Create .planning structure
  const planningDir = join(tmpDir, '.planning');
  const phasesDir = join(planningDir, 'phases');
  const phaseDir = join(phasesDir, '01-integration-test');

  await mkdir(phaseDir, { recursive: true });

  // config.json
  await writeFile(
    join(planningDir, 'config.json'),
    JSON.stringify({
      model_profile: 'balanced',
      commit_docs: false,
      workflow: {
        research: true,
        verifier: true,
        auto_advance: true,
        skip_discuss: false,
      },
    }),
  );

  // ROADMAP.md — required for roadmap_exists
  await writeFile(join(planningDir, 'ROADMAP.md'), '# Roadmap\n\n## Phase 01: Integration Test\n');

  // CONTEXT.md in phase dir — triggers has_context=true → discuss is skipped
  await writeFile(
    join(phaseDir, 'CONTEXT.md'),
    '# Context\n\nThis is an integration test phase with pre-existing context.\n',
  );

  return tmpDir;
}

// ─── Test suite ──────────────────────────────────────────────────────────────

describe.skipIf(!gsdToolsAvailable)('Integration: PhaseRunner against real gsd-tools.cjs', () => {
  let tmpDir: string;
  let tools: GSDTools;

  beforeAll(async () => {
    tmpDir = await createTempPlanningDir();
    tools = new GSDTools({
      projectDir: tmpDir,
      gsdToolsPath: GSD_TOOLS_PATH,
      timeoutMs: 10_000,
    });
  });

  afterAll(async () => {
    if (tmpDir) {
      await rm(tmpDir, { recursive: true, force: true });
    }
  });

  // ── Test 1: initPhaseOp returns valid PhaseOpInfo ──

  it('initPhaseOp returns valid PhaseOpInfo for temp phase', async () => {
    const info = await tools.initPhaseOp('01');

    expect(info.phase_found).toBe(true);
    expect(info.phase_number).toBe('01');
    expect(info.phase_name).toBe('integration-test');
    expect(info.phase_dir).toBe('.planning/phases/01-integration-test');
    expect(info.has_context).toBe(true);
    expect(info.has_plans).toBe(false);
    expect(info.plan_count).toBe(0);
    expect(info.roadmap_exists).toBe(true);
    expect(info.planning_exists).toBe(true);
  });

  it('initPhaseOp returns phase_found=false for nonexistent phase', async () => {
    const info = await tools.initPhaseOp('99');

    expect(info.phase_found).toBe(false);
    expect(info.has_context).toBe(false);
    expect(info.plan_count).toBe(0);
  });

  // ── Test 2: PhaseRunner state machine control flow ──

  it('PhaseRunner emits lifecycle events and captures session errors gracefully', { timeout: 300_000 }, async () => {
    const eventStream = new GSDEventStream();
    const config = await loadConfig(tmpDir);
    const contextEngine = new ContextEngine(tmpDir);
    const promptFactory = new PromptFactory();

    const events: GSDEvent[] = [];
    eventStream.on('event', (e: GSDEvent) => events.push(e));

    const deps: PhaseRunnerDeps = {
      projectDir: tmpDir,
      tools,
      promptFactory,
      contextEngine,
      eventStream,
      config,
    };

    const runner = new PhaseRunner(deps);
    // Tight budget/turns so each session finishes fast
    const result = await runner.run('01', {
      maxTurnsPerStep: 2,
      maxBudgetPerStep: 0.10,
    });

    // ── (a) Phase start event emitted ──
    const phaseStartEvents = events.filter(e => e.type === GSDEventType.PhaseStart);
    expect(phaseStartEvents).toHaveLength(1);
    const phaseStart = phaseStartEvents[0]!;
    if (phaseStart.type === GSDEventType.PhaseStart) {
      expect(phaseStart.phaseNumber).toBe('01');
      expect(phaseStart.phaseName).toBe('integration-test');
    }

    // ── (b) Discuss should be skipped (has_context=true) ──
    // No discuss step in results since it was skipped
    const discussSteps = result.steps.filter(s => s.step === PhaseStepType.Discuss);
    expect(discussSteps).toHaveLength(0);

    // ── (c) Step start events emitted for attempted steps ──
    const stepStartEvents = events.filter(e => e.type === GSDEventType.PhaseStepStart);
    expect(stepStartEvents.length).toBeGreaterThanOrEqual(1);

    // ── (d) Step results are properly structured ──
    // With CLI available, sessions may succeed or fail depending on budget/turns.
    // Either way, each step result must have correct structure.
    expect(result.steps.length).toBeGreaterThanOrEqual(1);
    for (const step of result.steps) {
      expect(Object.values(PhaseStepType)).toContain(step.step);
      expect(typeof step.success).toBe('boolean');
      expect(typeof step.durationMs).toBe('number');
      // Failed steps may or may not have an error message
      // (e.g. advance step can fail without explicit error string)
    }

    // ── (e) Phase complete event emitted ──
    const phaseCompleteEvents = events.filter(e => e.type === GSDEventType.PhaseComplete);
    expect(phaseCompleteEvents).toHaveLength(1);

    // ── (f) Result structure is valid ──
    expect(result.phaseNumber).toBe('01');
    expect(result.phaseName).toBe('integration-test');
    expect(typeof result.totalCostUsd).toBe('number');
    expect(typeof result.totalDurationMs).toBe('number');
    expect(result.totalDurationMs).toBeGreaterThan(0);
  });

  // ── Test 3: PhaseRunner with nonexistent phase throws ──

  it('PhaseRunner throws PhaseRunnerError for nonexistent phase', async () => {
    const eventStream = new GSDEventStream();
    const config = await loadConfig(tmpDir);
    const contextEngine = new ContextEngine(tmpDir);
    const promptFactory = new PromptFactory();

    const deps: PhaseRunnerDeps = {
      projectDir: tmpDir,
      tools,
      promptFactory,
      contextEngine,
      eventStream,
      config,
    };

    const runner = new PhaseRunner(deps);
    await expect(runner.run('99')).rejects.toThrow('Phase 99 not found on disk');
  });

  // ── Test 4: GSD.runPhase() public API delegates correctly ──

  it('GSD.runPhase() creates collaborators and delegates to PhaseRunner', { timeout: 300_000 }, async () => {
    // Import GSD here to test the public API wiring
    const { GSD } = await import('./index.js');

    const gsd = new GSD({ projectDir: tmpDir });
    const events: GSDEvent[] = [];
    gsd.onEvent((e) => events.push(e));

    const result = await gsd.runPhase('01', {
      maxTurnsPerStep: 2,
      maxBudgetPerStep: 0.10,
    });

    // Proves the full wiring works: GSD → PhaseRunner → GSDTools → gsd-tools.cjs
    expect(result.phaseNumber).toBe('01');
    expect(result.phaseName).toBe('integration-test');
    expect(result.steps.length).toBeGreaterThanOrEqual(1);
    expect(events.some(e => e.type === GSDEventType.PhaseStart)).toBe(true);
    expect(events.some(e => e.type === GSDEventType.PhaseComplete)).toBe(true);
  });
});

// ─── Wave / phasePlanIndex Integration Tests ─────────────────────────────────

/**
 * Creates a temp `.planning/` directory with multi-wave plan files.
 * - Plans 01 and 02 are wave 1 (parallel)
 * - Plan 03 is wave 2 (depends on wave 1)
 * - Plan 01 has a SUMMARY.md (marks it as completed)
 */
async function createMultiWavePlanningDir(): Promise<string> {
  const tmpDir = await mkdtemp(join(tmpdir(), 'gsd-sdk-wave-int-'));

  const planningDir = join(tmpDir, '.planning');
  const phaseDir = join(planningDir, 'phases', '01-wave-test');
  await mkdir(phaseDir, { recursive: true });

  // config.json — with parallelization enabled
  await writeFile(
    join(planningDir, 'config.json'),
    JSON.stringify({
      model_profile: 'balanced',
      commit_docs: false,
      parallelization: true,
      workflow: {
        research: true,
        verifier: true,
        auto_advance: true,
        skip_discuss: false,
      },
    }),
  );

  // ROADMAP.md
  await writeFile(join(planningDir, 'ROADMAP.md'), '# Roadmap\n\n## Phase 01: Wave Test\n');

  const planTemplate = (id: string, wave: number, dependsOn: string[] = []) => `---
phase: "01"
plan: "${id}"
type: "feature"
wave: ${wave}
depends_on: [${dependsOn.map(d => `"${d}"`).join(', ')}]
files_modified: ["src/${id}.ts"]
autonomous: true
requirements: []
must_haves:
  truths: ["${id} exists"]
  artifacts: []
  key_links: []
---

# Plan: ${id}

<task type="code" name="Create ${id}" files="src/${id}.ts">
  <read_first>none</read_first>
  <action>Create ${id}</action>
  <verify>File exists</verify>
  <acceptance_criteria>
    - File exists
  </acceptance_criteria>
  <done>Done</done>
</task>
`;

  // Wave 1 plans (parallel)
  await writeFile(join(phaseDir, '01-wave-test-01-PLAN.md'), planTemplate('01-wave-test-01', 1));
  await writeFile(join(phaseDir, '01-wave-test-02-PLAN.md'), planTemplate('01-wave-test-02', 1));

  // Wave 2 plan (depends on wave 1)
  await writeFile(
    join(phaseDir, '01-wave-test-03-PLAN.md'),
    planTemplate('01-wave-test-03', 2, ['01-wave-test-01']),
  );

  // Summary for plan 01 — marks it as completed
  await writeFile(
    join(phaseDir, '01-wave-test-01-SUMMARY.md'),
    `---\nresult: pass\nplan: "01-wave-test-01"\ncost_usd: 0.01\nduration_ms: 1000\n---\n\n# Summary\n\nAll tasks completed.\n`,
  );

  return tmpDir;
}

describe.skipIf(!gsdToolsAvailable)('Integration: phasePlanIndex and wave execution', () => {
  let tmpDir: string;
  let tools: GSDTools;

  beforeAll(async () => {
    tmpDir = await createMultiWavePlanningDir();
    tools = new GSDTools({
      projectDir: tmpDir,
      gsdToolsPath: GSD_TOOLS_PATH,
      timeoutMs: 10_000,
    });
  });

  afterAll(async () => {
    if (tmpDir) {
      await rm(tmpDir, { recursive: true, force: true });
    }
  });

  it('phasePlanIndex returns typed PhasePlanIndex with correct wave grouping', async () => {
    const index = await tools.phasePlanIndex('01');

    // 3 plans total
    expect(index.plans).toHaveLength(3);

    // Wave grouping: wave 1 has 2 plans, wave 2 has 1
    expect(index.waves['1']).toHaveLength(2);
    expect(index.waves['1']).toContain('01-wave-test-01');
    expect(index.waves['1']).toContain('01-wave-test-02');
    expect(index.waves['2']).toHaveLength(1);
    expect(index.waves['2']).toContain('01-wave-test-03');

    // Incomplete: plan 01 has summary so only 02 and 03 are incomplete
    expect(index.incomplete).toHaveLength(2);
    expect(index.incomplete).toContain('01-wave-test-02');
    expect(index.incomplete).toContain('01-wave-test-03');

    // All autonomous → no checkpoints
    expect(index.has_checkpoints).toBe(false);

    // Phase ID correct
    expect(index.phase).toBe('01');
  });

  it('phasePlanIndex marks has_summary correctly per plan', async () => {
    const index = await tools.phasePlanIndex('01');

    // Plan 01 has a SUMMARY.md on disk
    const plan01 = index.plans.find(p => p.id === '01-wave-test-01');
    expect(plan01).toBeDefined();
    expect(plan01!.has_summary).toBe(true);

    // Plans 02 and 03 have no summary
    const plan02 = index.plans.find(p => p.id === '01-wave-test-02');
    expect(plan02).toBeDefined();
    expect(plan02!.has_summary).toBe(false);

    const plan03 = index.plans.find(p => p.id === '01-wave-test-03');
    expect(plan03).toBeDefined();
    expect(plan03!.has_summary).toBe(false);
  });

  it('phasePlanIndex for nonexistent phase returns empty plans', async () => {
    const index = await tools.phasePlanIndex('99');

    expect(index.plans).toHaveLength(0);
    expect(Object.keys(index.waves)).toHaveLength(0);
    expect(index.incomplete).toHaveLength(0);
    expect(index.has_checkpoints).toBe(false);
  });
});
