/**
 * The served-region allowlist, and everything it refuses.
 *
 * Two halves. The first is that a box answers for the *places it was configured
 * for* rather than for one origin, because the map has two cities six hundred
 * kilometres apart and the Bay Area's fog is not Los Angeles's weather.
 *
 * The second is the one with teeth: **a caller's coordinate must never reach an
 * upstream.** An endpoint that fetches any point on demand is an amplifier
 * aimed at somebody else's public-good API, with this deployment's contact
 * string on every request. So `?lat=&lng=` selects among configured points and
 * nothing else, and everything it cannot select is a 400. See the header of
 * `regions.ts`; the assertion that this refusal actually holds at the HTTP layer
 * is `weather.test.ts`.
 */

import assert from "node:assert/strict";
import { after, describe, it } from "node:test";
import { buildApp } from "../app.ts";
import { loadConfig } from "../config.ts";
import { loadRegions, resolveRegion, type RegionSet } from "../regions.ts";
import type { ErrorBody, FlightsBody, WeatherBody } from "../../../src/server/wire.ts";

function appWith(env: Record<string, string>) {
  const config = loadConfig(env);
  config.logLevel = "silent";
  return buildApp(config);
}

/** The two shipped cities, which is what an empty environment resolves to. */
function shipped(): RegionSet {
  return loadConfig({}).regions;
}

describe("the region set a box ends up with", () => {
  it("serves both shipped cities when handed nothing, San Francisco first", () => {
    const config = loadConfig({});
    assert.deepEqual(
      config.regions.map((region) => region.id),
      ["sf", "socal"],
    );
    assert.deepEqual(config.degraded, []);
    // The default region is still the origin a pre-existing env file named, so
    // a bare GET answers exactly as it did before regions existed.
    assert.equal(config.regions[0].lat, config.origin.lat);
  });

  it("promotes the shipped city an operator's origin falls inside", () => {
    const config = loadConfig({ TERA_ORIGIN_LAT: "34.05", TERA_ORIGIN_LNG: "-118.24" });
    assert.deepEqual(
      config.regions.map((region) => region.id),
      ["socal", "sf"],
    );
    assert.deepEqual(config.degraded, []);
  });

  it("adds an origin that is neither city, and makes it the default", () => {
    const config = loadConfig({ TERA_ORIGIN_LAT: "39.7392", TERA_ORIGIN_LNG: "-104.9903" });
    assert.deepEqual(
      config.regions.map((region) => region.id),
      ["origin", "sf", "socal"],
    );
    assert.equal(config.regions[0].lat, 39.7392);
  });

  it("takes TERA_REGIONS literally, in order, with an optional radius", () => {
    const config = loadConfig({
      TERA_REGIONS: "pdx:45.5152,-122.6784:80; sea:47.6062,-122.3321",
    });
    assert.deepEqual(config.regions, [
      { id: "pdx", lat: 45.5152, lng: -122.6784, radiusKm: 80 },
      { id: "sea", lat: 47.6062, lng: -122.3321, radiusKm: 120 },
    ]);
    assert.deepEqual(config.degraded, []);
  });

  it("drops a malformed entry with a sentence and keeps the good ones", () => {
    const config = loadConfig({ TERA_REGIONS: "pdx:45.5152,-122.6784; nowhere; sea:200,0" });
    assert.deepEqual(
      config.regions.map((region) => region.id),
      ["pdx"],
    );
    assert.equal(config.degraded.length, 2);
    assert.match(config.degraded[0] ?? "", /nowhere/);
    assert.match(config.degraded[1] ?? "", /not a place on Earth/);
  });

  it("falls back to the shipped cities when nothing in TERA_REGIONS parses", () => {
    const config = loadConfig({ TERA_REGIONS: "?????" });
    assert.deepEqual(
      config.regions.map((region) => region.id),
      ["sf", "socal"],
    );
    assert.ok(config.degraded.some((line) => line.includes("nothing in it parsed")));
  });

  it("keeps the first of two entries sharing an id", () => {
    const config = loadConfig({ TERA_REGIONS: "sf:37.7749,-122.4194; sf:0,0" });
    assert.equal(config.regions.length, 1);
    assert.ok(config.degraded.some((line) => line.includes("twice")));
  });

  it("never ends up with an empty set, whatever it was handed", () => {
    for (const spec of ["", " ", ";;;", "sf:", "@:1,2", "x".repeat(200)]) {
      const regions = loadRegions({
        spec,
        origin: { lat: 37.7749, lng: -122.4194 },
        originConfigured: false,
        degraded: [],
      });
      assert.ok(regions.length > 0, `"${spec}" produced no regions`);
    }
  });
});

