import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { composeContract } from './launch.js';

/**
 * Composer unit test (R7/R8/R9): asserts the launcher-composed runtime contract
 *
 *   - includes the per-tier anchors (CONSTITUTION / AGENTS / USER / runtime),
 *   - keeps the CONSTITUTION block byte-equal to the on-disk file (Tier-3
 *     byte-equality — the bare-launch fallback read must match what is injected),
 *   - merges `*.local.md` operator overlays as deltas-by-value, and omits them
 *     entirely when absent (base-only),
 *   - selects the correct per-harness RUNTIME.md.
 *
 * `composeContract` takes `mosaicHome` as a param, so each test runs against an
 * isolated fixture home. We also chdir to an empty temp cwd so the cwd-relative
 * mission/PRD blocks contribute nothing (deterministic output).
 */

const CONSTITUTION = '# CONSTITUTION\n\nGATE-1: the non-negotiable law.\n';
const AGENTS = '# Mosaic Agent Dispatcher\n\nLoad order + guide router.\n';
const USER = '# operator\n\nName: Test Operator\n';
const TOOLS = '# tools index\n';

function makeHome(): { home: string; root: string } {
  const root = mkdtempSync(join(tmpdir(), 'mosaic-compose-'));
  const home = join(root, 'mosaic-home');
  for (const h of ['claude', 'codex', 'opencode', 'pi']) {
    mkdirSync(join(home, 'runtime', h), { recursive: true });
    writeFileSync(join(home, 'runtime', h, 'RUNTIME.md'), `# ${h} runtime contract\n`);
  }
  writeFileSync(join(home, 'CONSTITUTION.md'), CONSTITUTION);
  writeFileSync(join(home, 'AGENTS.md'), AGENTS);
  writeFileSync(join(home, 'USER.md'), USER);
  writeFileSync(join(home, 'TOOLS.md'), TOOLS);
  return { home, root };
}

