import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
  parseRosterAgents,
  buildFleetCommsBlock,
  renderPeerReach,
  readFleetCommsBlock,
  resolveCommsBlock,
  type CommsPeer,
} from './comms-onboarding.js';

const ROSTER = [
  'version: 1',
  'transport: tmux',
  'agents:',
  '  - name: orchestrator',
  '    runtime: claude',
  '    class: orchestrator',
  '  - name: enhancer',
  '    runtime: claude',
  '    class: enhancer',
  '  - name: coder0',
  '    runtime: pi',
  '    class: implementer',
  '  # a manually-listed cross-host peer (pre-federation stopgap)',
  '  - name: coder0-0',
  '    runtime: claude',
  '    class: implementer',
  '    host: 10.1.10.37',
  '    ssh: jwoltje@10.1.10.37',
  '',
].join('\n');

describe('parseRosterAgents', () => {
  it('parses name + class + optional host/ssh', () => {
    const peers = parseRosterAgents(ROSTER);
    expect(peers.map((p) => p.name)).toEqual(['orchestrator', 'enhancer', 'coder0', 'coder0-0']);
    expect(peers.find((p) => p.name === 'coder0')).toMatchObject({ className: 'implementer' });
    expect(peers.find((p) => p.name === 'coder0-0')).toMatchObject({
      className: 'implementer',
      host: '10.1.10.37',
      ssh: 'jwoltje@10.1.10.37',
    });
    // local agents have no host/ssh
    expect(peers.find((p) => p.name === 'orchestrator')!.host).toBeUndefined();
  });

  it('parses an optional per-agent socket', () => {
    const peers = parseRosterAgents(
      ['agents:', '  - name: a', '    class: worker', '    socket: mosaic-fleet'].join('\n'),
    );
    expect(peers[0]).toMatchObject({ name: 'a', socket: 'mosaic-fleet' });
  });

  it('stops at the next top-level key', () => {
    const peers = parseRosterAgents(
      ['agents:', '  - name: a', '    class: worker', 'defaults:', '  working_directory: ~'].join(
        '\n',
      ),
    );
    expect(peers.map((p) => p.name)).toEqual(['a']);
  });
});

