import { describe, expect, it, vi } from 'vitest';
import type { CommandResult, CommandRunner, FleetRoster } from '../commands/fleet.js';
import { TmuxPromotionTransport } from './promotion-transport.js';

const sessionId = 'a'.repeat(64);
const roster: FleetRoster = {
  agents: [{ className: 'worker', name: 'claude-seat', runtime: 'claude' }],
  defaults: { workingDirectory: '~/src' },
  runtimes: {},
  tmux: { holderSession: '_holder', socketName: 'mosaic-fleet' },
  transport: 'tmux',
  version: 1,
};

function result(stdout = '', exitCode = 0, stderr = ''): CommandResult {
  return { exitCode, stderr, stdout };
}

describe('TmuxPromotionTransport', () => {
  it('resolves the exact roster seat and sends the registered command literally', async () => {
    const runner = vi
      .fn<CommandRunner>()
      .mockResolvedValueOnce(result('1234 claude 0 0 0 0\n'))
      .mockResolvedValueOnce(result())
      .mockResolvedValueOnce(result());
    const environmentReader = vi.fn(async () => `MOSAIC_LEASE_SESSION_ID=${sessionId}\0`);
    const transport = new TmuxPromotionTransport({
      environmentReader,
      mosaicHome: '/mosaic',
      rosterLoader: async () => roster,
      runner,
    });

    const target = await transport.resolve('claude-seat');
    await transport.sendPromotion(target);

    expect(target).toEqual({
      bundle: 'mosaic-fleet',
      seat: 'claude-seat',
      sessionId,
    });
    expect(environmentReader).toHaveBeenCalledWith(1234);
    expect(runner).toHaveBeenNthCalledWith(2, 'tmux', [
      '-L',
      'mosaic-fleet',
      'send-keys',
      '-t',
      '=claude-seat:0.0',
      '-l',
      '/mosaic-promote',
    ]);
    expect(runner).toHaveBeenNthCalledWith(3, 'tmux', [
      '-L',
      'mosaic-fleet',
      'send-keys',
      '-t',
      '=claude-seat:0.0',
      'Enter',
    ]);
  });

  // Regression for #1124: the launcher runs claude as a spawnSync CHILD of
  // node(mosaic), so the lease env is on the child, not the tmux pane pid. The
  // transport must WALK the subtree. This test exercises the real walk (no
  // full mock of the resolution) — the seam the original unit test hid.
  it('walks the pane subtree to the claude child that carries the lease id', async () => {
    const runner = vi.fn<CommandRunner>().mockResolvedValueOnce(result('1234 node 0 0 0 0\n'));
    // pane pid 1234 = node(mosaic): NO lease env. child 5678 = claude: carries it.
    const environmentReader = vi.fn(async (pid: number) =>
      pid === 5678 ? `FOO=bar\0MOSAIC_LEASE_SESSION_ID=${sessionId}\0` : `FOO=bar\0`,
    );
    const childrenReader = vi.fn(async (pid: number) => (pid === 1234 ? [5678] : []));
    const transport = new TmuxPromotionTransport({
      environmentReader,
      childrenReader,
      mosaicHome: '/mosaic',
      rosterLoader: async () => roster,
      runner,
    });

    const target = await transport.resolve('claude-seat');

    expect(target.sessionId).toBe(sessionId);
    expect(environmentReader).toHaveBeenCalledWith(1234); // pane pid: no lease
    expect(environmentReader).toHaveBeenCalledWith(5678); // walked to the child
    expect(childrenReader).toHaveBeenCalledWith(1234); // walk actually ran
  });

  it('fails closed when no process in the pane subtree carries a lease id', async () => {
    const runner = vi.fn<CommandRunner>().mockResolvedValueOnce(result('1234 node 0 0 0 0\n'));
    const environmentReader = vi.fn(async () => `FOO=bar\0`);
    const childrenReader = vi.fn(async (pid: number) => (pid === 1234 ? [5678] : []));
    const transport = new TmuxPromotionTransport({
      environmentReader,
      childrenReader,
      mosaicHome: '/mosaic',
      rosterLoader: async () => roster,
      runner,
    });

    await expect(transport.resolve('claude-seat')).rejects.toThrow('no readable lease session');
    expect(childrenReader).toHaveBeenCalledWith(1234);
  });
});