describe("resolving a request to a region", () => {
  const regions = shipped();

  it("answers for the default when asked for nothing", () => {
    const resolved = resolveRegion(regions, {});
    assert.ok(resolved.ok && resolved.region.id === "sf");
  });

  it("answers for a city by id", () => {
    const resolved = resolveRegion(regions, { city: "socal" });
    assert.ok(resolved.ok && resolved.region.id === "socal");
  });

  it("snaps a nearby coordinate to the region that claims it", () => {
    // Berkeley, Pasadena: neither is a configured point, and both resolve to the
    // configured point that will actually be fetched.
    const berkeley = resolveRegion(regions, { lat: "37.8715", lng: "-122.2730" });
    assert.ok(berkeley.ok && berkeley.region.id === "sf");
    const pasadena = resolveRegion(regions, { lat: "34.1478", lng: "-118.1445" });
    assert.ok(pasadena.ok && pasadena.region.id === "socal");
  });

  it("refuses a coordinate this deployment has nothing to say about", () => {
    for (const point of [
      { lat: "36.7378", lng: "-119.7871" }, // Fresno, between the two boards.
      { lat: "40.7128", lng: "-74.0060" }, // New York.
      { lat: "0", lng: "0" }, // Null Island, the classic probe.
    ]) {
      const resolved = resolveRegion(regions, point);
      assert.ok(!resolved.ok, `${point.lat},${point.lng} must be refused`);
      assert.match(resolved.message, /serves: sf, socal/);
    }
  });

  it("refuses a coordinate that is not a coordinate", () => {
    for (const lat of [
      "banana",
      "NaN",
      "Infinity",
      "1e5",
      "0x2f",
      "37.7749deg",
      "91",
      "37.77490001",
      "",
      " ",
    ]) {
      const resolved = resolveRegion(regions, { lat, lng: "-122.4194" });
      assert.ok(!resolved.ok, `lat=${lat} must be refused`);
    }
    assert.ok(!resolveRegion(regions, { lat: "37.7", lng: "181" }).ok);
  });

  it("refuses a repeated parameter rather than picking one", () => {
    // `?lat=1&lat=2` arrives as an array, and quietly taking either half is how
    // a parser disagreement becomes a security bug somewhere downstream.
    assert.ok(!resolveRegion(regions, { lat: ["37.7", "0"], lng: "-122.4" }).ok);
    assert.ok(!resolveRegion(regions, { city: ["sf", "socal"] }).ok);
  });

  it("refuses half a coordinate", () => {
    assert.ok(!resolveRegion(regions, { lat: "37.7749" }).ok);
    assert.ok(!resolveRegion(regions, { lng: "-122.4194" }).ok);
  });

  it("refuses a request that asks two ways at once", () => {
    const resolved = resolveRegion(regions, { city: "sf", lat: "37.7", lng: "-122.4" });
    assert.ok(!resolved.ok);
    assert.match(resolved.message, /not both/);
  });

  it("refuses an unknown city without pretending it might exist elsewhere", () => {
    const resolved = resolveRegion(regions, { city: "atlantis" });
    assert.ok(!resolved.ok);
    assert.match(resolved.message, /sf, socal/);
  });
});

describe("the routes that take a region", () => {
  for (const route of ["weather", "flights"]) {
    it(`serves ${route} for either city, from that city's own point`, async () => {
      const app = appWith({});
      after(() => app.close());

      const sf = await app.inject({ method: "GET", url: `/api/v1/${route}?city=sf` });
      const socal = await app.inject({ method: "GET", url: `/api/v1/${route}?city=socal` });
      assert.equal(sf.statusCode, 200);
      assert.equal(socal.statusCode, 200);
      assert.notDeepEqual(sf.json(), socal.json());
    });

    it(`refuses a nonsense ${route} query with a 400 and no cached copy of it`, async () => {
      const app = appWith({});
      after(() => app.close());

      const res = await app.inject({ method: "GET", url: `/api/v1/${route}?lat=banana&lng=0` });
      assert.equal(res.statusCode, 400);
      assert.equal(res.json<ErrorBody>().error, "bad_request");
      // The fail-closed default still applies: nothing shared may keep a refusal.
      assert.equal(res.headers["cache-control"], "private, no-store");
    });

    it(`refuses a ${route} request for somewhere this box does not serve`, async () => {
      const app = appWith({});
      after(() => app.close());

      const res = await app.inject({
        method: "GET",
        url: `/api/v1/${route}?lat=51.5072&lng=-0.1276`,
      });
      assert.equal(res.statusCode, 400);
      assert.match(res.json<ErrorBody>().message, /sf, socal/);
    });
  }

  it("puts the weather where it was asked for, not where the box is", async () => {
    const app = appWith({});
    after(() => app.close());

    const res = await app.inject({ method: "GET", url: "/api/v1/weather?city=socal" });
    const body = res.json<WeatherBody>();
    assert.deepEqual(body.location, { lat: 33.82, lng: -118.05 });
    // Still the synthetic clear day, because a zero-config box has no source.
    assert.equal(body.synthetic, true);
  });

  it("flies the Southland's own airports over the Southland", async () => {
    const app = appWith({});
    after(() => app.close());

    const body = (
      await app.inject({ method: "GET", url: "/api/v1/flights?city=socal" })
    ).json<FlightsBody>();
    assert.ok(body.mode === "plan");
    // LAX's published reference point, as a departure or an arrival. The Bay
    // Area plan cannot produce it, which is the whole point of the assertion.
    const lax = body.routes.some(
      (leg) =>
        (leg.from[0] === 33.9425 && leg.from[1] === -118.4081) ||
        (leg.to[0] === 33.9425 && leg.to[1] === -118.4081),
    );
    assert.ok(lax, "the SoCal plan should fly out of LAX");
  });

  it("lays out spokes for a city it has never heard of", async () => {
    const app = appWith({ TERA_REGIONS: "pdx:45.5152,-122.6784" });
    after(() => app.close());

    const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
    assert.ok(body.mode === "plan" && body.routes.length === 8);
  });

  it("publishes the allowlist on health so a client can stop guessing", async () => {
    const app = appWith({});
    after(() => app.close());

    const body = (await app.inject({ method: "GET", url: "/api/v1/health" })).json<{
      regions: { id: string }[];
    }>();
    assert.deepEqual(
      body.regions.map((region) => region.id),
      ["sf", "socal"],
    );
  });
});
