import { describe, expect, test } from "bun:test";
import {
  checkPublicTarget,
  refuseAddress,
  refuseV4,
  refuseV6,
  resolveAuditTarget,
  settleWwwRedirect,
} from "./public-target";

/** A resolver that answers with exactly what the test names, without a network. */
function answering(...addresses: string[]) {
  return async () => addresses.map((address) => ({ address }));
}

const failing = async () => {
  throw new Error("NXDOMAIN");
};

describe("refuseV4", () => {
  test("lets the public internet through", () => {
    for (const address of ["1.1.1.1", "8.8.8.8", "93.184.216.34", "172.32.0.1", "100.128.0.1"]) {
      expect(refuseV4(address)).toBeNull();
    }
  });

  test("refuses every range that is not the public internet", () => {
    expect(refuseV4("10.0.0.1")).toBe("a private network");
    expect(refuseV4("172.16.0.1")).toBe("a private network");
    expect(refuseV4("172.31.255.254")).toBe("a private network");
    expect(refuseV4("192.168.1.1")).toBe("a private network");
    expect(refuseV4("127.0.0.1")).toBe("the loopback");
    expect(refuseV4("0.0.0.0")).toBe("this network");
    expect(refuseV4("100.64.0.1")).toBe("carrier-grade NAT space");
    expect(refuseV4("224.0.0.1")).toBe("multicast");
    expect(refuseV4("255.255.255.255")).toBe("reserved space");
  });

  test("names link-local, because that is where the credentials live", () => {
    expect(refuseV4("169.254.169.254")).toBe("link-local space, where cloud metadata lives");
  });

  test("is not fooled by things that are not octets", () => {
    // `Number` would happily read these; the regex must not.
    expect(refuseV4("127.0.0.01e1")).toBeNull();
    expect(refuseV4("127.0.0")).toBeNull();
    expect(refuseV4("127.0.0.256")).toBeNull();
    expect(refuseV4("127.0.0.")).toBeNull();
  });
});

describe("refuseV6", () => {
  test("lets a public address through", () => {
    expect(refuseV6("2606:4700:4700::1111")).toBeNull();
    expect(refuseV6("2a00:1450:4007:80f::200e")).toBeNull();
  });

  test("refuses loopback, link-local, unique-local and friends", () => {
    expect(refuseV6("::1")).toBe("the loopback");
    expect(refuseV6("::")).toBe("the unspecified address");
    expect(refuseV6("fe80::1")).toBe("link-local space");
    expect(refuseV6("febf::1")).toBe("link-local space");
    expect(refuseV6("fc00::1")).toBe("unique-local space");
    expect(refuseV6("fd12:3456::1")).toBe("unique-local space");
    expect(refuseV6("ff02::1")).toBe("multicast");
    expect(refuseV6("2001:db8::1")).toBe("documentation space");
  });

  test("strips the zone index and the brackets", () => {
    expect(refuseV6("[fe80::1%eth0]")).toBe("link-local space");
  });

  test("unmasks an IPv4 address wearing an IPv6 hat", () => {
    expect(refuseV6("::ffff:127.0.0.1")).toBe("the loopback");
    expect(refuseV6("::ffff:169.254.169.254")).toBe(
      "link-local space, where cloud metadata lives",
    );
    expect(refuseV6("::ffff:1.1.1.1")).toBeNull();
  });
});

describe("refuseAddress", () => {
  test("picks the family by the colon", () => {
    expect(refuseAddress("10.0.0.1")).toBe("a private network");
    expect(refuseAddress("::1")).toBe("the loopback");
  });
});

describe("checkPublicTarget", () => {
  test("accepts a name that resolves entirely onto the public internet", async () => {
    expect(await checkPublicTarget("example.com", answering("93.184.216.34"))).toEqual({
      ok: true,
    });
  });

  test("refuses a name where a single answer is off the public internet", async () => {
    // The rebinding shape: one honest answer, one that is the whole point.
    const verdict = await checkPublicTarget("evil.test", answering("93.184.216.34", "127.0.0.1"));
    expect(verdict).toEqual({ ok: false, reason: "evil.test resolves into the loopback" });
  });

  test("refuses localhost by name, whatever /etc/hosts says today", async () => {
    for (const host of ["localhost", "LOCALHOST", "app.localhost"]) {
      const verdict = await checkPublicTarget(host, answering("93.184.216.34"));
      expect(verdict.ok).toBe(false);
    }
  });

  test("refuses a .local name", async () => {
    const verdict = await checkPublicTarget("printer.local", answering("93.184.216.34"));
    expect(verdict).toEqual({
      ok: false,
      reason: "a .local name is not on the public internet",
    });
  });

  test("refuses a name with no dot as an intranet short name", async () => {
    const verdict = await checkPublicTarget("intranet", answering("93.184.216.34"));
    expect(verdict).toEqual({
      ok: false,
      reason: "that looks like an intranet name, not a public domain",
    });
  });

  test("refuses a private literal without any lookup at all", async () => {
    const never = async () => {
      throw new Error("the resolver must not be reached for a literal");
    };
    expect(await checkPublicTarget("192.168.1.1", never)).toEqual({
      ok: false,
      reason: "that address is in a private network",
    });
    expect(await checkPublicTarget("[::1]", never)).toEqual({
      ok: false,
      reason: "that address is in the loopback",
    });
  });

  test("accepts a public literal without a lookup", async () => {
    const never = async () => {
      throw new Error("the resolver must not be reached for a literal");
    };
    expect(await checkPublicTarget("1.1.1.1", never)).toEqual({ ok: true });
  });

  test("refuses a name that does not resolve, and one that resolves to nothing", async () => {
    expect(await checkPublicTarget("nope.example", failing)).toEqual({
      ok: false,
      reason: "nope.example does not resolve",
    });
    expect(await checkPublicTarget("nope.example", answering())).toEqual({
      ok: false,
      reason: "nope.example does not resolve",
    });
  });

  test("refuses an empty host", async () => {
    expect(await checkPublicTarget("   ", answering("1.1.1.1"))).toEqual({
      ok: false,
      reason: "the host is empty",
    });
  });
});

