/**
 * Tests for the unified automation registry (Phase 0).
 *
 * The headline guarantee: the catalog can NEVER claim a runnable capability
 * that has no handler behind it. Every capability whose runRef points at one of
 * the two existing browser registries must resolve to a real entry there.
 *
 * Run: node --test chrome-extension/automation-registry.test.js
 *
 * The two source registries are browser IIFEs that attach to `window`; the repo
 * is type:module, so we seed a fake window on globalThis, then dynamically import
 * the three files in order (dynamic import so the window is set before they run).
 */

import test from "node:test";
import assert from "node:assert/strict";
import { pathToFileURL } from "node:url";
import path from "node:path";

const dir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"));
const load = (f) => import(pathToFileURL(path.join(dir, f)).href);

// Fake browser global so the IIFE registries can attach to it, BEFORE they run.
globalThis.window = {};
await load("linkedin-action-registry.js");
await load("platform-actions.js");
await load("automation-registry.js");
const AR = globalThis.window.AUTOMATION_REGISTRY;

const ACTION_IDS = new Set(AR.ACTIONS.map((a) => a.id));
const SOURCE_IDS = new Set(AR.SOURCES.map((s) => s.id));
const MODE_IDS = new Set(AR.ACCOUNT_MODES.map((m) => m.id));
const PLATFORM_IDS = new Set(AR.PLATFORMS.map((p) => p.id));
const STATE_VALUES = new Set(Object.values(AR.STATE));
const ROUTE_VALUES = new Set(Object.values(AR.EXEC_ROUTES));
const CAP_IDS = new Set(AR.CAPABILITIES.map((c) => c.id));

test("the registries loaded", () => {
  assert.ok(Array.isArray(window.LI_ACTION_REGISTRY) && window.LI_ACTION_REGISTRY.length > 0);
  assert.ok(Array.isArray(window.PLATFORM_ACTIONS) && window.PLATFORM_ACTIONS.length > 0);
  assert.ok(AR && AR.CAPABILITIES.length > 0);
});

test("capability ids are unique", () => {
  assert.equal(CAP_IDS.size, AR.CAPABILITIES.length, "duplicate capability id");
});

test("every capability uses valid taxonomy values", () => {
  for (const c of AR.CAPABILITIES) {
    assert.ok(PLATFORM_IDS.has(c.platform), `${c.id}: bad platform ${c.platform}`);
    assert.ok(ACTION_IDS.has(c.action), `${c.id}: bad action ${c.action}`);
    assert.ok(STATE_VALUES.has(c.state), `${c.id}: bad state ${c.state}`);
    assert.ok(ROUTE_VALUES.has(c.route), `${c.id}: bad route ${c.route}`);
    assert.ok(Array.isArray(c.sources) && c.sources.length > 0, `${c.id}: needs sources`);
    for (const s of c.sources) assert.ok(SOURCE_IDS.has(s), `${c.id}: bad source ${s}`);
    assert.ok(
      Array.isArray(c.accountModes) && c.accountModes.length > 0,
      `${c.id}: needs accountModes`,
    );
    for (const m of c.accountModes) assert.ok(MODE_IDS.has(m), `${c.id}: bad accountMode ${m}`);
    assert.ok(c.title && c.description, `${c.id}: needs title + description`);
  }
});

test("NO DRIFT: every browser-registry runRef resolves to a real handler", () => {
  for (const c of AR.CAPABILITIES) {
    if (!c.runRef) continue; // not-yet-runnable (partial/coming-soon) is allowed
    const resolved = AR.resolveRunnable(c, {
      liRegistry: window.LI_ACTION_REGISTRY,
      platformActions: window.PLATFORM_ACTIONS,
    });
    assert.ok(resolved, `${c.id}: runRef present but resolveRunnable returned null`);
    if (c.runRef.kind === "li_registry" || c.runRef.kind === "platform_action") {
      assert.ok(
        resolved.entry && !resolved.missing,
        `${c.id}: runRef ${c.runRef.kind}:${c.runRef.id} does not resolve to an existing registry entry`,
      );
    } else if (c.runRef.kind === "engine") {
      assert.ok(c.runRef.preset, `${c.id}: engine runRef needs a preset name`);
    } else {
      assert.fail(`${c.id}: unknown runRef kind ${c.runRef.kind}`);
    }
  }
});

test("PARTIAL/coming-soon capabilities are honestly not-yet-runnable", () => {
  for (const c of AR.CAPABILITIES) {
    if (c.state === AR.STATE.SOON) {
      assert.equal(c.runRef, null, `${c.id}: coming_soon must not have a runRef`);
    }
  }
});

test("every use-case step references an existing capability", () => {
  for (const uc of AR.USE_CASES) {
    assert.ok(uc.id && uc.title && uc.goal, `use-case ${uc.id}: needs id/title/goal`);
    assert.ok(Array.isArray(uc.steps) && uc.steps.length > 0, `use-case ${uc.id}: needs steps`);
    for (const step of uc.steps) {
      assert.ok(
        CAP_IDS.has(step.capability),
        `use-case ${uc.id}: unknown capability ${step.capability}`,
      );
    }
    for (const s of uc.sourcesHint || []) {
      assert.ok(SOURCE_IDS.has(s), `use-case ${uc.id}: bad sourcesHint ${s}`);
    }
  }
});

test("getCapabilities filters AND-combine", () => {
  const liBuilt = AR.getCapabilities({ platform: "linkedin", state: AR.STATE.BUILT });
  assert.ok(liBuilt.length > 0);
  assert.ok(liBuilt.every((c) => c.platform === "linkedin" && c.state === AR.STATE.BUILT));

  const crmCaps = AR.getCapabilities({ source: "crm_list" });
  assert.ok(crmCaps.length > 0);
  assert.ok(crmCaps.every((c) => c.sources.includes("crm_list")));

  // Telegram reactions shipped (Part F.2): tg_react is now BUILT + engine-backed.
  const react = AR.getCapability("tg_react");
  assert.ok(react && react.state === AR.STATE.BUILT, "tg_react should be built");
  assert.equal(react.runRef && react.runRef.kind, "engine");
  assert.equal(react.runRef && react.runRef.preset, "group_reactions");
});
