// A disconnect must fail only the work that was in flight to the sidecar that actually went away.
//
// This exists because it did not. `unregisterSidecar` walked the whole `pending` map and rejected every
// entry, so `pm2 restart officer-music` would fail a running agent turn with `Sidecar "music" disconnected`
// — a message that points at the wrong process entirely and sends anyone debugging it in the wrong
// direction. Sidecars restart independently by design; that is the whole point of them being PM2 peers.

import { afterEach, describe, expect, test } from 'bun:test';
import { registerSidecar, unregisterSidecar, spawnClaude, startVnc } from './sidecar-registry';

/** A socket that records what was written and never answers, so requests stay pending. */
function silentSocket() {
  const sent: string[] = [];
  return { sent, ws: { send: (data: string) => void sent.push(data) } as never };
}

const registered: string[] = [];

/** Register a fake sidecar and remember it, so a failed assertion can't leak it into the next test. */
function register(name: string, capabilities: string[]) {
  const socket = silentSocket();
  const id = registerSidecar(socket.ws, { type: 'register', name, capabilities });
  registered.push(id);
  return { ...socket, id };
}

afterEach(() => {
  while (registered.length) unregisterSidecar(registered.pop()!);
});

/** Wait until a request has actually reached the socket — spawnClaude awaits waitForCapability first. */
async function awaitSend(socket: { sent: string[] }): Promise<void> {
  for (let i = 0; i < 50 && socket.sent.length === 0; i++) await Bun.sleep(1);
  expect(socket.sent).not.toHaveLength(0);
}

/** Resolves to 'pending' when the promise has not settled shortly after. */
async function settleState(promise: Promise<unknown>): Promise<'pending' | 'resolved' | { rejected: string }> {
  const marker = Symbol('pending');
  const outcome = await Promise.race([
    promise.then(
      () => 'resolved' as const,
      (err: Error) => ({ rejected: err.message }),
    ),
    Bun.sleep(25).then(() => marker),
  ]);
  return outcome === marker ? 'pending' : (outcome as 'resolved' | { rejected: string });
}

const CLAUDE_PARAMS = {
  userId: 1,
  email: 'owner@example.com',
  username: 'owner',
  prompt: 'hello',
  sessionKey: 'test-session',
};

const VNC_PARAMS = { email: 'owner@example.com', username: 'owner' };

describe('unregisterSidecar', () => {
  test('an unrelated sidecar disconnecting leaves other sidecars’ requests in flight', async () => {
    const agent = register('agent', ['claude']);
    const vnc = register('vnc', ['vnc']);

    const turn = spawnClaude(CLAUDE_PARAMS);
    // Take ownership of the eventual rejection immediately, so it is never an unhandled one.
    const turnState = settleState(turn);
    await awaitSend(agent);

    unregisterSidecar(vnc.id);

    // The agent never disconnected, so its turn is still running.
    expect(await turnState).toBe('pending');

    // And when the agent itself goes, its own request fails — naming the right process.
    unregisterSidecar(agent.id);
    expect(await settleState(turn)).toEqual({ rejected: 'Sidecar "agent" disconnected' });
  });

  test('a sidecar’s own disconnect still rejects its own request', async () => {
    const vnc = register('vnc', ['vnc']);

    const start = startVnc(VNC_PARAMS);
    const state = settleState(start);
    await awaitSend(vnc);

    unregisterSidecar(vnc.id);
    expect(await state).toEqual({ rejected: 'Sidecar "vnc" disconnected' });
  });

  test('re-registering under the same name only clears that name’s work', async () => {
    // registerSidecar unregisters an existing same-named sidecar first — a reconnect after a restart.
    // That path runs the same rejection loop, so it has the same blast radius if it is not scoped.
    register('agent', ['claude']);
    const vnc = register('vnc', ['vnc']);

    const desktop = startVnc(VNC_PARAMS);
    const desktopState = settleState(desktop);
    await awaitSend(vnc);

    // The agent reconnects — a different process entirely.
    register('agent', ['claude']);

    expect(await desktopState).toBe('pending');

    unregisterSidecar(vnc.id);
    expect(await settleState(desktop)).toEqual({ rejected: 'Sidecar "vnc" disconnected' });
  });
});
