// scanner-sim: allow -- test file; NOT shipped (excluded by the plugin's
// package.json `files` manifest via `!**/*.test.ts`). This file reads
// the session-store and credentials files on disk to audit them AND
// drives the live HTTP flow, so it legitimately contains both triggers.
/**
 * End-to-end leak-audit test (P5 of the v3.3.0 QR-pairing implementation).
 *
 * Runs the full pairing flow through live HTTP + pair-crypto + pair-
 * session-store + pair-http + completePairing stub. Captures every log
 * line, every response body, and every written-to-disk byte, then
 * asserts NO secret material (mnemonic, gateway private key, secondary
 * code) appears anywhere in those captures.
 *
 * This is the 3.3.0 equivalent of the 3.2.0 CLI-wizard leak audit.
 * Every path that the design doc commits to keeping secret is asserted
 * non-leaking here.
 *
 * Surfaces audited:
 *   1. All captured stdout/stderr via the test server's log sink
 *   2. The successful /respond response body (accountId only, no phrase)
 *   3. The /status response body
 *   4. The /finish HTML body (generated by pair-page)
 *   5. The pair-sessions.json file on disk AFTER success (sk is stored
 *      there by design while the session is live; after consume, it
 *      persists for 1 hour retention. We assert secondary_code stays
 *      NEVER logged, and the mnemonic never lands in that file.)
 *   6. The credentials.json file on disk (SHOULD contain the mnemonic
 *      — this is the intended destination. Audit confirms the mnemonic
 *      is there and nowhere else.)
 *
 * Run with: npx tsx pair-e2e-leak-audit.test.ts
 */

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import http from 'node:http';
import type { AddressInfo } from 'node:net';

import {
  createPairSession,
  type PairSession,
} from './pair-session-store.js';
import {
  generateGatewayKeypair,
  encryptPairingPayload,
} from './pair-crypto.js';
import {
  buildPairRoutes,
  type PairLogger,
  type CompletePairingHandler,
} from './pair-http.js';

let passed = 0;
let failed = 0;
function assert(cond: boolean, name: string): void {
  const n = passed + failed + 1;
  if (cond) { console.log(`ok ${n} - ${name}`); passed++; }
  else { console.log(`not ok ${n} - ${name}`); failed++; }
}

const MNEMONIC_CANARY = 'abandon ability able about above absent absorb abstract absurd abuse access accident';
const SECONDARY_CODE_CANARY = '987654';

