import assert from "node:assert/strict";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { parseDevicePairingPayload } from "../../../packages/shared/src/device-pairing.js";
import { createDeviceAuthStore } from "./device-auth-store.js";

test("pairing payload carries only the connection origin and one-time attempt data", async () => {
  await withStore(async ({ store }) => {
    const pairing = await store.createPairing({ origin: "https://pc.example.ts.net" });
    const parsed = parseDevicePairingPayload(pairing.qrPayload);

    assert.equal(parsed.origin, "https://pc.example.ts.net");
    assert.equal(parsed.attemptId, pairing.attemptId);
    assert.equal(parsed.expiresAt, pairing.expiresAt);
    assert.match(pairing.code, /^[0-9A-HJKMNP-TV-Z]{4}(?:-[0-9A-HJKMNP-TV-Z]{4}){2}$/);
    assert.equal("code" in parsed, false);
    assert.equal("credential" in parsed, false);
  });
});

test("pairing is single-use and persists no raw pairing or device secrets", async () => {
  await withStore(async ({ directory, store }) => {
    const pairing = await store.createPairing({ origin: "https://pc.example.ts.net" });
    const parsed = parseDevicePairingPayload(pairing.qrPayload);
    const enrolled = await store.completePairing({
      attemptId: pairing.attemptId,
      qrSecret: parsed.secret,
      label: "Operator phone"
    });

    assert.equal(enrolled.device.label, "Operator phone");
    assert.equal(enrolled.device.current, true);
    assert.equal((await store.authenticate(enrolled.credential)).id, enrolled.device.id);
    await assert.rejects(
      store.completePairing({ attemptId: pairing.attemptId, qrSecret: parsed.secret }),
      /pairing_unavailable/
    );

    const persisted = await readFile(join(directory, "security", "device-auth.json"), "utf8");
    assert.equal(persisted.includes(pairing.code.replaceAll("-", "")), false);
    assert.equal(persisted.includes(parsed.secret), false);
    assert.equal(persisted.includes(enrolled.credential), false);
  });
});

test("expired pairing and repeated guesses fail closed", async () => {
  await withStore(async ({ store, advance }) => {
    const expired = await store.createPairing({ origin: "https://pc.example.ts.net" });
    advance(5 * 60 * 1000 + 1);
    await assert.rejects(
      store.completePairing({ attemptId: expired.attemptId, code: expired.code }),
      /pairing_expired/
    );

    const limited = await store.createPairing({ origin: "https://pc.example.ts.net" });
    for (let attempt = 1; attempt <= 4; attempt += 1) {
      await assert.rejects(
        store.completePairing({ attemptId: limited.attemptId, code: "0000-0000-0000" }),
        /pairing_invalid/
      );
    }
    await assert.rejects(
      store.completePairing({ attemptId: limited.attemptId, code: "0000-0000-0000" }),
      /pairing_locked/
    );
    await assert.rejects(
      store.completePairing({ attemptId: limited.attemptId, code: limited.code }),
      /pairing_unavailable/
    );
  });
});

test("multiple device credentials rotate, revoke, expire, and survive backend restart independently", async () => {
  await withStore(async ({ directory, store, advance, now }) => {
    const first = await enrollWithCode(store, "First phone");
    const second = await enrollWithCode(store, "Tablet");
    const restarted = createDeviceAuthStore(directory, { now });

    assert.equal((await restarted.authenticate(first.credential)).id, first.device.id);
    assert.equal((await restarted.authenticate(second.credential)).id, second.device.id);

    const rotated = await restarted.rotateDevice(first.device.id);
    await assert.rejects(restarted.authenticate(first.credential), /authentication_invalid/);
    assert.equal((await restarted.authenticate(rotated.credential)).id, first.device.id);

    await restarted.revokeDevice(first.device.id);
    await assert.rejects(restarted.authenticate(rotated.credential), /authentication_invalid/);
    assert.equal((await restarted.authenticate(second.credential)).id, second.device.id);

    advance(181 * 24 * 60 * 60 * 1000);
    await assert.rejects(restarted.authenticate(second.credential), /authentication_expired/);
  });
});

test("pairing payload rejects insecure, credentialed, and malformed connection addresses", async () => {
  await withStore(async ({ store }) => {
    await assert.rejects(store.createPairing({ origin: "http://public.example.test" }), /invalid_pairing_payload/);
    await assert.rejects(store.createPairing({ origin: "https://user:secret@pc.example.test" }), /invalid_pairing_payload/);
    assert.throws(() => parseDevicePairingPayload("https://pc.example.test/pair?secret=value"), /invalid_pairing_payload/);
  });
});

async function enrollWithCode(store, label) {
  const pairing = await store.createPairing({ origin: "https://pc.example.ts.net" });
  return store.completePairing({ attemptId: pairing.attemptId, code: pairing.code, label });
}

async function withStore(run) {
  const directory = await mkdtemp(join(tmpdir(), "crm-device-auth-"));
  let currentTime = Date.parse("2026-07-17T00:00:00.000Z");
  const now = () => new Date(currentTime);
  const store = createDeviceAuthStore(directory, { now });
  try {
    await run({
      directory,
      store,
      now,
      advance(milliseconds) { currentTime += milliseconds; }
    });
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
}
