import { apiToolPath, TOOLS } from "@snapotter/shared";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getRegisteredToolIds, getToolConfig } from "../../apps/api/src/routes/tool-factory.js";
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";

/**
 * Drift guards between the shared TOOLS catalog and the API.
 *
 * Two intentional asymmetries exist and are pinned exactly:
 *  - REGISTRY_EXEMPT: tools whose contract does not fit the single-buffer
 *    process fn (multi-file, ZIP/JSON output, no-input generators, custom AI
 *    routes). They expose an HTTP route but are not in the pipeline/batch
 *    registry. If one of these gains registry support, remove it here.
 *  - LEGACY_ALIASES: extra registered toolIds kept for backwards-compatible
 *    URLs (consolidated into adjust-colors).
 */
const REGISTRY_EXEMPT = new Set([
  "auto-subtitles",
  "background-replace",
  "barcode-generate",
  "barcode-read",
  "blur-background",
  "bulk-rename",
  "collage",
  "color-palette",
  "compare",
  "compose",
  "erase-object",
  "favicon",
  "find-duplicates",
  "html-to-image",
  "image-to-base64",
  "image-to-pdf",
  "info",
  "ocr",
  "ocr-pdf",
  "pdf-to-image",
  "qr-generate",
  "stitch",
  "svg-to-raster",
  "transcribe-audio",
  "watermark-image",
]);

const LEGACY_ALIASES = new Set([
  "brightness-contrast",
  "saturation",
  "color-channels",
  "color-effects",
]);

describe("tool route drift", () => {
  let testApp: TestApp;
  let adminToken: string;

  beforeAll(async () => {
    testApp = await buildTestApp();
    adminToken = await loginAsAdmin(testApp.app);
  }, 30_000);

  afterAll(async () => {
    await testApp.cleanup();
  }, 10_000);

  it("every non-exempt TOOLS entry has a registered process fn", () => {
    const registered = new Set(getRegisteredToolIds());
    const missing = TOOLS.filter((t) => !REGISTRY_EXEMPT.has(t.id) && !registered.has(t.id)).map(
      (t) => t.id,
    );
    expect(missing, `tools not registered on the API: ${missing.join(", ")}`).toEqual([]);
  });

  it("registry-exempt list is not stale", () => {
    const registered = new Set(getRegisteredToolIds());
    for (const id of REGISTRY_EXEMPT) {
      expect(
        registered.has(id),
        `"${id}" is in REGISTRY_EXEMPT but IS registered now; remove it from the exempt list`,
      ).toBe(false);
    }
  });

  it("every registered tool exposes a settings schema and process fn", () => {
    for (const id of getRegisteredToolIds()) {
      const config = getToolConfig(id);
      expect(config?.settingsSchema, `tool "${id}" has no settings schema`).toBeTruthy();
      expect(typeof config?.process, `tool "${id}" has no process fn`).toBe("function");
    }
  });

  it("no orphan registrations (registered but missing from TOOLS, excluding legacy aliases)", () => {
    const ids = new Set(TOOLS.map((t) => t.id));
    for (const id of getRegisteredToolIds()) {
      if (LEGACY_ALIASES.has(id)) continue;
      expect(ids.has(id), `registered tool "${id}" has no TOOLS definition`).toBe(true);
    }
  });

  it("every TOOLS entry answers on POST /api/v1/tools/:section/:toolId (no dead routes)", async () => {
    for (const tool of TOOLS) {
      const res = await testApp.app.inject({
        method: "POST",
        url: apiToolPath(tool.id),
        headers: { authorization: `Bearer ${adminToken}`, "content-type": "application/json" },
        payload: {},
      });
      expect(res.statusCode, `tool "${tool.id}" has no live POST route (got 404)`).not.toBe(404);
    }
  }, 60_000);
});
