import { describe, expect, it } from "vitest";
import {
  buildCspDirectives,
  buildCspHeader,
  buildServeJson,
  injectCspMetaIntoHtml,
  serializeCsp,
  toOrigin
} from "./vite-plugin-csp";

describe("toOrigin", () => {
  it("renvoie null pour une entrée vide ou non string", () => {
    expect(toOrigin(null)).toBeNull();
    expect(toOrigin(undefined)).toBeNull();
    expect(toOrigin("")).toBeNull();
    expect(toOrigin("   ")).toBeNull();
  });

  it("extrait le protocole + host (+ port)", () => {
    expect(toOrigin("https://analytics.example.com/")).toBe("https://analytics.example.com");
    expect(toOrigin("https://analytics.example.com")).toBe("https://analytics.example.com");
    expect(toOrigin("https://analytics.example.com:8443/foo/bar?x=1")).toBe(
      "https://analytics.example.com:8443"
    );
  });

  it("refuse les protocoles non http(s)", () => {
    expect(toOrigin("file:///etc/passwd")).toBeNull();
    expect(toOrigin("javascript:alert(1)")).toBeNull();
    expect(toOrigin("data:text/plain,foo")).toBeNull();
  });

  it("renvoie null pour une URL invalide", () => {
    expect(toOrigin("not a url")).toBeNull();
    expect(toOrigin("http://")).toBeNull();
  });
});

describe("serializeCsp", () => {
  it("joint les directives par `; ` et quote chaque source", () => {
    const out = serializeCsp([
      { name: "default-src", sources: ["self"] },
      { name: "script-src", sources: ["self", "https://x.test"] }
    ]);
    expect(out).toBe("default-src 'self'; script-src 'self' 'https://x.test'");
  });

  it("préserve les tokens spéciaux (unsafe-inline, etc.)", () => {
    const out = serializeCsp([{ name: "script-src", sources: ["self", "unsafe-inline", "unsafe-eval"] }]);
    expect(out).toBe("script-src 'self' 'unsafe-inline' 'unsafe-eval'");
  });
});

describe("buildCspDirectives", () => {
  it("en prod sans Matomo : aucune origine externe, pas de unsafe-eval, frame-ancestors none", () => {
    const dirs = buildCspDirectives("production", null);
    const find = (name: string) => dirs.find((d) => d.name === name);
    expect(find("default-src")?.sources).toEqual(["self"]);
    expect(find("script-src")?.sources).toEqual(["self"]);
    // `style-src` inclut `unsafe-inline` même en prod (styles inline Solid).
    expect(find("style-src")?.sources).toEqual(["self", "unsafe-inline"]);
    expect(find("img-src")?.sources).toEqual(["self"]);
    expect(find("font-src")?.sources).toEqual(["self"]);
    expect(find("connect-src")?.sources).toEqual(["self"]);
    expect(find("media-src")?.sources).toEqual(["self"]);
    expect(find("object-src")?.sources).toEqual(["none"]);
    expect(find("base-uri")?.sources).toEqual(["self"]);
    expect(find("form-action")?.sources).toEqual(["self"]);
    expect(find("frame-ancestors")?.sources).toEqual(["none"]);
    expect(find("frame-src")?.sources).toEqual(["none"]);
  });

  it("en prod avec Matomo : autorise l'origine sur script/connect/img", () => {
    const dirs = buildCspDirectives("production", "https://analytics.example.com");
    expect(dirs.find((d) => d.name === "script-src")?.sources).toEqual([
      "self",
      "https://analytics.example.com"
    ]);
    expect(dirs.find((d) => d.name === "connect-src")?.sources).toEqual([
      "self",
      "https://analytics.example.com"
    ]);
    expect(dirs.find((d) => d.name === "img-src")?.sources).toEqual([
      "self",
      "https://analytics.example.com"
    ]);
    // Pas de Matomo sur font-src / default-src.
    expect(dirs.find((d) => d.name === "font-src")?.sources).toEqual(["self"]);
  });

  it("en dev : autorise unsafe-eval (HMR), ws:/wss:, et unsafe-inline sur style-src", () => {
    const dirs = buildCspDirectives("development", null);
    expect(dirs.find((d) => d.name === "script-src")?.sources).toEqual([
      "self",
      "unsafe-inline",
      "unsafe-eval"
    ]);
    expect(dirs.find((d) => d.name === "style-src")?.sources).toEqual(["self", "unsafe-inline"]);
    expect(dirs.find((d) => d.name === "connect-src")?.sources).toEqual(["self", "ws:", "wss:"]);
    // frame-ancestors reste 'none' en dev aussi.
    expect(dirs.find((d) => d.name === "frame-ancestors")?.sources).toEqual(["none"]);
  });

  it("en dev avec Matomo : cumule unsafe-* et l'origine Matomo", () => {
    const dirs = buildCspDirectives("development", "https://analytics.example.com");
    expect(dirs.find((d) => d.name === "script-src")?.sources).toEqual([
      "self",
      "unsafe-inline",
      "unsafe-eval",
      "https://analytics.example.com"
    ]);
  });
});

