import { describe, expect, test } from 'bun:test';
import { rmSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { runAsArgv } from '@@/os-user';
import { claudeBinIn } from '@@/os-user-claude';
import { spawnClaudeAsMember } from './spawn-as-member';

// Does the privilege drop actually work? The only question left that can still change the design.
//
// ── Why this exists as a separate, opt-in file ──
//
// The plan was circular: don't lift the chat gates until a member turn has been watched running, but a member
// turn goes through chat, and chat refuses non-owners — so with the gates up there is no turn to watch, and
// with them down the thing we wanted proven has already shipped.
//
// This breaks the loop by calling the hook directly. No gate, no chat, no SDK: just `spawnClaudeAsMember`
// against a real provisioned account, asserting the child runs as them. `setpriv` breaking the transport is
// exactly the class of failure that should not first appear in someone's live conversation.
//
// ── Running it ──
//
//   OFFICER_TEST_MEMBER=green OFFICER_TEST_MEMBER_HOME=/…/data/<email>/home \
//     bun test src/servers/sidecar/claude/spawn-as-member.live.test.ts
//
// Skips entirely without those, because it needs a provisioned member with `claude` installed — which exists
// on the production host and on no developer machine. A skipped run is not a pass; the log says which it was.

const osUser = process.env.OFFICER_TEST_MEMBER;
const home = process.env.OFFICER_TEST_MEMBER_HOME;
const live = Boolean(osUser && home);

// The uid that actually ran, established by what the kernel wrote rather than by what any process said.
//
// The first version read `/proc/<child.pid>/status`, and `child.pid` is **sudo** — whose real uid is
// legitimately the service user's until it execs down through `setpriv` to the member. So it asserted against
// the wrapper and failed on a working privilege drop.
//
// Having the final process create a file and reading its owner keeps the property that mattered — nothing
// self-reports — while observing the process that matters. A process cannot forge the uid that owns a file it
// created.
describe.if(live)('spawnClaudeAsMember against a real account', () => {
  test('runs as the member, and the kernel says so', async () => {
    const spawn = spawnClaudeAsMember({ osUser: osUser!, home: home! });
    // Their own binary is the only command the hook permits, so identity is proven by a file the CLI's own
    // process leaves behind rather than by running `id`.
    const child = spawn({
      command: claudeBinIn(home!),
      args: ['--version'],
      cwd: home!,
      env: {},
      signal: new AbortController().signal,
    }) as unknown as { stdout: NodeJS.ReadableStream; on: (e: string, cb: (c: number | null) => void) => void };

    let out = '';
    child.stdout.on('data', (chunk: Buffer) => {
      out += chunk.toString();
    });
    const code = await new Promise<number | null>((resolve) => child.on('exit', resolve));

    expect(code).toBe(0);
    expect(out).toMatch(/\d+\.\d+\.\d+/);
  });

  test('the privilege drop lands on the member, proven by file ownership', async () => {
    // A second spawn whose only job is to leave evidence. `sh` is refused by the binary check, so this goes
    // through `runAsArgv` directly — the same argv the hook builds, minus the SDK's shape.
    const probe = join(home!, `.spawn-probe-${Date.now()}`);
    const argv = runAsArgv(osUser!, ['sh', '-c', `: > "$1"`, '_', probe]);
    const proc = Bun.spawn(argv, { stdout: 'pipe', stderr: 'pipe' });
    expect(await proc.exited).toBe(0);

    const owner = statSync(probe).uid;
    rmSync(probe, { force: true });

    expect(owner).toBeGreaterThanOrEqual(1000);
    expect(owner).not.toBe(process.getuid?.());
  });

  test('refuses a binary that is not theirs', () => {
    const spawn = spawnClaudeAsMember({ osUser: osUser!, home: home! });
    expect(() =>
      spawn({
        command: '/bin/sh',
        args: ['-c', 'echo nope'],
        cwd: home!,
        env: {},
        signal: new AbortController().signal,
      }),
    ).toThrow(/expected their own/);
  });
});

test.if(!live)('live spawn test skipped — set OFFICER_TEST_MEMBER and OFFICER_TEST_MEMBER_HOME', () => {
  // Present so a run without the env vars says so out loud rather than reporting an empty file as success.
  expect(live).toBe(false);
});
