import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Command } from 'commander';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { registerFleetMigrationCommand } from './fleet-migration-command.js';

let cleanup: string | undefined;

afterEach(async (): Promise<void> => {
  if (cleanup) await rm(cleanup, { recursive: true, force: true });
  cleanup = undefined;
});

describe('fleet migrate-v1 preview command', (): void => {
  it('emits one ready JSON result without any runtime or file mutation API', async () => {
    cleanup = await mkdtemp(join(tmpdir(), 'fleet-migration-command-'));
    const rolesDir = join(cleanup, 'roles');
    const overrideDir = join(cleanup, 'roles.local');
    await mkdir(rolesDir);
    await mkdir(overrideDir);
    await writeFile(join(rolesDir, 'code.md'), '# code\n');
    const files: Record<string, string> = {
      source: `version: 1\ntransport: tmux\ntmux:\n  socket_name: test\ndefaults:\n  working_directory: /srv\nruntimes:\n  pi:\n    reset_command: /new\nagents:\n  - name: coder0\n    runtime: pi\n    class: implementer\n`,
      decisions: JSON.stringify({
        generation: 2,
        defaultRuntime: 'pi',
        agents: {
          coder0: {
            provider: 'openai',
            model: 'gpt-5.6-sol',
            reasoning: 'high',
            enabled: true,
            launchYolo: false,
            toolPolicyDisposition: { action: 'replace', className: 'code' },
          },
        },
      }),
      observations: JSON.stringify({ coder0: { systemd: 'inactive', tmux: 'missing' } }),
    };
    const printJson = vi.fn();
    const setExitCode = vi.fn();
    const program = new Command();
    const fleet = program.command('fleet').option('--mosaic-home <path>', '', cleanup);
    registerFleetMigrationCommand(fleet, {
      mosaicHome: cleanup,
      rolesDir,
      overrideDir,
      readText: async (path): Promise<string> => files[path] ?? '',
      printJson,
      setExitCode,
    });

    await program.parseAsync([
      'node',
      'test',
      'fleet',
      'migrate-v1',
      'preview',
      '--source',
      'source',
      '--decisions',
      'decisions',
      '--observations',
      'observations',
    ]);

    expect(printJson).toHaveBeenCalledTimes(1);
    expect(printJson).toHaveBeenCalledWith(expect.objectContaining({ status: 'ready' }));
    expect(setExitCode).not.toHaveBeenCalled();
    expect(fleet.helpInformation()).toContain('migrate-v1');
    const migration = fleet.commands.find((command) => command.name() === 'migrate-v1');
    expect(migration?.helpInformation()).not.toMatch(/--write|apply|canary|rollback/);
  });

  it('emits one stable blocked JSON result when --observations is missing', async () => {
    const printJson = vi.fn();
    const setExitCode = vi.fn();
    const program = new Command().exitOverride();
    program.configureOutput({
      writeErr: vi.fn(),
      writeOut: vi.fn(),
    });
    const fleet = program.command('fleet').option('--mosaic-home <path>', '', '/unused');
    registerFleetMigrationCommand(fleet, {
      mosaicHome: '/unused',
      readText: vi.fn(),
      printJson,
      setExitCode,
    });

    await expect(
      program.parseAsync([
        'node',
        'test',
        'fleet',
        'migrate-v1',
        'preview',
        '--source',
        'source',
        '--decisions',
        'decisions',
      ]),
    ).resolves.toBe(program);
    expect(printJson).toHaveBeenCalledTimes(1);
    expect(printJson).toHaveBeenCalledWith({
      status: 'blocked',
      blockers: [
        {
          code: 'missing-migration-preview-option',
          path: 'request.observations',
          detail: 'Required migration preview option is missing.',
        },
      ],
    });
    expect(setExitCode).toHaveBeenCalledWith(1);
  });

  it.each(['source', 'decisions', 'observations'] as const)(
    'emits one stable blocked JSON result when bare --%s has no value',
    async (bareOption) => {
      const printJson = vi.fn();
      const setExitCode = vi.fn();
      const readText = vi.fn();
      const writeErr = vi.fn();
      const program = new Command().exitOverride();
      program.configureOutput({ writeErr, writeOut: vi.fn() });
      const fleet = program.command('fleet').option('--mosaic-home <path>', '', '/unused');
      registerFleetMigrationCommand(fleet, {
        mosaicHome: '/unused',
        readText,
        printJson,
        setExitCode,
      });
      const args = [
        'node',
        'test',
        'fleet',
        'migrate-v1',
        'preview',
        '--source=source',
        '--decisions=decisions',
        '--observations=observations',
      ];
      args[args.findIndex((argument) => argument.startsWith(`--${bareOption}=`))] =
        `--${bareOption}`;

      await expect(program.parseAsync(args)).resolves.toBe(program);

      expect(printJson).toHaveBeenCalledTimes(1);
      expect(printJson).toHaveBeenCalledWith({
        status: 'blocked',
        blockers: [
          {
            code: 'missing-migration-preview-option-value',
            path: `request.${bareOption}`,
            detail: 'Required migration preview option value is missing.',
          },
        ],
      });
      expect(setExitCode).toHaveBeenCalledTimes(1);
      expect(setExitCode).toHaveBeenCalledWith(1);
      expect(readText).not.toHaveBeenCalled();
      expect(writeErr).not.toHaveBeenCalled();
    },
  );

  it.each(['source', 'decisions', 'observations'] as const)(
    'emits one stable blocked JSON result when --%s is present-empty',
    async (emptyOption) => {
      const printJson = vi.fn();
      const setExitCode = vi.fn();
      const readText = vi.fn();
      const program = new Command().exitOverride();
      program.configureOutput({ writeErr: vi.fn(), writeOut: vi.fn() });
      const fleet = program.command('fleet').option('--mosaic-home <path>', '', '/unused');
      registerFleetMigrationCommand(fleet, {
        mosaicHome: '/unused',
        readText,
        printJson,
        setExitCode,
      });
      const paths = { source: 'source', decisions: 'decisions', observations: 'observations' };
      paths[emptyOption] = '';

      await expect(
        program.parseAsync([
          'node',
          'test',
          'fleet',
          'migrate-v1',
          'preview',
          `--source=${paths.source}`,
          `--decisions=${paths.decisions}`,
          `--observations=${paths.observations}`,
        ]),
      ).resolves.toBe(program);

      expect(printJson).toHaveBeenCalledTimes(1);
      expect(printJson).toHaveBeenCalledWith({
        status: 'blocked',
        blockers: [
          {
            code: 'empty-migration-preview-option',
            path: `request.${emptyOption}`,
            detail: 'Required migration preview option must be a non-empty path.',
          },
        ],
      });
      expect(setExitCode).toHaveBeenCalledTimes(1);
      expect(setExitCode).toHaveBeenCalledWith(1);
      expect(readText).not.toHaveBeenCalled();
    },
  );

  it('emits a blocked result and sets exit 1 for malformed evidence', async () => {
    const printJson = vi.fn();
    const setExitCode = vi.fn();
    const program = new Command();
    const fleet = program.command('fleet').option('--mosaic-home <path>', '', '/unused');
    registerFleetMigrationCommand(fleet, {
      mosaicHome: '/unused',
      readText: async (path): Promise<string> => (path === 'decisions' ? 'not-json' : '{}'),
      printJson,
      setExitCode,
    });
    await program.parseAsync([
      'node',
      'test',
      'fleet',
      'migrate-v1',
      'preview',
      '--source',
      'source',
      '--decisions',
      'decisions',
      '--observations',
      'observations',
    ]);
    expect(printJson).toHaveBeenCalledWith(
      expect.objectContaining({
        status: 'blocked',
        blockers: [expect.objectContaining({ code: 'migration-preview-failed' })],
      }),
    );
    expect(setExitCode).toHaveBeenCalledWith(1);
  });

  it('redacts adversarial values from validation failures', async () => {
    const secret = 'never-print-command-or-token';
    const printJson = vi.fn();
    const setExitCode = vi.fn();
    const program = new Command();
    const fleet = program.command('fleet').option('--mosaic-home <path>', '', '/unused');
    registerFleetMigrationCommand(fleet, {
      mosaicHome: '/unused',
      readText: async (path): Promise<string> =>
        path === 'decisions'
          ? JSON.stringify({ generation: 2, agents: {}, [secret]: secret })
          : '{}',
      printJson,
      setExitCode,
    });
    await program.parseAsync([
      'node',
      'test',
      'fleet',
      'migrate-v1',
      'preview',
      '--source',
      'source',
      '--decisions',
      'decisions',
      '--observations',
      'observations',
    ]);
    expect(JSON.stringify(printJson.mock.calls)).not.toContain(secret);
    expect(setExitCode).toHaveBeenCalledWith(1);
  });
});
