import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { once } from "node:events";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { createDeviceAuthStore } from "./device-auth-store.js";

const ALLOWED_ORIGIN = "https://operator.example.ts.net";
const MUTATION_HEADERS = { origin: ALLOWED_ORIGIN, "x-code-remote-csrf": "1" };

test("health stays public while account data requires device authentication", async () => {
  await withBackend(async ({ baseUrl }) => {
    const health = await fetch(`${baseUrl}/health`);
    assert.equal(health.status, 200);
    const healthBody = await health.json();
    assert.equal(healthBody.authentication.required, true);
    assert.equal("pc" in healthBody, false);

    await assertResponse(fetch(`${baseUrl}/accounts`), 401, "authentication_required");
    await assertResponse(fetch(`${baseUrl}/accounts`, { headers: { origin: ALLOWED_ORIGIN } }), 401, "authentication_required");
    await assertResponse(fetch(`${baseUrl}/accounts`, { headers: { cookie: "crm_device=malformed" } }), 401, "authentication_required");
  });
});

test("an explicit local exemption skips device credentials but preserves request guards", async () => {
  await withBackend(async ({ baseUrl }) => {
    const health = await fetch(`${baseUrl}/health`);
    assert.equal(health.status, 200);
    assert.equal((await health.json()).authentication.required, false);

    assert.equal((await fetch(`${baseUrl}/accounts`)).status, 200);
    const session = await fetch(`${baseUrl}/auth/session`);
    assert.equal(session.status, 200);
    assert.deepEqual(await session.json(), {
      device: {
        id: "authentication-exempt",
        label: "Authentication exempt",
        createdAt: null,
        expiresAt: null,
        revoked: false,
        current: true
      }
    });

    const body = JSON.stringify({});
    await assertResponse(fetch(`${baseUrl}/sessions`, {
      method: "POST",
      headers: { "content-type": "application/json", "x-code-remote-csrf": "1" },
      body
    }), 403, "origin_required");
    await assertResponse(fetch(`${baseUrl}/sessions`, {
      method: "POST",
      headers: { origin: ALLOWED_ORIGIN, "content-type": "application/json" },
      body
    }), 403, "csrf_required");
    await assertResponse(fetch(`${baseUrl}/sessions/events`), 403, "origin_required");
  }, { deviceAuthentication: { required: false } });
});

test("pairing issues an HttpOnly device cookie that authenticates protected HTTP and SSE reads", async () => {
  await withBackend(async ({ baseUrl, pair }) => {
    const enrolled = await pair("Operator phone");
    assert.equal(enrolled.response.status, 201);
    assert.match(enrolled.setCookie, /HttpOnly/i);
    assert.match(enrolled.setCookie, /Path=\/api/i);
    assert.match(enrolled.setCookie, /SameSite=Strict/i);
    assert.match(enrolled.setCookie, /Secure/i);
    assert.equal(enrolled.setCookie.includes(enrolled.pairing.code), false);

    const accounts = await fetch(`${baseUrl}/accounts`, { headers: { cookie: enrolled.cookie } });
    assert.equal(accounts.status, 200);

    await assertResponse(fetch(`${baseUrl}/sessions/events`, {
      headers: { cookie: enrolled.cookie }
    }), 403, "origin_required");

    const controller = new AbortController();
    const events = await fetch(`${baseUrl}/sessions/events`, {
      headers: { cookie: enrolled.cookie, origin: ALLOWED_ORIGIN },
      signal: controller.signal
    });
    assert.equal(events.status, 200);
    assert.match(events.headers.get("content-type") ?? "", /text\/event-stream/);
    controller.abort();
  });
});

test("native first-scan pairing receives a cross-site Secure cookie", async () => {
  await withBackend(async ({ baseUrl, pair }) => {
    const enrolled = await pair("Mobile app", "https://localhost");
    assert.equal(enrolled.response.status, 201);
    assert.match(enrolled.setCookie, /HttpOnly/i);
    assert.match(enrolled.setCookie, /SameSite=None/i);
    assert.match(enrolled.setCookie, /Secure/i);
    const accounts = await fetch(`${baseUrl}/accounts`, { headers: { cookie: enrolled.cookie, origin: "https://localhost" } });
    assert.equal(accounts.status, 200);
    assert.equal(accounts.headers.get("access-control-allow-origin"), "https://localhost");
  });
});

