import { describe, expect, test } from "bun:test";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import {
  IncidentOptionsError,
  loadIncidentOptions,
  materializeIncidentCapability,
  serializeIncidentOptions,
  validateIncidentDispatchSelection,
  validateIncidentType,
  type AdapterRegistry,
  type IncidentCapability,
  type LoadedIncidentOptions,
} from "./dispatch-options";

const root = join(import.meta.dir, "../../..");
const wrapperModels = [
  "gpt-5.6-sol-low", "gpt-5.6-sol-medium", "gpt-5.6-sol-high",
  "gpt-5.6-terra-low", "gpt-5.6-terra-medium", "gpt-5.6-terra-high",
  "gpt-5.6-luna-low", "gpt-5.6-luna-medium", "gpt-5.6-luna-high",
];
const adapters = JSON.stringify({
  version: "adapters/v1",
  adapters: [{ id: "codex", wrapper: "wrappers/codex.sh", models: wrapperModels }],
});
const models = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].map((id) => ({
  id,
  efforts: ["low", "medium", "high"].map((effort) => ({ id: effort, wrapperModel: `${id}-${effort}` })),
}));
const capability = {
  cli: "codex",
  label: "Codex",
  adapter: "codex",
  wrapperContract: "incident-wrapper/v1",
  models,
  accountMode: { kind: "profile", provider: "codex" },
  timeoutSeconds: 3600,
  permissionModes: ["safe"],
};
const manifest = JSON.stringify({ version: "incident-dispatch-capabilities/v1", capabilities: [capability] });
const taxonomy = JSON.stringify([{
  id: "resource-overload",
  title: "Resource overload",
  absorbs: [],
  skill: "od-overload",
  doctrine: "measure",
  keywords: ["cpu"],
}]);

function loader(
  overrides: Record<string, string> = {},
  accounts: () => Promise<Array<{ slug: string; label: string }>> = async () => [{ slug: "work", label: "Work" }],
) {
  return loadIncidentOptions({
    read: async (path) => overrides[path] ?? ({ adapters, manifest, taxonomy }[path] ?? ""),
    adaptersPath: "adapters",
    manifestPath: "manifest",
    taxonomyPath: "taxonomy",
    accounts,
  });
}

function withCapability(mutate: (cap: IncidentCapability) => unknown) {
  const next = structuredClone(capability) as IncidentCapability;
  return JSON.stringify({ version: "incident-dispatch-capabilities/v1", capabilities: [mutate(next)] });
}

