import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';

import {
  generateRunId,
  selectStages,
  saveManifest,
  loadManifest,
  runPipeline,
  resumePipeline,
  getPipelineStatus,
} from '../src/pipeline-runner.js';
import type { ForgeTask, RunManifest, TaskExecutor } from '../src/types.js';
import type { TaskResult } from '@mosaicstack/macp';

/** Mock TaskExecutor that records submitted tasks and returns success. */
function createMockExecutor(options?: {
  failStage?: string;
}): TaskExecutor & { submittedTasks: ForgeTask[] } {
  const submittedTasks: ForgeTask[] = [];
  return {
    submittedTasks,
    async submitTask(task: ForgeTask) {
      submittedTasks.push(task);
    },
    async waitForCompletion(taskId: string): Promise<TaskResult> {
      const failStage = options?.failStage;
      const task = submittedTasks.find((t) => t.id === taskId);
      const stageName = task?.metadata?.['stageName'] as string | undefined;

      if (failStage && stageName === failStage) {
        return {
          task_id: taskId,
          status: 'failed',
          completed_at: new Date().toISOString(),
          exit_code: 1,
          gate_results: [],
        };
      }
      return {
        task_id: taskId,
        status: 'completed',
        completed_at: new Date().toISOString(),
        exit_code: 0,
        gate_results: [],
      };
    },
    async getTaskStatus() {
      return 'completed' as const;
    },
  };
}

describe('generateRunId', () => {
  it('returns a timestamp string', () => {
    const id = generateRunId();
    expect(id).toMatch(/^\d{8}-\d{6}$/);
  });

  it('returns unique IDs', () => {
    const ids = new Set(Array.from({ length: 10 }, generateRunId));
    // Given they run in the same second, they should at least be consistent format
    expect(ids.size).toBeGreaterThanOrEqual(1);
  });
});

describe('selectStages', () => {
  it('returns full sequence when no args', () => {
    const stages = selectStages();
    expect(stages.length).toBeGreaterThan(0);
    expect(stages[0]).toBe('00-intake');
  });

  it('returns provided stages', () => {
    const stages = selectStages(['00-intake', '05-coding']);
    expect(stages).toEqual(['00-intake', '05-coding']);
  });

  it('throws for unknown stages', () => {
    expect(() => selectStages(['unknown'])).toThrow('Unknown Forge stages');
  });

  it('skips to specified stage', () => {
    const stages = selectStages(undefined, '05-coding');
    expect(stages[0]).toBe('05-coding');
    expect(stages).not.toContain('00-intake');
  });

  it('throws if skipTo not in selected stages', () => {
    expect(() => selectStages(['00-intake'], '05-coding')).toThrow(
      "skip_to stage '05-coding' is not present",
    );
  });
});

describe('manifest operations', () => {
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-manifest-'));
  });

  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it('saveManifest and loadManifest roundtrip', () => {
    const manifest: RunManifest = {
      runId: 'test-123',
      brief: '/path/to/brief.md',
      codebase: '/project',
      briefClass: 'strategic',
      classSource: 'auto',
      forceBoard: false,
      createdAt: '2026-01-01T00:00:00Z',
      updatedAt: '2026-01-01T00:00:00Z',
      currentStage: '00-intake',
      status: 'in_progress',
      stages: {
        '00-intake': { status: 'passed', startedAt: '2026-01-01T00:00:00Z' },
      },
    };

    saveManifest(tmpDir, manifest);
    const loaded = loadManifest(tmpDir);
    expect(loaded.runId).toBe('test-123');
    expect(loaded.briefClass).toBe('strategic');
    expect(loaded.stages['00-intake']?.status).toBe('passed');
  });

  it('loadManifest throws for missing file', () => {
    expect(() => loadManifest('/nonexistent')).toThrow('manifest.json not found');
  });
});

