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

import { registerForgeCommand } from './cli.js';
import { loadManifest } from './pipeline-runner.js';

describe('registerForgeCommand', () => {
  it('registers a "forge" command on the parent program', () => {
    const program = new Command();
    registerForgeCommand(program);

    const forgeCmd = program.commands.find((c) => c.name() === 'forge');
    expect(forgeCmd).toBeDefined();
  });

  it('registers the four required subcommands under forge', () => {
    const program = new Command();
    registerForgeCommand(program);

    const forgeCmd = program.commands.find((c) => c.name() === 'forge');
    expect(forgeCmd).toBeDefined();

    const subNames = forgeCmd!.commands.map((c) => c.name());

    expect(subNames).toContain('run');
    expect(subNames).toContain('status');
    expect(subNames).toContain('resume');
    expect(subNames).toContain('personas');
  });

  it('registers "personas list" as a subcommand of "forge personas"', () => {
    const program = new Command();
    registerForgeCommand(program);

    const forgeCmd = program.commands.find((c) => c.name() === 'forge');
    const personasCmd = forgeCmd!.commands.find((c) => c.name() === 'personas');
    expect(personasCmd).toBeDefined();

    const personasSubNames = personasCmd!.commands.map((c) => c.name());
    expect(personasSubNames).toContain('list');
  });

  it('does not modify the parent program name or description', () => {
    const program = new Command('mosaic');
    program.description('Mosaic Stack CLI');
    registerForgeCommand(program);

    expect(program.name()).toBe('mosaic');
    expect(program.description()).toBe('Mosaic Stack CLI');
  });

  it('can be called multiple times without throwing', () => {
    const program = new Command();
    expect(() => {
      registerForgeCommand(program);
    }).not.toThrow();
  });
});

describe('forge run fail-closed behavior (SDLC-D-035)', () => {
  let tmpDir: string;
  let briefPath: string;
  let errSpy: ReturnType<typeof vi.spyOn>;
  let logSpy: ReturnType<typeof vi.spyOn>;
  let prevExitCode: string | number | null | undefined;

  const parse = (args: string[]) => {
    const program = new Command();
    registerForgeCommand(program);
    return program.parseAsync(['forge', ...args], { from: 'user' });
  };

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-cli-failclosed-'));
    briefPath = path.join(tmpDir, 'brief.md');
    fs.writeFileSync(briefPath, '# Fix bug\n\nA bugfix for lint cleanup.');
    errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
    logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    prevExitCode = process.exitCode;
  });

  afterEach(() => {
    errSpy.mockRestore();
    logSpy.mockRestore();
    process.exitCode = prevExitCode;
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it('exits nonzero with a typed FORGE_NO_EXECUTOR error when no executor is wired and --simulate is absent', async () => {
    await parse(['run', '--brief', briefPath, '--codebase', tmpDir]);

    expect(process.exitCode).toBe(1);
    const errText = errSpy.mock.calls.map((c) => c.join(' ')).join('\n');
    expect(errText).toContain('FORGE_NO_EXECUTOR');
    // It must never run the pipeline with a stub and report success.
    expect(fs.existsSync(path.join(tmpDir, '.forge', 'runs'))).toBe(false);
  });

  it('completes with typed simulated results and exit 0 under explicit --simulate', async () => {
    await parse(['run', '--brief', briefPath, '--codebase', tmpDir, '--simulate']);

    expect(process.exitCode).toBeUndefined();

    // Loud simulated-mode summary.
    const logText = logSpy.mock.calls.map((c) => c.join(' ')).join('\n');
    expect(logText).toContain('SIMULATED');

    // Manifest records the mode and simulated per-result statuses.
    const runsDir = path.join(tmpDir, '.forge', 'runs');
    const runIds = fs.readdirSync(runsDir);
    expect(runIds).toHaveLength(1);
    const manifest = loadManifest(path.join(runsDir, runIds[0]!));
    expect(manifest.mode).toBe('simulated');
    expect(manifest.status).toBe('simulated');
    for (const stageStatus of Object.values(manifest.stages)) {
      expect(stageStatus?.status).toBe('simulated');
      for (const gateResult of stageStatus?.gateResults ?? []) {
        expect(gateResult.outcome).toBe('simulated');
      }
    }
  });

  it('resume exits nonzero with a typed FORGE_NO_EXECUTOR error without --simulate', async () => {
    const runDir = path.join(tmpDir, '.forge', 'runs', '20260101-000000');
    fs.mkdirSync(runDir, { recursive: true });
    fs.writeFileSync(
      path.join(runDir, 'manifest.json'),
      JSON.stringify({
        runId: '20260101-000000',
        brief: briefPath,
        codebase: tmpDir,
        briefClass: 'hotfix',
        classSource: 'frontmatter',
        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' } },
      }),
    );

    await parse(['resume', '20260101-000000', '--project', tmpDir]);

    expect(process.exitCode).toBe(1);
    const errText = errSpy.mock.calls.map((c) => c.join(' ')).join('\n');
    expect(errText).toContain('FORGE_NO_EXECUTOR');
  });
});