describe("incident dispatch options", () => {
  test("joins deployed authority without incident history and keeps wrapper mappings private", async () => {
    const options = await loader();
    expect(serializeIncidentOptions(options)).toEqual({
      types: [{ id: "resource-overload", title: "Resource overload", keywords: ["cpu"] }],
      clis: [{
        id: "codex",
        label: "Codex",
        models: [
          { id: "gpt-5.6-sol", efforts: ["low", "medium", "high"] },
          { id: "gpt-5.6-terra", efforts: ["low", "medium", "high"] },
          { id: "gpt-5.6-luna", efforts: ["low", "medium", "high"] },
        ],
        accounts: [{ slug: "work", label: "Work", ready: true, fixed: false }],
        permissionModes: ["safe"],
      }],
    });
    expect(validateIncidentDispatchSelection({
      cli: "codex", model: "gpt-5.6-sol", reasoningEffort: "low", account: "work", unsafe: false,
    }, options).wrapperModel).toBe("gpt-5.6-sol-low");
    expect(() => validateIncidentDispatchSelection({
      cli: "codex", model: "gpt-5.6-sol", reasoningEffort: "low", account: "work", unsafe: true,
    }, options)).toThrow("invalid incident dispatch");
  });

  test("unsafe mode requires server-side authority and fails closed without it", async () => {
    const safeOnly = await loader();
    expect(() => validateIncidentDispatchSelection({
      cli: "codex", model: "gpt-5.6-sol", reasoningEffort: "low", account: "work", unsafe: true,
    }, safeOnly)).toThrow("invalid incident dispatch");

    const unsafeManifest = withCapability((cap) => {
      cap.permissionModes = ["safe", "unsafe"];
      return cap;
    });
    await expect(loader({ manifest: unsafeManifest })).rejects.toMatchObject({ kind: "assets" });

    const adapterRegistry = JSON.parse(adapters) as AdapterRegistry;
    const unsafeCapability = {
      ...structuredClone(capability),
      permissionModes: ["safe", "unsafe"],
    } as IncidentCapability;
    const materialized = await materializeIncidentCapability(unsafeCapability, adapterRegistry, async () => [{ slug: "work", label: "Work" }]);
    const options: LoadedIncidentOptions = {
      types: [{ id: "resource-overload", title: "Resource overload", keywords: ["cpu"] }],
      clis: [materialized.cli],
      sourcePaths: [],
      internal: materialized.internal,
    };
    expect(materialized.cli.permissionModes).toEqual(["safe", "unsafe"]);
    expect(validateIncidentDispatchSelection({
      cli: "codex", model: "gpt-5.6-sol", reasoningEffort: "low", account: "work", unsafe: true,
    }, options)).toMatchObject({ wrapperModel: "gpt-5.6-sol-low", permissionMode: "unsafe" });
    expect(validateIncidentDispatchSelection({
      cli: "codex", model: "gpt-5.6-sol", reasoningEffort: "low", account: "work", unsafe: false,
    }, options)).toMatchObject({ wrapperModel: "gpt-5.6-sol-low", permissionMode: "safe" });
  });

  test("profile joins via loader; fixed-account join stays on materialize seam", async () => {
    const profile = await loader();
    expect(profile.clis[0]?.accounts).toEqual([{ slug: "work", label: "Work", ready: true, fixed: false }]);

    const adapterRegistry = JSON.parse(adapters) as AdapterRegistry;
    const fixedCapability = {
      ...structuredClone(capability),
      accountMode: { kind: "fixed", account: "billing", label: "Billing" },
    } as IncidentCapability;
    const fixed = await materializeIncidentCapability(fixedCapability, adapterRegistry, async () => {
      throw new Error("profile registry must not be read for fixed mode");
    });
    expect(fixed.cli.accounts).toEqual([{ slug: "billing", label: "Billing", ready: true, fixed: true }]);
    expect(fixed.internal.get(["codex", "gpt-5.6-sol", "medium", "billing", "safe"].join("\0"))).toEqual({
      wrapperModel: "gpt-5.6-sol-medium", permissionMode: "safe", fixed: true,
      wrapper: "wrappers/codex.sh", timeoutSeconds: 3600,
    });
  });

  test("loadIncidentOptions rejects tampered deployed v1 that switches codex to fixed", async () => {
    await expect(loader({
      manifest: withCapability((cap) => {
        cap.accountMode = { kind: "fixed", account: "billing", label: "Billing" } as typeof cap.accountMode;
        return cap;
      }),
    })).rejects.toMatchObject({ kind: "assets", detail: expect.stringContaining("v1 account mode mismatch") });
  });

  test("validates the shipped Codex capability against the adapter registry", async () => {
    const options = await loadIncidentOptions({
      adaptersPath: join(root, "modules/harness/presets/adapters.json"),
      manifestPath: join(root, "modules/workstation/claude/incidents/dispatch-capabilities.json"),
      taxonomyPath: join(root, "modules/workstation/claude/incidents/taxonomy.json"),
      read: (path) => readFile(path, "utf8"),
      accounts: async () => [{ slug: "live-work", label: "Live work" }],
    });
    expect(serializeIncidentOptions(options).clis).toEqual([{
      id: "codex",
      label: "Codex",
      models: [
        { id: "gpt-5.6-sol", efforts: ["low", "medium", "high"] },
        { id: "gpt-5.6-terra", efforts: ["low", "medium", "high"] },
        { id: "gpt-5.6-luna", efforts: ["low", "medium", "high"] },
      ],
      accounts: [{ slug: "live-work", label: "Live work", ready: true, fixed: false }],
      permissionModes: ["safe"],
    }]);
  });

  test("trust-boundary matrix fails closed for unknown version/shape and scoped duplicates", async () => {
    const cases: Array<[string, Record<string, string>]> = [
      ["unknown version", { manifest: JSON.stringify({ version: "incident-dispatch-capabilities/v0", capabilities: [capability] }) }],
      ["unknown shape", { manifest: "{}" }],
      ["duplicate CLI", { manifest: JSON.stringify({ version: "incident-dispatch-capabilities/v1", capabilities: [capability, { ...capability, label: "Other" }] }) }],
      ["duplicate adapter id", { adapters: JSON.stringify({ version: "adapters/v1", adapters: [
        { id: "codex", wrapper: "wrappers/codex.sh", models: wrapperModels },
        { id: "codex", wrapper: "wrappers/codex.sh", models: wrapperModels },
      ] }) }],
      ["duplicate adapter model", { adapters: JSON.stringify({ version: "adapters/v1", adapters: [
        { id: "codex", wrapper: "wrappers/codex.sh", models: [...wrapperModels, "gpt-5.6-sol-low"] },
      ] }) }],
      ["duplicate model id", { manifest: withCapability((cap) => {
        cap.models = [cap.models[0]!, { ...cap.models[0]!, id: "gpt-5.6-sol" }];
        return cap;
      }) }],
      ["duplicate effort id", { manifest: withCapability((cap) => {
        cap.models[0]!.efforts = [
          { id: "low", wrapperModel: "gpt-5.6-sol-low" },
          { id: "low", wrapperModel: "gpt-5.6-sol-medium" },
          { id: "high", wrapperModel: "gpt-5.6-sol-high" },
        ];
        return cap;
      }) }],
      ["duplicate wrapper model", { manifest: withCapability((cap) => {
        cap.models[1]!.efforts[0]!.wrapperModel = "gpt-5.6-sol-low";
        return cap;
      }) }],
      ["duplicate permission mode", { manifest: withCapability((cap) => {
        cap.permissionModes = ["safe", "safe"];
        return cap;
      }) }],
      ["unsafe permission mode", { manifest: JSON.stringify({
        version: "incident-dispatch-capabilities/v1",
        capabilities: [{ ...capability, permissionModes: ["safe", "unsafe"] }],
      }) }],
      ["duplicate account slug", {}],
    ];

    for (const [label, overrides] of cases) {
      const load = label === "duplicate account slug"
        ? loader(overrides, async () => [{ slug: "work", label: "A" }, { slug: "work", label: "B" }])
        : loader(overrides);
      await expect(load).rejects.toBeInstanceOf(IncidentOptionsError);
      await expect(load).rejects.toMatchObject({ kind: label === "duplicate account slug" ? "accounts" : "assets" });
    }
  });

  test("trust-boundary matrix fails closed for adapter/wrapper/model emptiness and registry faults", async () => {
    await expect(loader({ adapters: JSON.stringify({ version: "adapters/v1", adapters: [
      { id: "other", wrapper: "wrappers/other.sh", models: wrapperModels },
    ] }) })).rejects.toMatchObject({ kind: "assets" });

    await expect(loader({ adapters: JSON.stringify({ version: "adapters/v1", adapters: [
      { id: "codex", wrapper: "wrappers/other.sh", models: wrapperModels },
    ] }) })).rejects.toMatchObject({ kind: "assets" });

    await expect(loader({ adapters: JSON.stringify({ version: "adapters/v1", adapters: [
      { id: "codex", wrapper: "wrappers/codex.sh", models: wrapperModels.filter((model) => model !== "gpt-5.6-luna-high") },
    ] }) })).rejects.toMatchObject({ kind: "assets" });

    await expect(loader({ manifest: withCapability((cap) => {
      cap.models[0]!.efforts[0]!.wrapperModel = "not-registered";
      return cap;
    }) })).rejects.toMatchObject({ kind: "assets" });

    await expect(materializeIncidentCapability({
      ...structuredClone(capability),
      accountMode: { kind: "fixed", account: " ", label: "Billing" },
    } as IncidentCapability, JSON.parse(adapters) as AdapterRegistry, async () => [])).rejects.toThrow(/empty fixed account/);

    await expect(loader({ manifest: withCapability((cap) => {
      cap.permissionModes = [];
      return cap;
    }) })).rejects.toMatchObject({ kind: "assets" });

    await expect(loader({ taxonomy: "{" })).rejects.toMatchObject({ kind: "assets" });
    await expect(loader({ taxonomy: '{"types":null}' })).rejects.toMatchObject({ kind: "assets" });
    await expect(loader({}, async () => { throw new Error("registry unreadable"); })).rejects.toMatchObject({ kind: "accounts" });
    await expect(loader({}, async () => [])).rejects.toMatchObject({ kind: "accounts" });
  });

  test("rejects self-declared non-v1 capabilities and unregistered selections", async () => {
    const parsed = JSON.parse(manifest) as { capabilities: Array<typeof capability> };
    parsed.capabilities.push({ ...parsed.capabilities[0]!, cli: "claude", adapter: "claude", label: "Claude" });
    await expect(loader({ manifest: JSON.stringify(parsed) })).rejects.toMatchObject({ kind: "assets" });

    const options = await loader();
    expect(() => validateIncidentDispatchSelection({
      cli: "codex", model: "unknown", reasoningEffort: "low", account: "work", unsafe: false,
    }, options)).toThrow("invalid incident dispatch");
  });

  test("validateIncidentType rejects unknown taxonomy ids and allows known ids", async () => {
    const options = await loader();
    expect(() => validateIncidentType("resource-overload", options)).not.toThrow();
    expect(() => validateIncidentType(undefined, options)).not.toThrow();
    expect(() => validateIncidentType("no-such-type", options)).toThrow("invalid incident type");
  });
});