describe('runPipeline', () => {
  let tmpDir: string;
  let briefPath: string;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-pipeline-'));
    briefPath = path.join(tmpDir, 'test-brief.md');
    fs.writeFileSync(
      briefPath,
      '---\nclass: hotfix\n---\n\n# Fix CSS bug\n\nFix the bugfix for lint cleanup.',
    );
  });

  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it('runs pipeline to completion with mock executor', async () => {
    const executor = createMockExecutor();
    const result = await runPipeline(briefPath, tmpDir, {
      executor,
      stages: ['00-intake', '00b-discovery'],
    });

    expect(result.runId).toMatch(/^\d{8}-\d{6}$/);
    expect(result.stages).toEqual(['00-intake', '00b-discovery']);
    expect(result.manifest.status).toBe('completed');
    expect(executor.submittedTasks).toHaveLength(2);
  });

  it('creates run directory under .forge/runs/', async () => {
    const executor = createMockExecutor();
    const result = await runPipeline(briefPath, tmpDir, {
      executor,
      stages: ['00-intake'],
    });

    expect(result.runDir).toContain(path.join('.forge', 'runs'));
    expect(fs.existsSync(result.runDir)).toBe(true);
  });

  it('writes manifest with stage statuses', async () => {
    const executor = createMockExecutor();
    const result = await runPipeline(briefPath, tmpDir, {
      executor,
      stages: ['00-intake', '00b-discovery'],
    });

    const manifest = loadManifest(result.runDir);
    expect(manifest.stages['00-intake']?.status).toBe('passed');
    expect(manifest.stages['00b-discovery']?.status).toBe('passed');
  });

  it('respects CLI class override', async () => {
    const executor = createMockExecutor();
    const result = await runPipeline(briefPath, tmpDir, {
      executor,
      briefClass: 'strategic',
      stages: ['00-intake'],
    });

    expect(result.manifest.briefClass).toBe('strategic');
    expect(result.manifest.classSource).toBe('cli');
  });

  it('uses frontmatter class', async () => {
    const executor = createMockExecutor();
    const result = await runPipeline(briefPath, tmpDir, {
      executor,
      stages: ['00-intake'],
    });

    expect(result.manifest.briefClass).toBe('hotfix');
    expect(result.manifest.classSource).toBe('frontmatter');
  });

  it('builds dependency chain between tasks', async () => {
    const executor = createMockExecutor();
    await runPipeline(briefPath, tmpDir, {
      executor,
      stages: ['00-intake', '00b-discovery', '02-planning-1'],
    });

    expect(executor.submittedTasks[0]!.dependsOn).toBeUndefined();
    expect(executor.submittedTasks[1]!.dependsOn).toEqual([executor.submittedTasks[0]!.id]);
    expect(executor.submittedTasks[2]!.dependsOn).toEqual([executor.submittedTasks[1]!.id]);
  });

  it('handles stage failure', async () => {
    const executor = createMockExecutor({ failStage: '00b-discovery' });

    await expect(
      runPipeline(briefPath, tmpDir, {
        executor,
        stages: ['00-intake', '00b-discovery'],
      }),
    ).rejects.toThrow('Stage 00b-discovery failed');
  });

  it('marks manifest as failed on stage failure', async () => {
    const executor = createMockExecutor({ failStage: '00-intake' });

    try {
      await runPipeline(briefPath, tmpDir, {
        executor,
        stages: ['00-intake'],
      });
    } catch {
      // expected
    }

    // Find the run dir (we don't have it from the failed result)
    const runsDir = path.join(tmpDir, '.forge', 'runs');
    const runDirs = fs.readdirSync(runsDir);
    expect(runDirs).toHaveLength(1);
    const manifest = loadManifest(path.join(runsDir, runDirs[0]!));
    expect(manifest.status).toBe('failed');
    expect(manifest.stages['00-intake']?.status).toBe('failed');
  });
});

describe('resumePipeline', () => {
  let tmpDir: string;
  let briefPath: string;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-resume-'));
    briefPath = path.join(tmpDir, 'brief.md');
    fs.writeFileSync(briefPath, '---\nclass: hotfix\n---\n\n# Fix bug');
  });

  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it('resumes from first incomplete stage', async () => {
    // First run fails on discovery
    const executor1 = createMockExecutor({ failStage: '00b-discovery' });
    let runDir: string;

    try {
      await runPipeline(briefPath, tmpDir, {
        executor: executor1,
        stages: ['00-intake', '00b-discovery', '02-planning-1'],
      });
    } catch {
      // expected
    }

    const runsDir = path.join(tmpDir, '.forge', 'runs');
    runDir = path.join(runsDir, fs.readdirSync(runsDir)[0]!);

    // Resume should pick up from 00b-discovery
    const executor2 = createMockExecutor();
    const result = await resumePipeline(runDir, executor2);

    expect(result.manifest.status).toBe('completed');
    // Should have re-run from 00b-discovery onward
    expect(result.stages[0]).toBe('00b-discovery');
  });
});

describe('getPipelineStatus', () => {
  let tmpDir: string;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-status-'));
  });

  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it('returns manifest', () => {
    const manifest: RunManifest = {
      runId: 'test',
      brief: '/brief.md',
      codebase: '',
      briefClass: 'strategic',
      classSource: 'auto',
      forceBoard: false,
      createdAt: '2026-01-01T00:00:00Z',
      updatedAt: '2026-01-01T00:00:00Z',
      currentStage: '00-intake',
      status: 'in_progress',
      stages: {},
    };
    saveManifest(tmpDir, manifest);

    const status = getPipelineStatus(tmpDir);
    expect(status.runId).toBe('test');
    expect(status.status).toBe('in_progress');
  });
});
