import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const here = dirname(fileURLToPath(import.meta.url));
const read = (f) => readFileSync(join(here, f), "utf8");

// The Circuit's origin is duplicated across FOUR places that cannot share a
// constant: manifest.json is JSON (no imports), background.js is a classic MV3
// service worker (importScripts is unreliable here — see background.js:84 and
// :306, which is why this codebase inlines shared code instead), and
// content-circuit.js / dashboard.js run in two more isolated contexts. The house
// rule for that situation is already settled twice over — dashboard-routes.drift.test.js
// and narrative-mirror.test.js both keep the copies and add a drift test.
//
// That duplication already failed once, silently, for months. Before r94 all
// four said "https://thecircuit.vercel.app" while the app is served from
// "https://thecircuit-app.vercel.app" — one missing "-app". Consequences:
//   · the manifest never injected content-circuit.js on the real app, so the
//     whole quest/subscribe bridge was dead in production from the day it shipped;
//   · the SW's isCircuitSender() would have rejected the real app anyway;
//   · dashboard.js's three "open the Circuit" buttons sent users to a live but
//     unrelated 644-byte Create-React-App stub on a host we do not control.
//
// CRITICAL DESIGN NOTE: checking only that the four copies agree with EACH OTHER
// would have passed green on that bug — they agreed perfectly, on the wrong host.
// So the production origin is additionally pinned to landing/vercel.json, which
// is where the deployed redirect actually sends users. That is the one copy in
// this repo that cannot be wrong without the product visibly breaking.

const WRONG_HOST = "https://thecircuit.vercel.app";

/**
 * The full URL users are actually redirected to, read from the deployed config.
 *
 * Pinned to the canonical `/circuit` door specifically rather than to "every
 * Circuit redirect agrees". Those are different claims: the marketing site may
 * legitimately grow deep links (/circuit/quests → …/quests) without the
 * extension's "open the app" button being wrong. Requiring global agreement
 * would fail on that entirely reasonable change, and a guard that cries wolf is
 * a guard someone deletes.
 */
function deployedCircuitUrl() {
  const v = JSON.parse(readFileSync(join(here, "..", "landing", "vercel.json"), "utf8"));
  const redirects = v.redirects || [];
  const door = redirects.find((r) => r.source === "/circuit" && /thecircuit/i.test(r.destination));
  if (door) return door.destination;

  // Fallback: /circuit was renamed. Then the only safe reading is that every
  // Circuit redirect points at one place — if they disagree we cannot guess.
  const uniq = [
    ...new Set(
      redirects
        .map((r) => r.destination)
        .filter((d) => typeof d === "string" && /thecircuit/i.test(d)),
    ),
  ];
  assert.ok(
    uniq.length > 0,
    "landing/vercel.json has no Circuit redirect at all — where is the app served from?",
  );
  assert.equal(
    uniq.length,
    1,
    `landing/vercel.json has no "/circuit" redirect and its Circuit targets disagree (${uniq.join(", ")}) — point this test at the new canonical door`,
  );
  return uniq[0];
}

/** Just the origin of the above — what the allow-lists are compared against. */
function deployedCircuitOrigin() {
  return new URL(deployedCircuitUrl()).origin;
}

/** Origins in the manifest's content_scripts entry for content-circuit.js. */
function manifestCircuitOrigins() {
  const m = JSON.parse(read("manifest.json"));
  const entry = (m.content_scripts || []).find((cs) =>
    (cs.js || []).includes("content-circuit.js"),
  );
  assert.ok(entry, "manifest.json declares a content_scripts entry for content-circuit.js");
  // Match patterns are "<origin>/*"; compare on origin so a path-glob change
  // (e.g. "/home/*") does not read as an origin change.
  return new Set(entry.matches.map((p) => p.replace(/\/\*$/, "")));
}

/**
 * String literals inside a `new Set([...])` assigned to `name`.
 * Deliberately source-scraping rather than importing: background.js and
 * content-circuit.js are not ESM and cannot be imported here.
 */
