/**
 * The acceptance test: a stranger with no keys, no account and no environment.
 *
 * This is CONTRACT.md §5.1 written as code, because it is the thing two
 * independent server designs got wrong in the same way — a weather source that
 * defaults to a provider needing a contact string, and a hard failure when it is
 * absent. The last test in this file starts the real entry point under a
 * genuinely empty environment and asks it for its health, which is the same
 * assertion the `docker compose up` CI job makes from outside.
 */

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { after, describe, it } from "node:test";
import { buildApp } from "../app.ts";
import { loadConfig } from "../config.ts";
import type {
  FlightsBody,
  HealthBody,
  MarkersBody,
  WeatherBody,
} from "../../../src/server/wire.ts";

/** `loadConfig({})` is exactly what `env -i` produces, minus the process. */
function emptyEnvApp() {
  const config = loadConfig({});
  config.logLevel = "silent";
  return buildApp(config);
}

describe("a box handed nothing", () => {
  it("defaults every source to its keyless setting and reports no demotions", () => {
    const config = loadConfig({});
    assert.equal(config.weather.source, "none");
    assert.equal(config.flights.source, "sim");
    assert.equal(config.markers.source, "none");
    assert.equal(config.auth.mode, "none");
    // Nobody configured an admin, so there is no admin. The god tier has to be
    // the thing an empty environment does *not* hand out.
    assert.deepEqual(config.auth.admins, { everyone: false, subjects: [] });
    assert.equal(config.host, "127.0.0.1");
    assert.equal(config.port, 8431);
    assert.deepEqual(config.degraded, []);
  });

  it("answers health", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const res = await app.inject({ method: "GET", url: "/api/v1/health" });
    assert.equal(res.statusCode, 200);

    const body = res.json<HealthBody>();
    assert.equal(body.ok, true);
    assert.equal(body.auth.mode, "none");
    assert.equal(body.auth.entryUrl, null);
    assert.deepEqual(body.degraded, []);
  });

  it("serves a synthetic clear day rather than failing on a missing contact", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const res = await app.inject({ method: "GET", url: "/api/v1/weather" });
    assert.equal(res.statusCode, 200);

    const body = res.json<WeatherBody>();
    assert.equal(body.synthetic, true);
    assert.equal(body.source, "none");
    assert.equal(body.condition, "clear");
  });

  it("serves the simulated sky as a plan, not as positions", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const body = (await app.inject({ method: "GET", url: "/api/v1/flights" })).json<FlightsBody>();
    assert.equal(body.mode, "plan");
    assert.equal(body.source, "sim");
    assert.ok(body.mode === "plan" && body.routes.length > 0);
    // A fixed origin, so a restart does not teleport every aircraft.
    assert.ok(body.mode === "plan" && body.t0 < Date.now());
  });

  it("serves no markers and says so, rather than 404ing the route", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const res = await app.inject({ method: "GET", url: "/api/v1/markers" });
    assert.equal(res.statusCode, 200);
    assert.deepEqual(res.json<MarkersBody>().markers, []);
  });

  it("has no offices", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const res = await app.inject({ method: "GET", url: "/api/v1/offices/anything" });
    assert.equal(res.statusCode, 404);
  });
});

describe("cache-control is fail-closed", () => {
  it("stamps private, no-store on anything that did not opt in", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const health = await app.inject({ method: "GET", url: "/api/v1/health" });
    assert.equal(health.headers["cache-control"], "private, no-store");

    const missing = await app.inject({ method: "GET", url: "/api/v1/nope" });
    assert.equal(missing.statusCode, 404);
    assert.equal(missing.headers["cache-control"], "private, no-store");
  });

  it("lets a route opt in explicitly", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const res = await app.inject({ method: "GET", url: "/api/v1/flights" });
    assert.match(String(res.headers["cache-control"]), /^public, max-age=\d+$/);
  });

  it("refuses to opt in when the request carried a credential", async () => {
    const app = emptyEnvApp();
    after(() => app.close());

    const res = await app.inject({
      method: "GET",
      url: "/api/v1/flights",
      headers: { authorization: "Bearer something" },
    });
    assert.equal(res.headers["cache-control"], "private, no-store");
  });
});

describe("the real process under env -i", () => {
  it("boots and answers health with no environment at all", async () => {
    const entry = new URL("../index.ts", import.meta.url).pathname;
    // A genuinely empty environment: no PATH, no HOME, no TERA_*. `execPath` is
    // absolute, so the child needs nothing from the parent to start.
    const child = spawn(process.execPath, [entry], { env: {}, stdio: "ignore" });
    after(() => child.kill("SIGKILL"));

    const url = "http://127.0.0.1:8431/api/v1/health";
    const deadline = Date.now() + 15_000;
    let body: HealthBody | null = null;
    while (Date.now() < deadline && body === null) {
      try {
        const res = await fetch(url);
        if (res.ok) body = (await res.json()) as HealthBody;
      } catch {
        await new Promise((resolve) => setTimeout(resolve, 200));
      }
    }

    assert.ok(body !== null, "the server never answered on 127.0.0.1:8431");
    assert.equal(body.ok, true);
    assert.equal(body.service, "tera-api");
  });
});
