import {
  chmod,
  mkdir,
  mkdtemp,
  readFile,
  rename,
  rm,
  stat,
  symlink,
  writeFile,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
  AgentEnvBoundaryError,
  parseAgentEnvironment,
  renderGeneratedAgentEnvironment,
  writeAgentEnvironmentProjection,
} from './generated-env-boundary.js';

const generatedValues = {
  MOSAIC_AGENT_NAME: 'coder0',
  MOSAIC_AGENT_CLASS: 'code',
  MOSAIC_AGENT_RUNTIME: 'pi',
  MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol',
  MOSAIC_AGENT_REASONING: 'high',
  MOSAIC_AGENT_TOOL_POLICY: 'code',
  MOSAIC_AGENT_WORKDIR: '/srv/mosaic',
  MOSAIC_TMUX_SOCKET: 'mosaic-fleet',
};

describe('generated fleet agent environment boundary', (): void => {
  let cleanup: string | undefined;

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

  it('renders a deterministic complete generated projection', (): void => {
    expect(renderGeneratedAgentEnvironment(generatedValues)).toBe(
      [
        'MOSAIC_AGENT_NAME=coder0',
        'MOSAIC_AGENT_CLASS=code',
        'MOSAIC_AGENT_RUNTIME=pi',
        'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol',
        'MOSAIC_AGENT_REASONING=high',
        'MOSAIC_AGENT_TOOL_POLICY=code',
        'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
        'MOSAIC_TMUX_SOCKET=mosaic-fleet',
        '',
      ].join('\n'),
    );
  });

  it.each([
    ['generated-key shadowing', 'MOSAIC_AGENT_RUNTIME=codex\n'],
    ['unknown key', 'UNRELATED_SETTING=value\n'],
    ['arbitrary command', 'MOSAIC_AGENT_COMMAND=mosaic yolo codex\n'],
    ['sensitive key', 'MOSAIC_AGENT_TOKEN=do-not-log-me\n'],
    ['malformed syntax', 'export MOSAIC_RUNTIME_BIN=/opt/bin\n'],
    ['duplicate keys', 'MOSAIC_RUNTIME_BIN=/opt/bin\nMOSAIC_RUNTIME_BIN=/other/bin\n'],
  ])('rejects local %s without echoing values', (_name: string, source: string): void => {
    let error: unknown;
    try {
      parseAgentEnvironment(source, 'local');
    } catch (caught: unknown) {
      error = caught;
    }

    expect(error).toBeInstanceOf(AgentEnvBoundaryError);
    expect(String(error)).not.toContain('mosaic yolo codex');
    expect(String(error)).not.toContain('do-not-log-me');
    expect(String(error)).toMatch(/key=.*sha256=/);
  });

  it('rejects unsafe generated paths before any launch consumer can use them', (): void => {
    expect((): void => {
      renderGeneratedAgentEnvironment({
        ...generatedValues,
        MOSAIC_AGENT_WORKDIR: '../outside',
      });
    }).toThrow(AgentEnvBoundaryError);
  });

  it('creates missing managed directories privately before writing a projection', async (): Promise<void> => {
    cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
    const mosaicHome = join(cleanup, 'mosaic');
    const agentEnvDir = join(mosaicHome, 'fleet', 'agents');

    const result = await writeAgentEnvironmentProjection({
      mosaicHome,
      agentEnvDir,
      agentName: 'coder0',
      generated: generatedValues,
    });

    for (const directory of [mosaicHome, join(mosaicHome, 'fleet'), agentEnvDir]) {
      expect((await stat(directory)).mode & 0o777).toBe(0o700);
    }
    expect((await stat(result.generatedPath)).mode & 0o777).toBe(0o600);
  });

  it('regenerates desired keys, relocates safe legacy local data, and quarantines forbidden legacy input', async (): Promise<void> => {
    cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
    const mosaicHome = join(cleanup, 'mosaic');
    const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
    await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
    await writeFile(
      join(agentEnvDir, 'coder0.env'),
      [
        'MOSAIC_AGENT_NAME=stale-name',
        'MOSAIC_AGENT_RUNTIME=codex',
        'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin',
        'MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous',
        '',
      ].join('\n'),
      { mode: 0o600 },
    );

    const result = await writeAgentEnvironmentProjection({
      mosaicHome,
      agentEnvDir,
      agentName: 'coder0',
      generated: generatedValues,
    });

    expect(await readFile(result.generatedPath, 'utf8')).toBe(
      renderGeneratedAgentEnvironment(generatedValues),
    );
    expect(await readFile(result.localPath, 'utf8')).toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
    const quarantine = await readFile(result.quarantinePath!, 'utf8');
    expect(quarantine).toContain('MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous');
    await expect(readFile(join(agentEnvDir, 'coder0.env'), 'utf8')).rejects.toThrow();
    expect((await stat(result.generatedPath)).mode & 0o077).toBe(0);
    expect((await stat(result.localPath)).mode & 0o077).toBe(0);
    expect(result.diagnostics).toEqual([expect.objectContaining({ key: 'MOSAIC_AGENT_COMMAND' })]);
    expect(JSON.stringify(result.diagnostics)).not.toContain('mosaic yolo codex --dangerous');
  });

  it('fails closed on an existing local override with unsafe permissions', async (): Promise<void> => {
    cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
    const mosaicHome = join(cleanup, 'mosaic');
    const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
    await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
    const localPath = join(agentEnvDir, 'coder0.env.local');
    await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
    await chmod(localPath, 0o644);

    await expect(
      writeAgentEnvironmentProjection({
        mosaicHome,
        agentEnvDir,
        agentName: 'coder0',
        generated: generatedValues,
      }),
    ).rejects.toThrow(/permissions/i);
  });

  it('rejects an existing group/world-accessible projection directory before reading it', async (): Promise<void> => {
    cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
    const mosaicHome = join(cleanup, 'mosaic');
    const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
    const localPath = join(agentEnvDir, 'coder0.env.local');
    const generatedPath = join(agentEnvDir, 'coder0.env.generated');
    await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
    await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
    await chmod(agentEnvDir, 0o777);

    await expect(
      writeAgentEnvironmentProjection({
        mosaicHome,
        agentEnvDir,
        agentName: 'coder0',
        generated: generatedValues,
      }),
    ).rejects.toThrow(/unsafe-permissions/i);

    await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
    await expect(readFile(generatedPath, 'utf8')).rejects.toThrow();
  });

  it.each([
    ['MOSAIC_HOME', 'symlink'],
    ['MOSAIC_HOME/fleet', 'symlink'],
    ['MOSAIC_HOME/fleet/agents', 'symlink'],
    ['MOSAIC_HOME', 'group/world-writable'],
    ['MOSAIC_HOME/fleet', 'group/world-writable'],
    ['MOSAIC_HOME/fleet/agents', 'group/world-writable'],
  ])(
    'rejects a %s %s managed ancestor before any projection mutation',
    async (ancestor: string, hazard: string): Promise<void> => {
      cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
      const mosaicHome = join(cleanup, 'mosaic');
      const fleetDir = join(mosaicHome, 'fleet');
      const agentEnvDir = join(fleetDir, 'agents');
      const generatedPath = join(agentEnvDir, 'coder0.env.generated');
      const localPath = join(agentEnvDir, 'coder0.env.local');
      const legacyPath = join(agentEnvDir, 'coder0.env');
      const quarantinePath = join(agentEnvDir, 'coder0.env.quarantine');
      const managedPaths: Record<string, string> = {
        MOSAIC_HOME: mosaicHome,
        'MOSAIC_HOME/fleet': fleetDir,
        'MOSAIC_HOME/fleet/agents': agentEnvDir,
      };
      const unsafePath = managedPaths[ancestor];
      if (unsafePath === undefined) throw new Error(`Unknown managed ancestor: ${ancestor}`);

      await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
      await chmod(mosaicHome, 0o700);
      await chmod(fleetDir, 0o700);
      await chmod(agentEnvDir, 0o700);
      await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
      await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/safe/runtime\n', { mode: 0o600 });
      await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });

      if (hazard === 'symlink') {
        const targetPath = `${unsafePath}-target`;
        await rename(unsafePath, targetPath);
        await symlink(targetPath, unsafePath, 'dir');
      } else {
        await chmod(unsafePath, 0o777);
      }

      await expect(
        writeAgentEnvironmentProjection({
          mosaicHome,
          agentEnvDir,
          agentName: 'coder0',
          generated: generatedValues,
        }),
      ).rejects.toThrow(/unsafe-(directory|permissions)/i);

      await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
      await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/safe/runtime\n');
      await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
        'MOSAIC_AGENT_COMMAND=legacy-command\n',
      );
      await expect(readFile(quarantinePath, 'utf8')).rejects.toThrow();
    },
  );

  it('rejects a symlinked projection directory without mutating its target', async (): Promise<void> => {
    cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
    const mosaicHome = join(cleanup, 'mosaic');
    const targetDirectory = join(cleanup, 'target');
    const linkedDirectory = join(mosaicHome, 'fleet', 'agents');
    const legacyPath = join(targetDirectory, 'coder0.env');
    const generatedPath = join(targetDirectory, 'coder0.env.generated');
    const localPath = join(targetDirectory, 'coder0.env.local');
    const quarantinePath = join(targetDirectory, 'coder0.env.quarantine');
    await mkdir(join(mosaicHome, 'fleet'), { recursive: true, mode: 0o700 });
    await mkdir(targetDirectory, { mode: 0o700 });
    await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });
    await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
    await writeFile(localPath, 'local-before\n', { mode: 0o600 });
    await writeFile(quarantinePath, 'quarantine-before\n', { mode: 0o600 });
    await symlink(targetDirectory, linkedDirectory, 'dir');

    await expect(
      writeAgentEnvironmentProjection({
        mosaicHome,
        agentEnvDir: linkedDirectory,
        agentName: 'coder0',
        generated: generatedValues,
      }),
    ).rejects.toThrow(/unsafe-directory/i);

    await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
      'MOSAIC_AGENT_COMMAND=legacy-command\n',
    );
    await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
    await expect(readFile(localPath, 'utf8')).resolves.toBe('local-before\n');
    await expect(readFile(quarantinePath, 'utf8')).resolves.toBe('quarantine-before\n');
  });
});