test("authenticated mutations still require the exact Origin and CSRF header", async () => {
  await withBackend(async ({ baseUrl, pair }) => {
    const enrolled = await pair("Operator phone");
    const body = JSON.stringify({});
    const cookie = enrolled.cookie;

    await assertResponse(fetch(`${baseUrl}/sessions`, {
      method: "POST",
      headers: { cookie, "content-type": "application/json", "x-code-remote-csrf": "1" },
      body
    }), 403, "origin_required");
    await assertResponse(fetch(`${baseUrl}/sessions`, {
      method: "POST",
      headers: { cookie, origin: "https://evil.example", "content-type": "application/json", "x-code-remote-csrf": "1" },
      body
    }), 403, "origin_not_allowed");
    await assertResponse(fetch(`${baseUrl}/sessions`, {
      method: "POST",
      headers: { cookie, origin: ALLOWED_ORIGIN, "content-type": "application/json" },
      body
    }), 403, "csrf_required");
  });
});

test("authenticated message response loss, SSE reconnect, and backend restart execute one action", async () => {
  await withBackend(async ({ baseUrl, pair, restart }) => {
    const enrolled = await pair("Operator phone");
    const headers = { ...MUTATION_HEADERS, cookie: enrolled.cookie, "content-type": "application/json" };
    const clientSessionId = "abababab-abab-4aba-8aba-abababababab";
    const clientMessageId = "cdcdcdcd-cdcd-4cdc-8dcd-cdcdcdcdcdcd";
    const sessionBody = JSON.stringify({
      accountId: "account-a",
      workspaceId: "project",
      clientSessionId
    });
    const messageBody = JSON.stringify({ text: "Execute exactly once", clientMessageId });

    const created = await fetch(`${baseUrl}/sessions`, { method: "POST", headers, body: sessionBody });
    assert.equal(created.status, 201);
    assert.equal((await created.json()).session.id, clientSessionId);

    const first = await fetch(`${baseUrl}/sessions/${clientSessionId}/messages`, {
      method: "POST",
      headers,
      body: messageBody
    });
    assert.equal(first.status, 202); // Treat the response body as lost.

    const replay = await fetch(`${baseUrl}/sessions/${clientSessionId}/messages`, {
      method: "POST",
      headers,
      body: messageBody
    });
    assert.equal(replay.status, 202);
    assert.deepEqual((await replay.json()).delivery, { disposition: "started" });
    assert.equal(acceptedMessageCount(await readSessionSnapshot(baseUrl, clientSessionId, enrolled.cookie)), 1);
    assert.equal(acceptedMessageCount(await readSessionSnapshot(baseUrl, clientSessionId, enrolled.cookie)), 1);

    await restart();
    const recreated = await fetch(`${baseUrl}/sessions`, { method: "POST", headers, body: sessionBody });
    assert.equal(recreated.status, 201);
    assert.equal((await recreated.json()).session.id, clientSessionId);
    const restartedReplay = await fetch(`${baseUrl}/sessions/${clientSessionId}/messages`, {
      method: "POST",
      headers,
      body: messageBody
    });
    assert.equal(restartedReplay.status, 202);
    assert.deepEqual((await restartedReplay.json()).delivery, { disposition: "started" });
    assert.equal(acceptedMessageCount(await readSessionSnapshot(baseUrl, clientSessionId, enrolled.cookie)), 1);
  });
});