describe("buildCspHeader", () => {
  it("inclut toutes les directives attendues en prod", () => {
    const csp = buildCspHeader("production", null);
    expect(csp).toContain("default-src 'self'");
    expect(csp).toContain("frame-ancestors 'none'");
    expect(csp).toContain("object-src 'none'");
    // `unsafe-inline` reste sur `style-src` (styles inline Solid) — seul `unsafe-eval` est
    // strictement exclu en prod (HMR Vite ne sert qu'en dev).
    expect(csp).not.toContain("unsafe-eval");
  });
});

describe("injectCspMetaIntoHtml", () => {
  const sample = `<!doctype html>
<html lang="fr">
<head>
    <meta charset="UTF-8" />
    <title>Test</title>
</head>
<body></body>
</html>
`;

  it("insère la balise CSP juste après le charset", () => {
    const out = injectCspMetaIntoHtml(sample, "default-src 'self'");
    expect(out).toMatch(
      /<meta\s+charset="UTF-8"\s*\/>\s*<meta\s+http-equiv="Content-Security-Policy"\s+content="default-src 'self'">/
    );
  });

  it("insère après <head> si pas de charset", () => {
    const html = "<html><head><title>X</title></head><body/></html>";
    const out = injectCspMetaIntoHtml(html, "default-src 'self'");
    expect(out).toMatch(/<head>\s*<meta\s+http-equiv="Content-Security-Policy"/);
  });

  it("quote correctement la valeur CSP", () => {
    const out = injectCspMetaIntoHtml(sample, "default-src 'self'; script-src 'self'");
    expect(out).toContain(`content="default-src 'self'; script-src 'self'"`);
  });
});

describe("buildServeJson", () => {
  it("déclare une règle HTML avec CSP + en-têtes statiques, et une règle catch-all sans CSP", () => {
    const sj = buildServeJson(null);
    expect(sj.headers).toHaveLength(2);

    const htmlRule = sj.headers.find((r) => r.source === "**/*.html");
    expect(htmlRule).toBeDefined();
    const htmlKeys = htmlRule!.headers.map((h) => h.key);
    expect(htmlKeys).toEqual(
      expect.arrayContaining([
        "Content-Security-Policy",
        "X-Content-Type-Options",
        "Referrer-Policy",
        "X-Frame-Options",
        "Permissions-Policy"
      ])
    );
    const cspEntry = htmlRule!.headers.find((h) => h.key === "Content-Security-Policy");
    expect(cspEntry?.value).not.toContain("unsafe-eval");
    expect(cspEntry?.value).toContain("frame-ancestors 'none'");

    const allRule = sj.headers.find((r) => r.source === "**/*");
    expect(allRule).toBeDefined();
    const allKeys = allRule!.headers.map((h) => h.key);
    expect(allKeys).not.toContain("Content-Security-Policy");
    expect(allKeys).toEqual(
      expect.arrayContaining([
        "X-Content-Type-Options",
        "Referrer-Policy",
        "X-Frame-Options",
        "Permissions-Policy"
      ])
    );
  });

  it("la CSP s'adapte à l'origine Matomo fournie", () => {
    const sj = buildServeJson("https://analytics.example.com");
    const cspEntry = sj.headers
      .find((r) => r.source === "**/*.html")!
      .headers.find((h) => h.key === "Content-Security-Policy");
    expect(cspEntry?.value).toContain("https://analytics.example.com");
  });

  it("la Permissions-Policy désactive les capacités sensibles", () => {
    const sj = buildServeJson(null);
    const allHeaders = sj.headers.flatMap((r) => r.headers);
    const pp = allHeaders.find((h) => h.key === "Permissions-Policy");
    expect(pp?.value).toContain("camera=()");
    expect(pp?.value).toContain("microphone=()");
    expect(pp?.value).toContain("geolocation=()");
  });
});
