import { lstat, mkdtemp, readFile, readdir, readlink, 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 { registerFleetAgentScaffoldCommand } from './fleet-agent-scaffold-command.js';

let root: string | undefined;

afterEach(async (): Promise<void> => {
  vi.restoreAllMocks();
  process.exitCode = undefined;
  if (root) await rm(root, { recursive: true, force: true });
  root = undefined;
});

async function fleetDataHome(): Promise<string> {
  root = await mkdtemp(join(tmpdir(), 'mosaic-fleet-agent-new-'));
  return join(root, '.mosaic');
}

function program(dataHome: string): Command {
  const result = new Command();
  result.exitOverride();
  const fleet = result.command('fleet');
  registerFleetAgentScaffoldCommand(fleet, { fleetDataHome: dataHome });
  return result;
}

async function files(rootDir: string, prefix = ''): Promise<string[]> {
  const result: string[] = [];
  for (const entry of await readdir(join(rootDir, prefix), { withFileTypes: true })) {
    const path = join(prefix, entry.name);
    if (entry.isDirectory()) result.push(...(await files(rootDir, path)));
    else result.push(path);
  }
  return result.sort();
}

describe('mosaic fleet agent new', (): void => {
  it('creates the exact authored user-data scaffold under a temp ~/.mosaic root', async (): Promise<void> => {
    const dataHome = await fleetDataHome();
    await program(dataHome).parseAsync(['node', 'mosaic', 'fleet', 'agent', 'new', 'mira']);
    const agent = join(dataHome, 'fleet', 'agents', 'mira');

    expect(await files(agent)).toEqual([
      '.claude/.claude.json',
      '.claude/.credentials.json',
      '.claude/CLAUDE.md',
      'SOUL.md',
      'overlay.json',
      'profile.json',
    ]);
    expect(JSON.parse(await readFile(join(agent, 'profile.json'), 'utf8'))).toEqual({
      schema: 1,
      harness: 'claude',
      bundle: 'primary',
      overlay: 'overlay.json',
      env: { MOSAIC_AGENT_NAME: 'mira' },
    });
    expect(await readFile(join(agent, 'SOUL.md'), 'utf8')).toContain('## Identity');
    expect(await readFile(join(agent, '.claude', '.claude.json'), 'utf8')).toEqual(
      `${JSON.stringify({ hasCompletedOnboarding: true, theme: 'dark' }, null, 2)}\n`,
    );
    expect(await readlink(join(agent, '.claude', '.credentials.json'))).toBe(
      join(dataHome, 'auth', 'claude', 'primary', '.credentials.json'),
    );
  });

  it('creates a Pi home without Claude onboarding state', async (): Promise<void> => {
    const dataHome = await fleetDataHome();
    await program(dataHome).parseAsync([
      'node',
      'mosaic',
      'fleet',
      'agent',
      'new',
      'pi-seat',
      '--harness',
      'pi',
    ]);
    expect(await files(join(dataHome, 'fleet', 'agents', 'pi-seat'))).toEqual([
      '.pi/AGENTS.md',
      '.pi/auth.json',
      'SOUL.md',
      'overlay.json',
      'profile.json',
    ]);
  });

  it('round-trips quotes, backticks, and shell-looking input literally', async (): Promise<void> => {
    const dataHome = await fleetDataHome();
    const name = 'seat"`$(literal)`';
    const bundle = 'bundle"`$(literal)`';
    const model = 'model"`$(literal)`';
    await program(dataHome).parseAsync([
      'node',
      'mosaic',
      'fleet',
      'agent',
      'new',
      name,
      '--harness',
      'pi',
      '--bundle',
      bundle,
      '--model',
      model,
    ]);
    const agent = join(dataHome, 'fleet', 'agents', name);
    expect(JSON.parse(await readFile(join(agent, 'profile.json'), 'utf8'))).toMatchObject({
      harness: 'pi',
      bundle,
      model,
      env: { MOSAIC_AGENT_NAME: name },
    });
    expect(await readFile(join(agent, 'SOUL.md'), 'utf8')).toContain(`You are ${name},`);
    expect(await readlink(join(agent, '.pi', 'auth.json'))).toBe(
      join(dataHome, 'auth', 'pi', bundle, 'auth.json'),
    );
  });

  it.each(['', '../outside', '/absolute', 'a/b', 'a\\b'])(
    'rejects unsafe agent name %j with a non-zero outcome',
    async (name: string): Promise<void> => {
      const dataHome = await fleetDataHome();
      const error = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
      try {
        await program(dataHome).parseAsync(['node', 'mosaic', 'fleet', 'agent', 'new', name]);
      } catch {
        // Commander rejects a missing positional before the action. That is also
        // a non-zero CLI failure; all other unsafe names reach the scaffold.
        process.exitCode = 1;
      }
      expect(process.exitCode).toBe(1);
      if (name !== '')
        expect(error).toHaveBeenCalledWith(expect.stringContaining('invalid-request'));
    },
  );

  it.each([
    ['--harness', 'codex'],
    ['--bundle', '../outside'],
    ['--model', ''],
  ])(
    'returns non-zero for invalid %s input',
    async (option: string, value: string): Promise<void> => {
      const dataHome = await fleetDataHome();
      const error = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
      await program(dataHome).parseAsync([
        'node',
        'mosaic',
        'fleet',
        'agent',
        'new',
        'mira',
        option,
        value,
      ]);
      expect(process.exitCode).toBe(1);
      expect(error).toHaveBeenCalledWith(expect.stringContaining('invalid-request'));
    },
  );

  it('is idempotent for byte-identical content and refuses a changed user file', async (): Promise<void> => {
    const dataHome = await fleetDataHome();
    const command = ['node', 'mosaic', 'fleet', 'agent', 'new', 'mira'];
    await program(dataHome).parseAsync(command);
    await program(dataHome).parseAsync(command);
    expect(process.exitCode).toBeUndefined();

    const soul = join(dataHome, 'fleet', 'agents', 'mira', 'SOUL.md');
    await writeFile(soul, '# user-owned change\n');
    const error = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
    await program(dataHome).parseAsync(command);
    expect(process.exitCode).toBe(1);
    expect(error).toHaveBeenCalledWith(expect.stringContaining('SOUL.md'));
    expect(await readFile(soul, 'utf8')).toBe('# user-owned change\n');
  });

  it('does not follow a managed credential link while comparing existing content', async (): Promise<void> => {
    const dataHome = await fleetDataHome();
    await program(dataHome).parseAsync(['node', 'mosaic', 'fleet', 'agent', 'new', 'mira']);
    const credential = join(dataHome, 'fleet', 'agents', 'mira', '.claude', '.credentials.json');
    expect((await lstat(credential)).isSymbolicLink()).toBe(true);
    await program(dataHome).parseAsync(['node', 'mosaic', 'fleet', 'agent', 'new', 'mira']);
    expect(process.exitCode).toBeUndefined();
  });
});