test("authenticated attachment upload survives restart, reaches the fake engine, and deletes safely", async () => {
  await withBackend(async ({ baseUrl, directory, pair, restart }) => {
    const enrolled = await pair("Operator phone");
    const headers = { ...MUTATION_HEADERS, cookie: enrolled.cookie, "content-type": "application/json" };
    const sessionId = "12121212-1212-4212-8212-121212121212";
    const created = await fetch(`${baseUrl}/sessions`, {
      method: "POST",
      headers,
      body: JSON.stringify({ accountId: "account-a", workspaceId: "project", clientSessionId: sessionId })
    });
    assert.equal(created.status, 201);

    await assertResponse(fetch(`${baseUrl}/sessions/${sessionId}/attachments`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        name: "forged.png",
        mimeType: "image/png",
        dataBase64: Buffer.from("not an image").toString("base64")
      })
    }), 400, "unsupported_attachment_type");

    const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
    const uploaded = await fetch(`${baseUrl}/sessions/${sessionId}/attachments`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        name: "browser-claimed.jpg",
        mimeType: "image/jpeg",
        dataBase64: png.toString("base64")
      })
    });
    assert.equal(uploaded.status, 201);
    const uploadBody = await uploaded.json();
    assert.equal(uploadBody.attachment.mimeType, "image/png");
    assert.equal(uploadBody.attachment.name, "browser-claimed.png");
    assert.equal(uploadBody.attachment.size, png.length);
    assert.equal(JSON.stringify(uploadBody).includes(directory), false);

    const attachmentId = uploadBody.attachment.id;
    const downloaded = await fetch(`${baseUrl}/sessions/${sessionId}/attachments/${attachmentId}`, {
      headers: { cookie: enrolled.cookie, origin: ALLOWED_ORIGIN }
    });
    assert.equal(downloaded.status, 200);
    assert.equal(downloaded.headers.get("content-type"), "image/png");
    assert.equal(downloaded.headers.get("x-content-type-options"), "nosniff");
    assert.deepEqual(Buffer.from(await downloaded.arrayBuffer()), png);

    await restart();
    const restored = await readSessionSnapshot(baseUrl, sessionId, enrolled.cookie);
    assert.equal(JSON.stringify(restored).includes(directory), false);
    const restoredDownload = await fetch(`${baseUrl}/sessions/${sessionId}/attachments/${attachmentId}`, {
      headers: { cookie: enrolled.cookie, origin: ALLOWED_ORIGIN }
    });
    assert.equal(restoredDownload.status, 200);
    assert.deepEqual(Buffer.from(await restoredDownload.arrayBuffer()), png);

    const sent = await fetch(`${baseUrl}/sessions/${sessionId}/messages`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        text: "Inspect this test image",
        attachments: [{ id: attachmentId }],
        clientMessageId: "34343434-3434-4434-8434-343434343434"
      })
    });
    assert.equal(sent.status, 202);
    const completed = await waitForSessionSnapshot(
      baseUrl,
      sessionId,
      enrolled.cookie,
      (snapshot) => snapshot.session.status === "completed"
    );
    const accepted = completed.events.filter((event) => event.event === "message.accepted");
    assert.equal(accepted.length, 1);
    assert.equal(accepted[0].data.attachments[0].id, attachmentId);
    assert.equal(JSON.stringify(completed).includes(directory), false);

    const deleted = await fetch(`${baseUrl}/sessions/${sessionId}/attachments/${attachmentId}`, {
      method: "DELETE",
      headers
    });
    assert.equal(deleted.status, 200);
    assert.deepEqual(await deleted.json(), { deletion: { attachmentId, deleted: true } });
    await assertResponse(fetch(`${baseUrl}/sessions/${sessionId}/attachments/${attachmentId}`, {
      headers: { cookie: enrolled.cookie, origin: ALLOWED_ORIGIN }
    }), 404, "attachment_not_found");
  });
});

test("rotation, independent revocation, expiry, and backend restart reject old credentials", async () => {
  await withBackend(async ({ baseUrl, directory, pair, restart }) => {
    const first = await pair("First phone");
    const second = await pair("Tablet");

    const rotatedResponse = await fetch(`${baseUrl}/auth/device/rotate`, {
      method: "POST",
      headers: { ...MUTATION_HEADERS, cookie: first.cookie }
    });
    assert.equal(rotatedResponse.status, 200);
    const rotatedCookie = cookiePair(rotatedResponse.headers.get("set-cookie"));
    await assertResponse(fetch(`${baseUrl}/accounts`, { headers: { cookie: first.cookie } }), 401, "authentication_required");
    assert.equal((await fetch(`${baseUrl}/accounts`, { headers: { cookie: rotatedCookie } })).status, 200);

    const firstDeviceId = first.body.device.id;
    const revoked = await fetch(`${baseUrl}/auth/devices/${encodeURIComponent(firstDeviceId)}`, {
      method: "DELETE",
      headers: { ...MUTATION_HEADERS, cookie: second.cookie }
    });
    assert.equal(revoked.status, 200);
    await assertResponse(fetch(`${baseUrl}/accounts`, { headers: { cookie: rotatedCookie } }), 401, "authentication_required");
    assert.equal((await fetch(`${baseUrl}/accounts`, { headers: { cookie: second.cookie } })).status, 200);

    await restart();
    assert.equal((await fetch(`${baseUrl}/accounts`, { headers: { cookie: second.cookie } })).status, 200);

    const statePath = join(directory, "data", "security", "device-auth.json");
    const state = JSON.parse(await readFile(statePath, "utf8"));
    const secondDevice = state.devices.find((device) => device.id === second.body.device.id);
    secondDevice.expiresAt = "2000-01-01T00:00:00.000Z";
    await writeFile(statePath, JSON.stringify(state));
    await assertResponse(fetch(`${baseUrl}/accounts`, { headers: { cookie: second.cookie } }), 401, "authentication_required");
  });
});