describe('renderPeerReach — same-host vs cross-host', () => {
  const send = '/home/u/.config/mosaic/tools/tmux/agent-send.sh';

  it('renders the short form for a same-host peer', () => {
    const peer: CommsPeer = { name: 'enhancer', className: 'enhancer' };
    expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -s enhancer -m "…"`);
  });

  it('renders the -H form for a cross-host peer using ssh', () => {
    const peer: CommsPeer = {
      name: 'coder0-0',
      className: 'implementer',
      host: '10.1.10.37',
      ssh: 'jwoltje@10.1.10.37',
    };
    expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(
      `${send} -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`,
    );
  });

  it('falls back to host when a cross-host peer has no ssh', () => {
    const peer: CommsPeer = { name: 'x', className: 'worker', host: '10.0.0.9' };
    expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -H 10.0.0.9 -s x -m "…"`);
  });

  it('treats a peer whose host equals the fleet host as same-host', () => {
    const peer: CommsPeer = { name: 'y', className: 'worker', host: 'w-jarvis' };
    expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -s y -m "…"`);
  });

  it('emits NO -L for an unset/default socket', () => {
    const peer: CommsPeer = { name: 'lead', className: 'orchestrator' };
    expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(`${send} -s lead -m "…"`);
  });

  it('emits -L <socket> for a named socket', () => {
    const peer: CommsPeer = { name: 'coder0', className: 'implementer', socket: 'mosaic-fleet' };
    expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(
      `${send} -L mosaic-fleet -s coder0 -m "…"`,
    );
  });

  it('combines -L (named socket) and -H (cross-host) in order', () => {
    const peer: CommsPeer = {
      name: 'coder0-0',
      className: 'implementer',
      host: '10.1.10.37',
      ssh: 'jwoltje@10.1.10.37',
      socket: 'mosaic-fleet',
    };
    expect(renderPeerReach(peer, 'w-jarvis', send)).toBe(
      `${send} -L mosaic-fleet -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`,
    );
  });
});

describe('buildFleetCommsBlock', () => {
  const send = '/h/.config/mosaic/tools/tmux/agent-send.sh';
  const agents = parseRosterAgents(ROSTER);

  it('excludes self, lists peers, flags the orchestrator, and emits both address forms', () => {
    const block = buildFleetCommsBlock({
      selfName: 'enhancer',
      agents,
      fleetHost: 'w-jarvis',
      agentSendPath: send,
    });
    expect(block).toContain('# Fleet Comms');
    expect(block).toContain('You are **enhancer**');
    // criterion 1: agent's own [host:session] identity
    expect(block).toContain('`[w-jarvis:enhancer]`');
    // self excluded
    expect(block).not.toMatch(/\|\s*enhancer\s*\|/);
    // peers present
    expect(block).toContain('| orchestrator |');
    expect(block).toContain('point of contact');
    // same-host peer short form
    expect(block).toContain(`${send} -s coder0 -m "…"`);
    // cross-host peer -H form + host annotation
    expect(block).toContain(`${send} -H jwoltje@10.1.10.37 -s coder0-0 -m "…"`);
    expect(block).toContain('host `10.1.10.37`');
    // conventions
    expect(block).toContain('FLIP the preamble');
    expect(block).toContain('ACCEPTED');
  });

  it('returns empty when the agent has no peers', () => {
    expect(
      buildFleetCommsBlock({
        selfName: 'solo',
        agents: [{ name: 'solo', className: 'orchestrator' }],
        fleetHost: 'h',
        agentSendPath: send,
      }),
    ).toBe('');
  });
});

describe('readFleetCommsBlock — situational (the context a spawned agent gets)', () => {
  let home: string;
  beforeEach(() => {
    home = mkdtempSync(join(tmpdir(), 'mosaic-comms-'));
    mkdirSync(join(home, 'fleet'), { recursive: true });
    writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER);
  });
  afterEach(() => rmSync(home, { recursive: true, force: true }));

  it('builds the cheat-sheet with correct peer addresses for a fleet member', () => {
    const block = readFleetCommsBlock(home, 'orchestrator', 'w-jarvis');
    expect(block).toContain('# Fleet Comms');
    expect(block).toContain('| enhancer |');
    expect(block).toContain(`${join(home, 'tools', 'tmux', 'agent-send.sh')} -s coder0 -m "…"`);
    expect(block).toContain('-H jwoltje@10.1.10.37 -s coder0-0');
    expect(block).not.toMatch(/\|\s*orchestrator\s*\|/); // self excluded
  });

  it('returns empty when MOSAIC_AGENT_NAME is unset, no roster, or agent not a member', () => {
    expect(readFleetCommsBlock(home, undefined, 'w-jarvis')).toBe('');
    expect(readFleetCommsBlock(home, 'stranger', 'w-jarvis')).toBe('');
    expect(readFleetCommsBlock(mkdtempSync(join(tmpdir(), 'noroster-')), 'orchestrator')).toBe('');
  });
});

describe('resolveCommsBlock — `mosaic fleet comms-block <role>` emitter semantics', () => {
  // The emitter wraps readFleetCommsBlock but must NEVER print an empty string silently:
  // an unknown role / missing roster has to fail loud (caller maps !ok → stderr + exit 1)
  // so `mosaic fleet comms-block bogus` is a visible error, not a confusing no-op. The
  // success path returns the block verbatim for `mosaic fleet comms-block <peer>` previews.
  let home: string;
  beforeEach(() => {
    home = mkdtempSync(join(tmpdir(), 'mosaic-commsblk-'));
    mkdirSync(join(home, 'fleet'), { recursive: true });
    writeFileSync(join(home, 'fleet', 'roster.yaml'), ROSTER);
  });
  afterEach(() => rmSync(home, { recursive: true, force: true }));

  it('returns ok + the cheat-sheet for a roster member', () => {
    const res = resolveCommsBlock(home, 'orchestrator', 'w-jarvis');
    expect(res.ok).toBe(true);
    expect(res.output).toContain('# Fleet Comms');
    expect(res.output).toContain('| enhancer |');
    expect(res.error).toBeUndefined();
  });

  it('fails loud (not ok + error naming the role) for a non-member — never silently empty', () => {
    const res = resolveCommsBlock(home, 'stranger', 'w-jarvis');
    expect(res.ok).toBe(false);
    expect(res.output).toBe('');
    expect(res.error).toContain('stranger');
  });

  it('fails loud when no roster exists at the mosaic home', () => {
    const noRoster = mkdtempSync(join(tmpdir(), 'mosaic-noroster-'));
    const res = resolveCommsBlock(noRoster, 'orchestrator', 'w-jarvis');
    expect(res.ok).toBe(false);
    expect(res.error).toBeTruthy();
    rmSync(noRoster, { recursive: true, force: true });
  });

  it('fails loud for a missing role argument', () => {
    const res = resolveCommsBlock(home, undefined, 'w-jarvis');
    expect(res.ok).toBe(false);
    expect(res.error).toBeTruthy();
  });

  it('honors a host override so a peer can preview its own cross-host view', () => {
    // coder0-0 viewing with its own host → its self-identity line uses that host.
    const res = resolveCommsBlock(home, 'coder0-0', '10.1.10.37');
    expect(res.ok).toBe(true);
    expect(res.output).toContain('`[10.1.10.37:coder0-0]`');
  });
});
