// src/lib/mcp/tools.test.ts — MCP tool functions (postgres).
// Integration tests — require Postgres + the test DB migrated. Skipped unless DB_TESTS=1.
// Verifies org-scoping: list_sites returns only the org's sites, get_site_audit
// rejects a cross-org siteId. Static-spec tools (list_spec_topics, get_spec_topic)
// are exercised without DB.
import { beforeEach, describe, expect, mock, test } from "bun:test";
import { eq } from "drizzle-orm";
import type { AuditResultRow } from "@/lib/spec/audit/run";

mock.module("server-only", () => ({}));

const RUN_DB = process.env.DB_TESTS === "1";
const ORG = "demo-org";
const OTHER = "other-org";

describe.skipIf(!RUN_DB)("mcp tools (postgres)", () => {
  let db: typeof import("@/lib/db/client").db;
  let schema: typeof import("@/lib/db/domain-schema");

  const RESULTS: AuditResultRow[] = [
    { itemId: "foundations/doctype", status: "pass", detail: "" },
    { itemId: "foundations/title", status: "fail", detail: "missing" },
    { itemId: "seo/robots-txt", status: "warn", detail: "weak" },
    { itemId: "foundations/html-lang", status: "manual", detail: "" },
  ];

  beforeEach(async () => {
    ({ db } = await import("@/lib/db/client"));
    schema = await import("@/lib/db/domain-schema");
    const { assertTestDb } = await import("@/lib/db/assert-test-db");
    await assertTestDb();
    await db.delete(schema.siteAudits);
    await db.delete(schema.sites);
    await db.insert(schema.sites).values([
      {
        id: "mcp-site-a",
        orgId: ORG,
        ownerUserId: "seed-owner",
        name: "Site A",
        url: "https://a.test",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
      {
        id: "mcp-site-foreign",
        orgId: OTHER,
        ownerUserId: "seed-owner",
        name: "Foreign",
        url: "https://y.test",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
    ]);
    await db.insert(schema.siteAudits).values({
      id: "mcp-audit-a",
      siteId: "mcp-site-a",
      score: 50,
      results: RESULTS,
      // Normally computed by persistSiteAudit — set explicitly here since the
      // fixture inserts directly (RESULTS has 1 pass / 1 fail / 1 warn / 1
      // manual, manual excluded like the score).
      passCount: 1,
      failCount: 1,
      warnCount: 1,
    });
  });

  test("listSites returns only the org's sites", async () => {
    const { listSites } = await import("./tools");
    const sites = await listSites(ORG);
    expect(sites.map((s) => s.id)).toEqual(["mcp-site-a"]);
    expect(sites[0].url).toBe("https://a.test");
    // The foreign org's site must not leak.
    expect(sites.some((s) => s.id === "mcp-site-foreign")).toBe(false);
  });

  test("auditOverview summarizes the latest audit per org site", async () => {
    const { auditOverview } = await import("./tools");
    const rows = await auditOverview(ORG);
    expect(rows).toHaveLength(1);
    const [row] = rows;
    expect(row.siteId).toBe("mcp-site-a");
    expect(row.score).toBe(50);
    expect(row.pass).toBe(1);
    expect(row.fail).toBe(1);
    expect(row.warn).toBe(1);
    expect(row.ranAt).not.toBeNull();
  });

  test("getSiteAudit returns the detailed audit for an in-org site", async () => {
    const { getSiteAudit } = await import("./tools");
    const audit = await getSiteAudit(ORG, "mcp-site-a");
    expect(audit).not.toBeNull();
    expect(audit).not.toBe("never-audited");
    if (audit && audit !== "never-audited") {
      expect(audit.score).toBe(50);
      expect(audit.results).toHaveLength(4);
    }
  });

  test("getSiteAudit rejects a cross-org siteId (returns null)", async () => {
    const { getSiteAudit } = await import("./tools");
    const audit = await getSiteAudit(ORG, "mcp-site-foreign");
    expect(audit).toBeNull();
  });

  test("getSiteAudit returns 'never-audited' for an in-org site with no audit", async () => {
    await db.insert(schema.sites).values({
      id: "mcp-site-b",
      orgId: ORG,
      ownerUserId: "seed-owner",
      name: "Site B",
      url: "https://b.test",
      primaryLanguage: "fr",
      languages: [],
      targetCountries: ["FR"],
    });
    const { getSiteAudit } = await import("./tools");
    expect(await getSiteAudit(ORG, "mcp-site-b")).toBe("never-audited");
  });

  describe("runSiteAuditForOrg", () => {
    // Un fetch canné qui renvoie une petite page HTML valide pour toutes les URLs.
    const fakeFetch = (async () =>
      new Response(
        "<html><head><title>t</title></head><body>ok</body></html>",
        {
          status: 200,
          headers: { "content-type": "text/html" },
        },
      )) as unknown as typeof fetch;

    test("cross-org → not-found", async () => {
      const tools = await import("./tools");
      const out = await tools.runSiteAuditForOrg(
        OTHER,
        "mcp-site-a",
        fakeFetch,
      );
      expect(out.kind).toBe("not-found");
    });

    test("site sans URL → no-url", async () => {
      await db
        .update(schema.sites)
        .set({ url: null })
        .where(eq(schema.sites.id, "mcp-site-a"));
      const tools = await import("./tools");
      const out = await tools.runSiteAuditForOrg(ORG, "mcp-site-a", fakeFetch);
      expect(out.kind).toBe("no-url");
    });

    test("run → audit persisté (une ligne), detail retourné", async () => {
      await db.delete(schema.siteAudits);
      const tools = await import("./tools");
      const out = await tools.runSiteAuditForOrg(ORG, "mcp-site-a", fakeFetch);
      expect(out.kind).toBe("ran");
      if (out.kind !== "ran") throw new Error("unreachable");
      expect(out.detail.siteId).toBe("mcp-site-a");
      expect(out.detail.results.length).toBeGreaterThan(100);
      const rows = await db.select().from(schema.siteAudits);
      expect(rows).toHaveLength(1);
    });

    test("2e appel < 60 s → cooldown, résultat existant, AUCUN fetch", async () => {
      await db.delete(schema.siteAudits);
      const tools = await import("./tools");
      await tools.runSiteAuditForOrg(ORG, "mcp-site-a", fakeFetch);
      let called = 0;
      const spyFetch = (async (...a: Parameters<typeof fetch>) => {
        called++;
        return fakeFetch(...a);
      }) as unknown as typeof fetch;
      const out = await tools.runSiteAuditForOrg(ORG, "mcp-site-a", spyFetch);
      expect(out.kind).toBe("cooldown");
      expect(called).toBe(0);
      const rows = await db.select().from(schema.siteAudits);
      expect(rows).toHaveLength(1);
    });
  });
});

describe("mcp spec tools (static)", () => {
  test("listSpecTopics returns all topics, filterable by category", async () => {
    const { listSpecTopics } = await import("./tools");
    const all = listSpecTopics();
    expect(all.length).toBeGreaterThan(100);
    expect(all[0]).toHaveProperty("id");
    expect(all[0]).toHaveProperty("summary");

    const foundations = listSpecTopics("foundations");
    expect(foundations.length).toBeGreaterThan(0);
    expect(foundations.every((t) => t.category === "foundations")).toBe(true);
    expect(foundations.length).toBeLessThan(all.length);

    expect(listSpecTopics("does-not-exist")).toEqual([]);
  });

  test("getSpecTopic returns the markdown body for a known topic, null otherwise", async () => {
    const { getSpecTopic } = await import("./tools");
    const page = getSpecTopic("foundations", "doctype");
    expect(page).not.toBeNull();
    if (page) {
      expect(typeof page.body).toBe("string");
      expect(page.body.length).toBeGreaterThan(0);
    }
    expect(getSpecTopic("foundations", "nope-not-real")).toBeNull();
    // Path-injection-shaped input is rejected by getSpecPage's safe-segment guard.
    expect(getSpecTopic("..", "etc-passwd")).toBeNull();
  });
});