function setLiteral(file, name) {
  const src = read(file);
  const at = src.indexOf(`const ${name} = new Set([`);
  assert.ok(at >= 0, `${file} declares "const ${name} = new Set([" (did it get renamed?)`);
  const close = src.indexOf("])", at);
  assert.ok(close > at, `${file}: end of ${name} literal located`);
  const found = [...src.slice(at, close).matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
  assert.ok(found.length > 0, `${file}: ${name} contains at least one origin`);
  return new Set(found);
}

/**
 * Source with the NEXUS_BUILD_NOTES* constants removed. Those are a changelog:
 * they legitimately quote old, wrong hostnames while describing the bug that was
 * fixed — including this one. Scanning them would make an accurate build note
 * fail the build, so the "stub host is gone" check must look at code only.
 */
function codeOnly(file) {
  return read(file).replace(/const NEXUS_BUILD_NOTES\w* =\s*"[\s\S]*?";/g, "");
}

test("production origin matches where landing/vercel.json actually sends users", () => {
  // The assertion that would have caught the original bug. Everything else in
  // this file only proves the copies are consistent; this proves they are RIGHT.
  const deployed = deployedCircuitOrigin();
  const bridge = setLiteral("content-circuit.js", "ALLOWED_ORIGINS");
  assert.ok(
    bridge.has(deployed),
    `landing/vercel.json sends users to ${deployed}, which content-circuit.js does not allow-list (has: ${[...bridge].join(", ")}). The bridge will not wire up on the app.`,
  );
});

test("manifest match, bridge allow-list and SW sender gate name the SAME origins", () => {
  const manifest = manifestCircuitOrigins();
  const bridge = setLiteral("content-circuit.js", "ALLOWED_ORIGINS");
  const worker = setLiteral("background.js", "CIRCUIT_ORIGINS");

  const sorted = (s) => [...s].sort();
  // Set equality in both directions. An origin in the manifest but not in the
  // bridge means the script injects and then silently returns; an origin in the
  // bridge but not the manifest means it never injects at all. Both are the bug
  // this file exists to catch, so neither direction may pass alone.
  assert.deepEqual(
    sorted(bridge),
    sorted(manifest),
    "content-circuit.js ALLOWED_ORIGINS must equal the manifest content_scripts match",
  );
  assert.deepEqual(
    sorted(worker),
    sorted(manifest),
    "background.js CIRCUIT_ORIGINS must equal the manifest content_scripts match",
  );
});

test("dashboard's Open-the-Circuit URL lands where every other entry point lands", () => {
  const m = read("dashboard.js").match(/const CIRCUIT_APP_URL = "([^"]+)"/);
  assert.ok(m, "dashboard.js declares CIRCUIT_APP_URL");
  const url = m[1];
  // Full-URL equality, not just origin. "/" on the Circuit is the public
  // marketing splash and the app is at /home, so an origin-only match would
  // happily send a paired operator to a signup page. landing/vercel.json is the
  // authority on where a Circuit link goes; the extension must agree with it.
  assert.equal(
    url,
    deployedCircuitUrl(),
    "CIRCUIT_APP_URL must match the destination landing/vercel.json redirects to",
  );
  // Its ORIGIN must also be one the bridge trusts, or the user arrives on a page
  // where the extension refuses to talk — the failure that reads as "the
  // extension is broken" rather than "the URL is wrong".
  assert.ok(
    setLiteral("content-circuit.js", "ALLOWED_ORIGINS").has(new URL(url).origin),
    `CIRCUIT_APP_URL origin (${new URL(url).origin}) must be in ALLOWED_ORIGINS`,
  );
});

test("the stub host we do not control is referenced nowhere in extension code", () => {
  // thecircuit.vercel.app is a live but unrelated Create-React-App deployment.
  // Allow-listing it would expose the consent-gated bridge to whoever owns it,
  // so this is a security assertion, not merely a correctness one.
  //
  // The wrong host is a strict PREFIX of the right one, so a plain substring
  // search matches both. Require a terminator to test for the wrong host alone.
  const bad = new RegExp(`${WRONG_HOST.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=["'/\\s,)]|$)`);
  for (const f of ["manifest.json", "content-circuit.js", "background.js", "dashboard.js"]) {
    assert.equal(bad.test(codeOnly(f)), false, `${f} still references ${WRONG_HOST}`);
  }
});

test("every allow-listed origin is a bare https origin with no trailing slash", () => {
  // A trailing slash silently breaks every comparison in this system:
  // window.location.origin never has one, so "https://x/" === origin is false.
  for (const o of setLiteral("content-circuit.js", "ALLOWED_ORIGINS")) {
    const u = new URL(o);
    assert.equal(u.pathname, "/", `${o} must be a bare origin (no path)`);
    assert.equal(o.endsWith("/"), false, `${o} must not end in a slash`);
    assert.equal(o, u.origin, `${o} must equal its own URL.origin`);
    if (u.hostname !== "localhost") {
      assert.equal(u.protocol, "https:", `${o} must be https outside localhost`);
    }
  }
});
