// src/lib/spec/audit/snapshot.test.ts — inject a fake fetcher; no real network.
import { describe, expect, test } from "bun:test";
import { fetchSiteSnapshot, normalizeBaseUrl } from "./snapshot";
import type { DnsResolver } from "./ssrf-guard";

/** Resolver that maps every host to a fixed public IP (keeps tests hermetic). */
const publicResolver: DnsResolver = async () => ["93.184.216.34"];

function fakeFetcher(
  routes: Record<
    string,
    { status?: number; body?: string; headers?: Record<string, string> }
  >,
): typeof fetch {
  return (async (input: RequestInfo | URL) => {
    const url = typeof input === "string" ? input : input.toString();
    const route = routes[url];
    if (!route) {
      return new Response("not found", { status: 404 });
    }
    const status = route.status ?? 200;
    return new Response(route.body ?? "", {
      status,
      headers: route.headers ?? {},
    });
  }) as unknown as typeof fetch;
}

describe("normalizeBaseUrl", () => {
  test("adds https:// when no scheme", () => {
    expect(normalizeBaseUrl("example.com")).toBe("https://example.com");
  });
  test("keeps existing scheme and trims trailing slash", () => {
    expect(normalizeBaseUrl("http://example.com/")).toBe("http://example.com");
  });
});

describe("fetchSiteSnapshot", () => {
  test("collects home + well-known resources", async () => {
    const base = "https://example.com";
    const fetcher = fakeFetcher({
      [base]: {
        body: "<!doctype html><html lang='fr'></html>",
        headers: { "x-content-type-options": "nosniff" },
      },
      [`${base}/robots.txt`]: { body: "User-agent: *" },
      [`${base}/sitemap.xml`]: { body: "<urlset></urlset>" },
      [`${base}/.well-known/security.txt`]: { body: "Contact: a@b.c" },
      [`${base}/llms.txt`]: { body: "# Site" },
    });

    const snap = await fetchSiteSnapshot(
      "example.com",
      fetcher,
      publicResolver,
    );

    expect(snap.baseUrl).toBe(base);
    expect(snap.home.ok).toBe(true);
    expect(snap.home.status).toBe(200);
    expect(snap.home.headers["x-content-type-options"]).toBe("nosniff");
    expect(snap.home.html).toContain("doctype");
    expect(snap.robotsTxt.ok).toBe(true);
    expect(snap.sitemapXml.text).toContain("urlset");
    expect(snap.securityTxt.text).toContain("Contact");
    expect(snap.llmsTxt.text).toContain("# Site");
  });

  test("lowercases header keys", async () => {
    const base = "https://example.com";
    const fetcher = fakeFetcher({
      [base]: {
        body: "<html></html>",
        headers: { "Strict-Transport-Security": "max-age=1" },
      },
    });
    const snap = await fetchSiteSnapshot(base, fetcher, publicResolver);
    expect(snap.home.headers["strict-transport-security"]).toBe("max-age=1");
  });

  test("a missing resource yields ok:false (404), not a throw", async () => {
    const base = "https://example.com";
    const fetcher = fakeFetcher({ [base]: { body: "<html></html>" } });
    const snap = await fetchSiteSnapshot(base, fetcher, publicResolver);
    expect(snap.home.ok).toBe(true);
    expect(snap.robotsTxt.ok).toBe(false);
    expect(snap.robotsTxt.status).toBe(404);
  });

  test("a throwing/timeout fetch yields ok:false, never throws", async () => {
    const throwing = (async () => {
      throw new Error("network down");
    }) as unknown as typeof fetch;
    const snap = await fetchSiteSnapshot(
      "https://example.com",
      throwing,
      publicResolver,
    );
    expect(snap.home.ok).toBe(false);
    expect(snap.home.status).toBe(0);
    expect(snap.home.html).toBe("");
    expect(snap.robotsTxt.ok).toBe(false);
  });

  test("an internal URL yields empty results without any network fetch", async () => {
    // Site created as an SSRF probe: internal loopback target. The guard must
    // block it at the fetch boundary (before fetchImpl is ever called), so the
    // audit sees the exact same empty snapshot as a dead site.
    let fetchCalls = 0;
    const spyFetcher = (async () => {
      fetchCalls++;
      return new Response("SECRET INTERNAL DATA", { status: 200 });
    }) as unknown as typeof fetch;
    // Resolver would even resolve it to a public IP — irrelevant, the literal
    // internal host must be rejected outright.
    const snap = await fetchSiteSnapshot(
      "http://127.0.0.1:6379",
      spyFetcher,
      publicResolver,
    );
    expect(fetchCalls).toBe(0);
    expect(snap.home.ok).toBe(false);
    expect(snap.home.status).toBe(0);
    expect(snap.home.html).toBe("");
    expect(snap.robotsTxt.ok).toBe(false);
    expect(snap.sitemapXml.ok).toBe(false);
    expect(snap.securityTxt.ok).toBe(false);
    expect(snap.llmsTxt.ok).toBe(false);
  });

  test("a public host that resolves to a private IP is blocked (rebinding)", async () => {
    let fetchCalls = 0;
    const spyFetcher = (async () => {
      fetchCalls++;
      return new Response("<html></html>", { status: 200 });
    }) as unknown as typeof fetch;
    const rebindResolver: DnsResolver = async () => ["169.254.169.254"];
    const snap = await fetchSiteSnapshot(
      "https://evil.example.com",
      spyFetcher,
      rebindResolver,
    );
    expect(fetchCalls).toBe(0);
    expect(snap.home.ok).toBe(false);
  });
});