describe("resolveAuditTarget", () => {
  const ok = answering("93.184.216.34");

  test("gives a bare domain the https it meant", async () => {
    expect(await resolveAuditTarget("example.com", ok)).toEqual({
      ok: true,
      origin: "https://example.com",
    });
  });

  test("keeps an explicit scheme and an explicit port", async () => {
    expect(await resolveAuditTarget("http://example.com:8443", ok)).toEqual({
      ok: true,
      origin: "http://example.com:8443",
    });
  });

  test("drops the path, because the audit is origin-wide", async () => {
    expect(await resolveAuditTarget("https://example.com/blog/post?a=1#x", ok)).toEqual({
      ok: true,
      origin: "https://example.com",
    });
  });

  test("refuses a scheme the binary would refuse anyway", async () => {
    const verdict = await resolveAuditTarget("ftp://example.com", ok);
    expect(verdict).toEqual({ ok: false, reason: "ftp is not a scheme we audit" });
  });

  test("refuses credentials in the URL", async () => {
    const verdict = await resolveAuditTarget("https://user:pass@example.com", ok);
    expect(verdict).toEqual({ ok: false, reason: "drop the credentials from the URL" });
  });

  test("refuses an empty entry and an unreadable one", async () => {
    expect(await resolveAuditTarget("  ", ok)).toEqual({ ok: false, reason: "type a domain first" });
    expect((await resolveAuditTarget("https://", ok)).ok).toBe(false);
  });

  test("carries the host verdict through, reason and all", async () => {
    const verdict = await resolveAuditTarget("http://169.254.169.254/latest/meta-data/", ok);
    expect(verdict).toEqual({
      ok: false,
      reason: "that address is in link-local space, where cloud metadata lives",
    });
  });
});

/** A server that answers one status and one `Location`, without a network. */
function answeringWith(status: number, location?: string): typeof fetch {
  return (async () =>
    new Response(null, {
      status,
      headers: location === undefined ? {} : { location },
    })) as unknown as typeof fetch;
}

/** A server that bounces between the two names for ever. */
const pingPong: typeof fetch = (async (input: string | URL | Request) => {
  const here = new URL(typeof input === "string" ? input : input.toString());
  const there = here.hostname.startsWith("www.")
    ? here.hostname.slice(4)
    : `www.${here.hostname}`;
  return new Response(null, { status: 308, headers: { location: `https://${there}/` } });
}) as unknown as typeof fetch;

describe("settling on the name that serves the site", () => {
  const public4 = answering("93.184.216.34");

  test("crosses from the apex to www", async () => {
    const settled = await settleWwwRedirect(
      "https://example.com",
      answeringWith(308, "https://www.example.com/"),
      public4,
    );
    expect(settled).toBe("https://www.example.com");
  });

  test("and from www to the apex", async () => {
    const settled = await settleWwwRedirect(
      "https://www.example.com",
      answeringWith(301, "https://example.com/"),
      public4,
    );
    expect(settled).toBe("https://example.com");
  });

  test("leaves a site that does not redirect alone", async () => {
    expect(await settleWwwRedirect("https://example.com", answeringWith(200), public4)).toBe(
      "https://example.com",
    );
  });

  // The whole safety argument: a hop anywhere but the twin is not inspected
  // and refused, it is never taken.
  test("refuses to follow a redirect to another host", async () => {
    expect(
      await settleWwwRedirect(
        "https://example.com",
        answeringWith(302, "https://evil.test/"),
        public4,
      ),
    ).toBe("https://example.com");
  });

  test("refuses to follow one to the metadata endpoint", async () => {
    expect(
      await settleWwwRedirect(
        "https://example.com",
        answeringWith(302, "http://169.254.169.254/latest/meta-data/"),
        public4,
      ),
    ).toBe("https://example.com");
  });

  test("refuses a twin that resolves somewhere private", async () => {
    expect(
      await settleWwwRedirect(
        "https://example.com",
        answeringWith(308, "https://www.example.com/"),
        answering("10.0.0.7"),
      ),
    ).toBe("https://example.com");
  });

  test("refuses a downgrade to http on the same host", async () => {
    expect(
      await settleWwwRedirect(
        "https://example.com",
        answeringWith(308, "http://example.com/"),
        public4,
      ),
    ).toBe("https://example.com");
  });

  test("stops on a site that bounces between both names", async () => {
    const settled = await settleWwwRedirect("https://example.com", pingPong, public4);
    expect(settled).toBe("https://example.com");
  });

  test("keeps the typed origin when the site does not answer", async () => {
    const refusing = (async () => {
      throw new Error("connection refused");
    }) as unknown as typeof fetch;
    expect(await settleWwwRedirect("https://example.com", refusing, public4)).toBe(
      "https://example.com",
    );
  });

  test("keeps it when the redirect carries no location", async () => {
    expect(await settleWwwRedirect("https://example.com", answeringWith(308), public4)).toBe(
      "https://example.com",
    );
  });
});