describe('composeContract — overlay composer', () => {
  let fixture: ReturnType<typeof makeHome>;
  let prevCwd: string;
  let cwdDir: string;

  beforeEach(() => {
    fixture = makeHome();
    prevCwd = process.cwd();
    cwdDir = mkdtempSync(join(tmpdir(), 'mosaic-cwd-'));
    process.chdir(cwdDir); // neutralize cwd-relative mission/PRD blocks
  });

  afterEach(() => {
    process.chdir(prevCwd);
    rmSync(fixture.root, { recursive: true, force: true });
    rmSync(cwdDir, { recursive: true, force: true });
  });

  it('injects the fleet comms cheat-sheet for a spawned fleet agent (situational)', () => {
    // A spawned agent has MOSAIC_AGENT_NAME set + is a member of the roster.
    mkdirSync(join(fixture.home, 'fleet'), { recursive: true });
    writeFileSync(
      join(fixture.home, 'fleet', 'roster.yaml'),
      [
        'version: 1',
        'transport: tmux',
        'agents:',
        '  - name: orchestrator',
        '    runtime: claude',
        '    class: orchestrator',
        '  - name: enhancer',
        '    runtime: claude',
        '    class: enhancer',
        '  - name: coder0-0',
        '    runtime: claude',
        '    class: implementer',
        '    host: 10.1.10.37',
        '    ssh: jwoltje@10.1.10.37',
        '',
      ].join('\n'),
    );
    const prev = process.env['MOSAIC_AGENT_NAME'];
    try {
      process.env['MOSAIC_AGENT_NAME'] = 'enhancer';
      const out = composeContract('claude', fixture.home);
      expect(out).toContain('# Fleet Comms');
      expect(out).toMatch(/`\[[^\]]+:enhancer\]`/); // own [host:session] identity (host machine-dependent)
      // local peer → no -H; cross-host peer → -H ssh
      expect(out).toContain('-s orchestrator -m "…"');
      expect(out).toContain('-H jwoltje@10.1.10.37 -s coder0-0 -m "…"');
      expect(out).not.toContain('-H jwoltje@10.1.10.37 -s orchestrator'); // local stays local
    } finally {
      if (prev === undefined) delete process.env['MOSAIC_AGENT_NAME'];
      else process.env['MOSAIC_AGENT_NAME'] = prev;
    }
  });

  it('does NOT inject fleet comms when MOSAIC_AGENT_NAME is unset (non-fleet launch)', () => {
    const prev = process.env['MOSAIC_AGENT_NAME'];
    try {
      delete process.env['MOSAIC_AGENT_NAME'];
      expect(composeContract('claude', fixture.home)).not.toContain('# Fleet Comms');
    } finally {
      if (prev !== undefined) process.env['MOSAIC_AGENT_NAME'] = prev;
    }
  });

  it('includes the per-tier anchors and the selected harness runtime', () => {
    const out = composeContract('claude', fixture.home);
    expect(out).toContain('GATE-1: the non-negotiable law.'); // L0
    expect(out).toContain('Mosaic Agent Dispatcher'); // AGENTS
    expect(out).toContain('# User Profile'); // USER header
    expect(out).toContain('Name: Test Operator'); // USER body
    expect(out).toContain('# Runtime-Specific Contract');
    expect(out).toContain('# claude runtime contract');
  });

  it('keeps the CONSTITUTION block byte-equal to the on-disk file (Tier-3)', () => {
    const out = composeContract('pi', fixture.home);
    const onDisk = readFileSync(join(fixture.home, 'CONSTITUTION.md'), 'utf-8');
    // The injected L0 must be a byte-equal substring of the composed blob, so a
    // bare-launch fallback read of CONSTITUTION.md matches what was injected.
    expect(out.includes(onDisk)).toBe(true);
  });

  it('is base-only when no *.local overlays exist', () => {
    const out = composeContract('claude', fixture.home);
    expect(out).not.toContain('# Operator Overlays');
    expect(out).not.toContain('Operator Overlay (USER.local.md)');
    expect(out).not.toContain('Persona Overlay');
    expect(out).not.toContain('Standards Overlay');
  });

  it('merges USER.local.md directly under the operator profile', () => {
    writeFileSync(join(fixture.home, 'USER.local.md'), 'Prefer terse status updates.\n');
    const out = composeContract('claude', fixture.home);
    expect(out).toContain('## Operator Overlay (USER.local.md)');
    expect(out).toContain('Prefer terse status updates.');
    // Overlay appears AFTER its base profile.
    expect(out.indexOf('# User Profile')).toBeLessThan(
      out.indexOf('## Operator Overlay (USER.local.md)'),
    );
  });

  it('merges SOUL.local.md + STANDARDS.local.md as deltas in the Operator Overlays block', () => {
    writeFileSync(join(fixture.home, 'SOUL.local.md'), 'Tone: dry and direct.\n');
    writeFileSync(join(fixture.home, 'STANDARDS.local.md'), 'Require 90% coverage on auth code.\n');
    const out = composeContract('claude', fixture.home);
    expect(out).toContain('# Operator Overlays');
    expect(out).toContain('## Persona Overlay (SOUL.local.md)');
    expect(out).toContain('Tone: dry and direct.');
    expect(out).toContain('## Standards Overlay (STANDARDS.local.md)');
    expect(out).toContain('Require 90% coverage on auth code.');
  });

  it('ignores whitespace-only *.local overlays (no empty overlay section)', () => {
    writeFileSync(join(fixture.home, 'SOUL.local.md'), '   \n\n');
    const out = composeContract('claude', fixture.home);
    expect(out).not.toContain('# Operator Overlays');
  });

  it('selects a different RUNTIME.md per harness', () => {
    expect(composeContract('codex', fixture.home)).toContain('# codex runtime contract');
    expect(composeContract('pi', fixture.home)).toContain('# pi runtime contract');
    expect(composeContract('codex', fixture.home)).not.toContain('# pi runtime contract');
  });

  it('injects the configured operator-interaction tool policy into the runtime contract', () => {
    const previous = process.env['MOSAIC_AGENT_TOOL_POLICY'];
    try {
      process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'operator-interaction';
      const out = composeContract('pi', fixture.home);
      expect(out).toContain('# Fleet Tool Policy (operator-interaction)');
      expect(out).toContain('Denied by default: coding/general orchestration claims');
    } finally {
      if (previous === undefined) delete process.env['MOSAIC_AGENT_TOOL_POLICY'];
      else process.env['MOSAIC_AGENT_TOOL_POLICY'] = previous;
    }
  });

  // ── Persona contract injection (A3b) ──────────────────────────────────────
  // composeContract reads MOSAIC_AGENT_CLASS and injects the resolved persona
  // (override-aware). Save/restore the env so these tests don't leak state.
  describe('persona contract (A3b)', () => {
    let prevClass: string | undefined;

    beforeEach(() => {
      prevClass = process.env['MOSAIC_AGENT_CLASS'];
    });

    afterEach(() => {
      if (prevClass === undefined) delete process.env['MOSAIC_AGENT_CLASS'];
      else process.env['MOSAIC_AGENT_CLASS'] = prevClass;
    });

    const seedBaseline = (klass: string, body: string): void => {
      mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true });
      writeFileSync(join(fixture.home, 'fleet', 'roles', `${klass}.md`), body);
    };
    const seedOverride = (klass: string, body: string): void => {
      mkdirSync(join(fixture.home, 'fleet', 'roles.local'), { recursive: true });
      writeFileSync(join(fixture.home, 'fleet', 'roles.local', `${klass}.md`), body);
    };

    it('injects the baseline persona when MOSAIC_AGENT_CLASS is set and a role file exists', () => {
      seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE: ship the lane.\n');
      process.env['MOSAIC_AGENT_CLASS'] = 'coder';
      const out = composeContract('claude', fixture.home);
      expect(out).toContain('# Persona Contract (coder)');
      expect(out).toContain('BASELINE-MANDATE');
    });

    it('OVERRIDE WINS at launch: roles.local persona is injected over baseline (AC-NS-7)', () => {
      seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE.\n');
      seedOverride('coder', '# Coder (override)\n\n(`class: coder`)\n\nOVERRIDE-MANDATE.\n');
      process.env['MOSAIC_AGENT_CLASS'] = 'coder';
      const out = composeContract('claude', fixture.home);
      expect(out).toContain('# Persona Contract (coder)');
      expect(out).toContain('OVERRIDE-MANDATE');
      expect(out).not.toContain('BASELINE-MANDATE');
    });

    it('does NOT inject a persona when MOSAIC_AGENT_CLASS is unset', () => {
      seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE.\n');
      delete process.env['MOSAIC_AGENT_CLASS'];
      const out = composeContract('claude', fixture.home);
      expect(out).not.toContain('# Persona Contract');
    });

    it('does NOT inject (no throw) when MOSAIC_AGENT_CLASS names an unknown class', () => {
      seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE.\n');
      process.env['MOSAIC_AGENT_CLASS'] = 'nonexistent';
      expect(() => composeContract('claude', fixture.home)).not.toThrow();
      expect(composeContract('claude', fixture.home)).not.toContain('# Persona Contract');
    });

    it('places the persona contract BEFORE the fleet comms block (identity, then peers)', () => {
      seedBaseline('enhancer', '# Enhancer\n\n(`class: enhancer`)\n\nIMPROVE.\n');
      mkdirSync(join(fixture.home, 'fleet'), { recursive: true });
      writeFileSync(
        join(fixture.home, 'fleet', 'roster.yaml'),
        [
          'agents:',
          '  - name: orchestrator',
          '    class: orchestrator',
          '  - name: enhancer',
          '    class: enhancer',
          '',
        ].join('\n'),
      );
      const prevName = process.env['MOSAIC_AGENT_NAME'];
      try {
        process.env['MOSAIC_AGENT_CLASS'] = 'enhancer';
        process.env['MOSAIC_AGENT_NAME'] = 'enhancer';
        const out = composeContract('claude', fixture.home);
        expect(out).toContain('# Persona Contract (enhancer)');
        expect(out).toContain('# Fleet Comms');
        expect(out.indexOf('# Persona Contract')).toBeLessThan(out.indexOf('# Fleet Comms'));
      } finally {
        if (prevName === undefined) delete process.env['MOSAIC_AGENT_NAME'];
        else process.env['MOSAIC_AGENT_NAME'] = prevName;
      }
    });
  });
});