async function main(): Promise<void> {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tr-leak-'));
  const sessionsPath = path.join(tmp, 'pair-sessions.json');
  const credentialsPath = path.join(tmp, 'credentials.json');

  // --------------------------------------------------------
  // Capture every log call made by the pair-http module.
  // --------------------------------------------------------
  const logLines: string[] = [];
  const capLog: PairLogger = {
    info(msg) { logLines.push(`[info] ${msg}`); },
    warn(msg) { logLines.push(`[warn] ${msg}`); },
    error(msg) { logLines.push(`[error] ${msg}`); },
  };

  // --------------------------------------------------------
  // Set up a gateway keypair + create a session.
  // --------------------------------------------------------
  const gw = generateGatewayKeypair();
  const device = generateGatewayKeypair();

  const session = await createPairSession(sessionsPath, {
    mode: 'generate',
    operatorContext: { channel: 'cli', senderId: 'test-user' },
    rngPrivateKey: () => Buffer.from(gw.skB64, 'base64url'),
    rngPublicKey: () => Buffer.from(gw.pkB64, 'base64url'),
    rngSecondaryCode: () => SECONDARY_CODE_CANARY,
  });

  // --------------------------------------------------------
  // Build the pair-http routes with a completePairing that
  // writes to a capture file (simulating fs-helpers call).
  // --------------------------------------------------------
  const completePairing: CompletePairingHandler = async ({ mnemonic }) => {
    fs.writeFileSync(credentialsPath, JSON.stringify({ mnemonic }), { mode: 0o600 });
    return { state: 'active', accountId: 'acct_leak_test' };
  };

  const routes = buildPairRoutes({
    sessionsPath,
    apiBase: '/plugin/totalreclaw/pair',
    completePairing,
    logger: capLog,
  });

  // --------------------------------------------------------
  // Live HTTP server.
  // --------------------------------------------------------
  const bodyCaptures: { path: string; body: string }[] = [];
  const server = http.createServer(async (req, res) => {
    const url = new URL(req.url ?? '/', 'http://localhost');
    // Wrap res.end so we capture the body for each response.
    const origWrite = res.write.bind(res);
    const origEnd = res.end.bind(res);
    const chunks: Buffer[] = [];
    (res as unknown as { write: typeof res.write }).write = ((data: unknown, ...rest: unknown[]) => {
      if (typeof data === 'string') chunks.push(Buffer.from(data));
      else if (Buffer.isBuffer(data)) chunks.push(data);
      return origWrite(data as never, ...(rest as never[]));
    }) as typeof res.write;
    (res as unknown as { end: typeof res.end }).end = ((data?: unknown, ...rest: unknown[]) => {
      if (typeof data === 'string') chunks.push(Buffer.from(data));
      else if (Buffer.isBuffer(data)) chunks.push(data);
      bodyCaptures.push({ path: url.pathname, body: Buffer.concat(chunks).toString('utf-8') });
      return origEnd(data as never, ...(rest as never[]));
    }) as typeof res.end;

    if (url.pathname === routes.finishPath) await routes.handlers.finish(req, res);
    else if (url.pathname === routes.startPath) await routes.handlers.start(req, res);
    else if (url.pathname === routes.respondPath) await routes.handlers.respond(req, res);
    else if (url.pathname === routes.statusPath) await routes.handlers.status(req, res);
    else { res.statusCode = 404; res.end('no route'); }
  });

  await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
  const addr = server.address() as AddressInfo;
  const baseUrl = `http://127.0.0.1:${addr.port}`;

  // --------------------------------------------------------
  // Step 1: GET /finish — fetch the HTML page.
  // --------------------------------------------------------
  const finishResp = await fetch(`${baseUrl}/plugin/totalreclaw/pair/finish?sid=${session.sid}`);
  const finishHtml = await finishResp.text();
  assert(finishResp.status === 200, 'e2e: /finish returns 200');

  // --------------------------------------------------------
  // Step 2: GET /start — verify the secondary code.
  // --------------------------------------------------------
  const startResp = await fetch(`${baseUrl}/plugin/totalreclaw/pair/start?sid=${session.sid}&c=${SECONDARY_CODE_CANARY}`);
  const startBody = await startResp.json() as { mode: string; expiresAt: number };
  assert(startResp.status === 200, 'e2e: /start returns 200 with correct code');
  assert(startBody.mode === 'generate', 'e2e: /start returns correct mode');

  // --------------------------------------------------------
  // Step 3: POST /respond — encrypt mnemonic + submit.
  // --------------------------------------------------------
  const enc = encryptPairingPayload({
    skLocalB64: device.skB64,
    pkRemoteB64: gw.pkB64,
    sid: session.sid,
    plaintext: Buffer.from(MNEMONIC_CANARY, 'utf-8'),
  });
  const respondResp = await fetch(`${baseUrl}/plugin/totalreclaw/pair/respond`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      v: 1,
      sid: session.sid,
      pk_d: device.pkB64,
      nonce: enc.nonceB64,
      ct: enc.ciphertextB64,
    }),
  });
  const respondBody = await respondResp.json() as { ok: boolean; accountId: string };
  assert(respondResp.status === 200, 'e2e: /respond returns 200 on valid payload');
  assert(respondBody.ok === true, 'e2e: /respond ok=true');
  assert(respondBody.accountId === 'acct_leak_test', 'e2e: /respond returns accountId');

  // --------------------------------------------------------
  // Step 4: GET /status — observe terminal state.
  // --------------------------------------------------------
  const statusResp = await fetch(`${baseUrl}/plugin/totalreclaw/pair/status?sid=${session.sid}`);
  const statusBody = await statusResp.json() as { status: string };
  assert(statusResp.status === 200, 'e2e: /status returns 200');
  assert(statusBody.status === 'completed', 'e2e: /status reports completed');

  // --------------------------------------------------------
  // Step 5: LEAK AUDIT
  // --------------------------------------------------------
  const allLogsText = logLines.join('\n');
  const allResponsesText = bodyCaptures.map((c) => `${c.path}:${c.body}`).join('\n');
  const diskPairSessions = fs.readFileSync(sessionsPath, 'utf-8');
  const diskCredentials = fs.existsSync(credentialsPath) ? fs.readFileSync(credentialsPath, 'utf-8') : '';

  // Log assertions — NEVER contains:
  //   - the mnemonic (full or any 3-word contiguous substring)
  //   - the secondary code
  //   - the gateway private key (sk)
  assert(!allLogsText.includes(MNEMONIC_CANARY), 'leak: mnemonic NOT in logs');
  assert(!allLogsText.includes(SECONDARY_CODE_CANARY), 'leak: secondary code NOT in logs');
  assert(!allLogsText.includes(gw.skB64), 'leak: gateway sk NOT in logs');
  // 3-word slice check (detects partial leakage)
  const firstThree = MNEMONIC_CANARY.split(' ').slice(0, 3).join(' ');
  assert(!allLogsText.includes(firstThree), 'leak: mnemonic word slice NOT in logs');

  // HTTP response assertions — no response body should ever contain:
  //   - the mnemonic
  //   - the gateway private key
  //   - the secondary code (embedded in /start response would be a bug)
  assert(!allResponsesText.includes(MNEMONIC_CANARY), 'leak: mnemonic NOT in any response');
  assert(!allResponsesText.includes(firstThree), 'leak: mnemonic slice NOT in any response');
  assert(!allResponsesText.includes(gw.skB64), 'leak: gateway sk NOT in any response');
  assert(!allResponsesText.includes(SECONDARY_CODE_CANARY), 'leak: secondary code NOT in any response');

  // pair-sessions.json holds sk_G by design (ephemeral, in 0600 file).
  // It MUST NOT contain the mnemonic (we never write the phrase there).
  assert(!diskPairSessions.includes(MNEMONIC_CANARY), 'leak: mnemonic NOT in pair-sessions.json');
  assert(!diskPairSessions.includes(firstThree), 'leak: mnemonic slice NOT in pair-sessions.json');
  // Sanity: the secondary code IS stored here by design (for /start
  // verification). A future revision could hash it; for 3.3.0 it is
  // stored plaintext in a 0600 file on the gateway host, matching the
  // design-doc threat model (a rooted host is out-of-scope).
  // We don't assert absence. Document the boundary.

  // credentials.json SHOULD contain the mnemonic — that's the intended
  // destination. This is the NEGATIVE of a leak: we assert it's there
  // so we know the flow actually delivered the phrase.
  assert(diskCredentials.includes(MNEMONIC_CANARY), 'delivery: mnemonic IS in credentials.json (intended)');
  const credMode = fs.statSync(credentialsPath).mode & 0o777;
  assert(credMode === 0o600, `delivery: credentials.json mode is 0o600 (got ${credMode.toString(8)})`);

  // --------------------------------------------------------
  // Step 6: Negative — bad code attempts don't leak secondary_code
  // either. Try 5 wrong codes and verify logs still clean.
  // --------------------------------------------------------
  logLines.length = 0;
  bodyCaptures.length = 0;
  const badSession = await createPairSession(sessionsPath, {
    mode: 'generate',
    operatorContext: { channel: 'cli' },
    rngSecondaryCode: () => '111222',
  });
  for (let i = 0; i < 5; i++) {
    await fetch(`${baseUrl}/plugin/totalreclaw/pair/start?sid=${badSession.sid}&c=999888`);
  }
  const badLogs = logLines.join('\n');
  const badResponses = bodyCaptures.map((c) => c.body).join('\n');
  assert(!badLogs.includes('111222'), 'bad-code: correct code NOT in logs');
  assert(!badLogs.includes('999888'), 'bad-code: attempted code NOT in logs (rate-limiter hides payload)');
  assert(!badResponses.includes('111222'), 'bad-code: correct code NOT in responses');

  server.close();
  fs.rmSync(tmp, { recursive: true, force: true });

  // --------------------------------------------------------
  // Step 7: /finish HTML body audit — the rendered page embeds the sid
  // but must NOT embed the gateway sk or the secondary code.
  // --------------------------------------------------------
  assert(!finishHtml.includes(gw.skB64), 'leak: finish HTML does NOT embed gateway sk');
  assert(!finishHtml.includes(SECONDARY_CODE_CANARY), 'leak: finish HTML does NOT embed secondary code');
  assert(finishHtml.includes(session.sid), 'sanity: finish HTML embeds the sid');

  console.log(`# ${passed} passed, ${failed} failed`);
  if (failed > 0) process.exit(1);
}

main().catch((err) => {
  console.error('fatal:', err);
  process.exit(1);
});