async function withBackend(run, configOverrides = {}) {
  const directory = await mkdtemp(join(tmpdir(), "crm-auth-http-"));
  const port = await availablePort();
  const configPath = join(directory, "config.json");
  const baseUrl = `http://127.0.0.1:${port}`;
  const dataDir = join(directory, "data");
  await writeFile(configPath, JSON.stringify({
    schemaVersion: 1,
    codexCliPath: "codex.cmd",
    engine: "fake",
    bindHost: "127.0.0.1",
    port,
    allowedOrigins: [ALLOWED_ORIGIN],
    dataDir,
    accounts: [{ id: "account-a", label: "Account", codexHome: join(directory, "codex-home") }],
    workspaces: [{ id: "project", label: "Project", path: directory }],
    ...configOverrides
  }));

  let backend;
  async function start() {
    backend = spawn(process.execPath, [fileURLToPath(new URL("./index.js", import.meta.url))], {
      env: { ...process.env, CODE_REMOTE_MANAGER_CONFIG_PATH: configPath },
      stdio: "ignore"
    });
    await waitForResponse(`${baseUrl}/health`);
  }
  async function stop() {
    if (!backend || backend.exitCode !== null) return;
    backend.kill();
    await once(backend, "exit");
  }
  async function restart() {
    await stop();
    await start();
  }
  async function pair(label, origin = ALLOWED_ORIGIN) {
    const store = createDeviceAuthStore(dataDir);
    const pairing = await store.createPairing({ origin });
    const response = await fetch(`${baseUrl}/auth/pair`, {
      method: "POST",
      headers: { origin, "x-code-remote-csrf": "1", "content-type": "application/json" },
      body: JSON.stringify({ attemptId: pairing.attemptId, code: pairing.code, label })
    });
    const body = await response.json();
    const setCookie = response.headers.get("set-cookie") ?? "";
    return { response, body, pairing, setCookie, cookie: cookiePair(setCookie) };
  }

  try {
    await start();
    await run({ baseUrl, directory, pair, restart });
  } finally {
    await stop();
    await rm(directory, { recursive: true, force: true });
  }
}

async function assertResponse(responsePromise, status, error) {
  const response = await responsePromise;
  assert.equal(response.status, status);
  assert.deepEqual(await response.json(), { error });
}

function cookiePair(setCookie) {
  const pair = String(setCookie ?? "").split(";", 1)[0];
  if (!pair.startsWith("crm_device=")) throw new Error("device_cookie_missing");
  return pair;
}

async function availablePort() {
  const server = createServer();
  await new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(0, "127.0.0.1", resolve);
  });
  const address = server.address();
  await new Promise((resolve) => server.close(resolve));
  return address.port;
}

async function waitForResponse(url) {
  const deadline = Date.now() + 5000;
  while (Date.now() < deadline) {
    try {
      return await fetch(url);
    } catch {
      await new Promise((resolve) => setTimeout(resolve, 25));
    }
  }
  throw new Error("backend_start_timeout");
}

async function readSessionSnapshot(baseUrl, sessionId, cookie) {
  const response = await fetch(`${baseUrl}/sessions/${encodeURIComponent(sessionId)}/events`, {
    headers: { cookie, origin: ALLOWED_ORIGIN }
  });
  assert.equal(response.status, 200);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  try {
    while (!buffer.includes("\n\n")) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
    }
  } finally {
    await reader.cancel();
  }
  const block = buffer.split("\n\n", 1)[0];
  const event = block.split("\n").find((line) => line.startsWith("event: "))?.slice(7);
  const data = block.split("\n").find((line) => line.startsWith("data: "))?.slice(6);
  assert.equal(event, "session.snapshot");
  return JSON.parse(data);
}

async function waitForSessionSnapshot(baseUrl, sessionId, cookie, predicate) {
  const deadline = Date.now() + 5000;
  while (Date.now() < deadline) {
    const snapshot = await readSessionSnapshot(baseUrl, sessionId, cookie);
    if (predicate(snapshot)) return snapshot;
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
  throw new Error("session_snapshot_timeout");
}

function acceptedMessageCount(snapshot) {
  return (snapshot.events ?? []).filter((event) => event.event === "message.accepted").length;
}
