// src/lib/spec/audit/ssrf-guard.test.ts — pure, hermetic (mocked resolver).
import { describe, expect, test } from "bun:test";
import {
  assertPublicUrl,
  BlockedUrlError,
  type DnsResolver,
  isBlockedHostname,
} from "./ssrf-guard";

/** Deterministic resolver: maps hostname -> fixed addresses (no real DNS). */
function fakeResolver(map: Record<string, string[]>): DnsResolver {
  return async (hostname: string) => {
    const addrs = map[hostname];
    if (!addrs) throw new Error(`ENOTFOUND ${hostname}`);
    return addrs;
  };
}

describe("isBlockedHostname", () => {
  const blocked = [
    "localhost",
    "app.localhost",
    "127.0.0.1",
    "127.1.2.3",
    "0.0.0.0",
    "10.0.0.5",
    "172.16.0.1",
    "172.31.255.255",
    "192.168.1.1",
    "169.254.169.254", // cloud metadata
    "100.64.0.1", // CGNAT
    "::1", // IPv6 loopback
    "::", // IPv6 unspecified
    "fc00::1", // ULA
    "fd12:3456::1", // ULA
    "fe80::1", // link-local
    "::ffff:127.0.0.1", // IPv4-mapped loopback
    "::ffff:169.254.169.254", // IPv4-mapped metadata
    "redis", // bare non-FQDN single label
    "metadata", // bare non-FQDN single label
  ];
  for (const host of blocked) {
    test(`blocks ${host}`, () => {
      expect(isBlockedHostname(host)).toBe(true);
    });
  }

  const allowed = [
    "example.com",
    "www.elevaseo.com",
    "8.8.8.8", // public DNS
    "1.1.1.1",
    "172.15.0.1", // just outside 172.16/12
    "172.32.0.1", // just outside 172.16/12
    "100.63.255.255", // just outside CGNAT 100.64/10
    "100.128.0.1", // just outside CGNAT 100.64/10
    "2606:4700:4700::1111", // Cloudflare public IPv6
    "::ffff:8.8.8.8", // IPv4-mapped public
  ];
  for (const host of allowed) {
    test(`allows ${host}`, () => {
      expect(isBlockedHostname(host)).toBe(false);
    });
  }
});

describe("assertPublicUrl", () => {
  test("allows a public host (resolver -> public IP)", async () => {
    const resolver = fakeResolver({ "example.com": ["93.184.216.34"] });
    await expect(
      assertPublicUrl("https://example.com/robots.txt", resolver),
    ).resolves.toBeUndefined();
  });

  test("rejects a literal loopback IP before any DNS", async () => {
    let called = false;
    const resolver: DnsResolver = async () => {
      called = true;
      return [];
    };
    await expect(
      assertPublicUrl("http://127.0.0.1:6379/", resolver),
    ).rejects.toBeInstanceOf(BlockedUrlError);
    expect(called).toBe(false);
  });

  test("rejects cloud metadata IP literal", async () => {
    await expect(
      assertPublicUrl("http://169.254.169.254/latest/meta-data/"),
    ).rejects.toBeInstanceOf(BlockedUrlError);
  });

  test("rejects localhost literal", async () => {
    await expect(
      assertPublicUrl("http://localhost:3000/"),
    ).rejects.toBeInstanceOf(BlockedUrlError);
  });

  test("rejects a host that RESOLVES to a private IP (rebinding)", async () => {
    const resolver = fakeResolver({ "evil.example.com": ["10.0.0.5"] });
    await expect(
      assertPublicUrl("https://evil.example.com/", resolver),
    ).rejects.toBeInstanceOf(BlockedUrlError);
  });

  test("rejects if ANY resolved address is private (mixed set)", async () => {
    const resolver = fakeResolver({
      "mixed.example.com": ["93.184.216.34", "127.0.0.1"],
    });
    await expect(
      assertPublicUrl("https://mixed.example.com/", resolver),
    ).rejects.toBeInstanceOf(BlockedUrlError);
  });

  test("rejects non-http(s) schemes", async () => {
    await expect(assertPublicUrl("file:///etc/passwd")).rejects.toBeInstanceOf(
      BlockedUrlError,
    );
    await expect(
      assertPublicUrl("gopher://example.com/"),
    ).rejects.toBeInstanceOf(BlockedUrlError);
  });

  test("rejects an unparseable URL", async () => {
    await expect(assertPublicUrl("not a url")).rejects.toBeInstanceOf(
      BlockedUrlError,
    );
  });

  test("rejects when DNS resolution fails (nothing to audit)", async () => {
    const resolver = fakeResolver({}); // every lookup throws
    await expect(
      assertPublicUrl("https://does-not-resolve.example.com/", resolver),
    ).rejects.toBeInstanceOf(BlockedUrlError);
  });

  test("rejects when DNS returns no addresses", async () => {
    const resolver: DnsResolver = async () => [];
    await expect(
      assertPublicUrl("https://empty.example.com/", resolver),
    ).rejects.toBeInstanceOf(BlockedUrlError);
  });
});
