// src/lib/spec/audit/actions.test.ts — integration, gated on DB_TESTS=1, *_test DB only.
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { __clearTestCaller, __setTestCaller } from "@/lib/sites/access";
import type { SiteSnapshot } from "./snapshot";

mock.module("server-only", () => ({}));
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));
// Stub the session primitive; "demo-org" matches the seeded org of the fixtures
// below so the REAL tenancy guard runs against the test DB.
mock.module("@/lib/active-org", () => ({
  getActiveOrgId: async () => "demo-org",
  getActiveUserId: async () => "seed-owner",
}));
// Avoid real network: the snapshot module returns a canned "good" snapshot.
function cannedSnapshot(): SiteSnapshot {
  const html = `<!doctype html><html lang="fr"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>Accueil</title><meta name="description" content="d">
<link rel="canonical" href="https://x.test/">
<meta property="og:title" content="x"><meta property="og:image" content="i"></head><body>x</body></html>`;
  return {
    baseUrl: "https://x.test",
    home: {
      ok: true,
      status: 200,
      headers: {
        "strict-transport-security": "max-age=1",
        "content-security-policy": "default-src 'self'",
        "x-content-type-options": "nosniff",
        "referrer-policy": "no-referrer",
        "x-frame-options": "DENY",
      },
      html,
      finalUrl: "https://x.test/",
    },
    robotsTxt: {
      ok: true,
      status: 200,
      text: "User-agent: *\nSitemap: https://x.test/sitemap.xml",
    },
    sitemapXml: { ok: true, status: 200, text: "<urlset></urlset>" },
    securityTxt: { ok: true, status: 200, text: "Contact: a@b.c" },
    llmsTxt: { ok: true, status: 200, text: "# x" },
  };
}
mock.module("./snapshot", () => ({
  fetchSiteSnapshot: async () => cannedSnapshot(),
  normalizeBaseUrl: (u: string) => u,
}));

const RUN_DB = process.env.DB_TESTS === "1";

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

  beforeEach(async () => {
    __setTestCaller({ userId: "seed-owner", orgId: "demo-org", role: "owner" });
    ({ db } = await import("@/lib/db/client"));
    schema = await import("@/lib/db/domain-schema");
    actions = await import("./actions");
    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: "site-a",
        orgId: "demo-org",
        ownerUserId: "seed-owner",
        name: "Site A",
        url: "https://x.test",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
      {
        id: "site-foreign",
        orgId: "other-org",
        ownerUserId: "seed-owner",
        name: "Foreign",
        url: "https://y.test",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
    ]);
  });

  afterEach(() => __clearTestCaller());

  test("runSiteAuditAction persists a row and returns the result", async () => {
    const res = await actions.runSiteAuditAction("site-a");
    expect(res.score).toBeGreaterThan(0);
    expect(res.results.length).toBeGreaterThan(20);
    const rows = await db.select().from(schema.siteAudits);
    expect(rows).toHaveLength(1);
    expect(rows[0].siteId).toBe("site-a");
    expect(rows[0].score).toBe(res.score);
    expect(Array.isArray(rows[0].results)).toBe(true);
    // Compteurs dénormalisés cohérents avec `results` (na/manual exclus).
    const expectedPass = res.results.filter((r) => r.status === "pass").length;
    const expectedFail = res.results.filter((r) => r.status === "fail").length;
    const expectedWarn = res.results.filter((r) => r.status === "warn").length;
    expect(rows[0].passCount).toBe(expectedPass);
    expect(rows[0].failCount).toBe(expectedFail);
    expect(rows[0].warnCount).toBe(expectedWarn);
  });

  test("getOrgAuditOverviewAction — une requête, sans results, compteurs corrects", async () => {
    await actions.runSiteAuditAction("site-a");
    const overview = await actions.getOrgAuditOverviewAction();
    // Seul le site de l'org active (demo-org) apparaît — site-foreign est
    // dans un autre org et n'est jamais retourné par getProjectsForOrg.
    expect(overview).toHaveLength(1);
    const [row] = overview;
    expect(row.siteId).toBe("site-a");
    expect(row.score).not.toBeNull();
    expect(row.ranAt).not.toBeNull();
    expect(row.pass + row.fail + row.warn).toBeGreaterThan(0);
    const [stored] = await db.select().from(schema.siteAudits);
    expect(row.pass).toBe(stored.passCount);
    expect(row.fail).toBe(stored.failCount);
    expect(row.warn).toBe(stored.warnCount);
  });

  test("getOrgAuditOverviewAction — site jamais audité → score null, compteurs à 0", async () => {
    const overview = await actions.getOrgAuditOverviewAction();
    expect(overview).toHaveLength(1);
    const [row] = overview;
    expect(row.siteId).toBe("site-a");
    expect(row.score).toBeNull();
    expect(row.ranAt).toBeNull();
    expect(row.pass).toBe(0);
    expect(row.fail).toBe(0);
    expect(row.warn).toBe(0);
  });

  test("re-run remplace l'audit — une seule ligne par site, ranAt rafraîchi", async () => {
    await actions.runSiteAuditAction("site-a");
    const [first] = await db.select().from(schema.siteAudits);
    await new Promise((r) => setTimeout(r, 5));
    const second = await actions.runSiteAuditAction("site-a");
    const latest = await actions.getLatestAuditAction("site-a");
    expect(latest).not.toBeNull();
    expect(latest?.score).toBe(second.score);
    const rows = await db.select().from(schema.siteAudits);
    expect(rows).toHaveLength(1);
    expect(rows[0].id).toBe(first.id); // la ligne est mise à jour, pas réinsérée
    expect(rows[0].ranAt.getTime()).toBeGreaterThan(first.ranAt.getTime());
  });

  test("getLatestAuditAction returns null when no audit ran", async () => {
    const latest = await actions.getLatestAuditAction("site-a");
    expect(latest).toBeNull();
  });

  test("cross-org site throws (tenancy guard)", async () => {
    await expect(actions.runSiteAuditAction("site-foreign")).rejects.toThrow();
    await expect(
      actions.getLatestAuditAction("site-foreign"),
    ).rejects.toThrow();
  });

  test("site without URL throws a clear error", async () => {
    await db
      .update(schema.sites)
      .set({ url: null })
      .where((await import("drizzle-orm")).eq(schema.sites.id, "site-a"));
    await expect(actions.runSiteAuditAction("site-a")).rejects.toThrow("URL");
  });
});
