/**
 * The boundary between this bundle and a deployment, from the browser's side.
 *
 * Three things meet here and all three are easy to get wrong quietly:
 *
 * 1. **The wire bodies are JSON and stay JSON.** `src/server/wire.ts` compiles
 *    to nothing, so nothing in it can be checked by running it — but the shapes
 *    it declares are the shapes both sides build, and a body that does not
 *    survive `JSON.parse(JSON.stringify(x))` unchanged is a body the two sides
 *    will disagree about. The device bodies are new and carry the first
 *    optional-reading type on the wire, which is exactly where a `undefined`
 *    versus `null` mistake hides.
 * 2. **`/health` is read defensively.** `sources.devices` and `degraded` are
 *    newer than servers this client will meet, and a missing field has to fall
 *    the safe way rather than throw or be assumed.
 * 3. **The seam chooses a strategy and says which.** An anonymous visitor and a
 *    zero-config clone both get the local simulator, alive and labelled; a
 *    signed-in viewer on a configured box gets the deployment's readings. What
 *    must never happen is a studio that looks live and is not, or a control
 *    that appears to work and changes nothing anybody else can see.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { resolveAccess } from "../../access.ts";
import { createTeraClient } from "../../adapters/http.ts";
import { createDeviceSource, createNullDeviceSource } from "../../devices/adapter.ts";
import { initialDeviceState, type DeviceDeclaration, type DeviceState } from "../../devices/types.ts";
import type {
  BirdsBody,
  DeviceCommandBody,
  DeviceCommandResultBody,
  DevicesBody,
  HealthBody,
  PortsBody,
  RadarBody,
  VesselsBody,
} from "../../server/wire.ts";

Object.defineProperty(globalThis, "window", {
  configurable: true,
  value: { location: { origin: "https://office.example.test" } },
});

const DECLARATION: DeviceDeclaration = {
  id: "mic-1",
  kind: "mic",
  label: "Desk mic",
  assetId: "tera:device.mic.desk",
  anchor: { levelId: "l1", propId: "mic-prop", seatId: "desk-01" },
  capabilities: ["power", "mute", "gain", "level"],
  provenance: "simulated",
  disclosure: "Simulated studio hardware. Demonstration data, never presence data.",
};

function json(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "content-type": "application/json" },
  });
}

function deployment(routes: Record<string, () => Response>): typeof fetch {
  return (async (input: RequestInfo | URL) => {
    const url = String(input);
    for (const [path, answer] of Object.entries(routes)) {
      if (url.includes(path)) return answer();
    }
    throw new TypeError("Failed to fetch");
  }) as typeof fetch;
}

const health = (over: Partial<HealthBody> = {}): HealthBody => ({
  ok: true,
  service: "tera-api",
  version: "0.1.0",
  uptimeSeconds: 1,
  sources: { weather: "nws", flights: "adsb", satellites: "none", markers: "none", devices: "sim" },
  auth: { mode: "none", entryUrl: null },
  regions: [{ id: "sf", lat: 37.77, lng: -122.42, radiusKm: 120 }],
  degraded: [],
  ...over,
});

describe("the bodies are JSON, and stay JSON", () => {
  it("round-trips a devices body unchanged", () => {
    const body: DevicesBody = {
      officeId: "hq",
      devices: [
        { id: "mic-1", kind: "mic", powered: true, muted: false, gainDb: 12, levelDb: -22.4, observedAt: 17, synthetic: true },
        { id: "spk-1", kind: "speaker", powered: false, volume: 0.35, playing: false, observedAt: 17, synthetic: true },
      ],
      observedAt: 17,
      source: "sim",
      synthetic: true,
      ttlSeconds: 5,
    };
    assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
  });

  it("round-trips a command and its result unchanged", () => {
    const command: DeviceCommandBody = { command: { deviceId: "mic-1", op: "gain", value: 18 } };
    assert.deepEqual(JSON.parse(JSON.stringify(command)), command);

    const result: DeviceCommandResultBody = {
      officeId: "hq",
      device: initialDeviceState(DECLARATION, 17),
      observedAt: 17,
    };
    assert.deepEqual(JSON.parse(JSON.stringify(result)), result);
  });

  it("omits a reading a device does not have, rather than nulling it", () => {
    // The difference matters on the wire as well as in the panel: `null` would
    // survive JSON as a *reading of zero information* that a consumer has to
    // special-case, while an absent key is already the thing every consumer
    // checks for. `initialDeviceState` is where the rule is implemented.
    const state = initialDeviceState({ ...DECLARATION, capabilities: ["power"] }, 1);
    const round = JSON.parse(JSON.stringify(state)) as DeviceState;
    assert.equal("levelDb" in round, false);
    assert.equal("muted" in round, false);
    assert.equal(round.powered, false);
    assert.equal(round.synthetic, true);
  });

  it("declares regions and a device source on the health body", () => {
    // A compile-time assertion made at runtime: `health()` above is typed as a
    // `HealthBody`, so this file would not build if either field left the type.
    const body = health();
    assert.equal(body.sources.devices, "sim");
    assert.equal(body.regions[0]?.id, "sf");
  });
});

describe("the four bodies this build added", () => {
  /**
   * The empty answer for each one, round-tripped.
   *
   * Every one of these feeds spends most of its life empty and **that is the
   * case that has to be right**: California is under rain a mean 0.596% of the
   * time, birds are aloft about ten hours in twenty-four by construction, and
   * no deployment has an AIS licence. So the assertions below are all about the
   * empty body — that it survives JSON, that it carries a fetch age, and that
   * emptiness arrives with a reason rather than as a bare `[]`.
   */
  it("round-trips an empty vessels body, with the sampling interval on it", () => {
    const body: VesselsBody = {
      source: "none",
      fetchedAt: new Date(0).toISOString(),
      vessels: [],
      intervalSeconds: 900,
      ttlSeconds: 60,
    };
    assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
    // Not decoration. Upstream listens for thirty seconds every fifteen minutes,
    // so a hull under way has moved about five kilometres between two fixes: a
    // client may dead-reckon along the reported course, and may never draw a
    // point on the chord between two samples.
    assert.equal(body.intervalSeconds, 900);
  });

  it("keeps every AIS sentinel expressible as absence rather than as a number", () => {
    // The three sentinels are valid numbers and none of them is ever NULL, so a
    // null-check catches nothing and the gate upstream is what strips them. What
    // the wire has to provide is somewhere for "absent" to go once it has.
    const body: VesselsBody = {
      source: "modelled",
      fetchedAt: new Date(0).toISOString(),
      vessels: [
        {
          id: "hull-1",
          kind: "container",
          lat: 33.74,
          lon: -118.26,
          speed: 0,
          course: null,
          heading: null,
          navStatus: 5,
          length: null,
          beam: null,
          ageSeconds: 120,
        },
      ],
      intervalSeconds: 900,
      ttlSeconds: 60,
    };
    assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
    const [hull] = body.vessels;
    assert.ok(hull);
    assert.equal(hull.course, null, "an unknown course must be null, never 360");
    assert.equal(hull.heading, null, "an unknown heading must be null, never 511");
    // And no identity. Names arrive with a licensed feed or not at all.
    for (const forbidden of ["name", "mmsi", "callsign", "destination"]) {
      assert.ok(!(forbidden in hull), `WireVessel grew a ${forbidden}`);
    }
  });

  it("dates a port's throughput to a month and never to now", () => {
    const body: PortsBody = {
      source: "none",
      fetchedAt: new Date(0).toISOString(),
      throughput: [
        {
          portId: "USLAX",
          asOf: "2026-07",
          loadedExport: 111_776,
          emptyExport: 348_691,
          loadedImport: 499_552,
          emptyImport: 446,
        },
      ],
      rates: [
        { id: "FBX01", lane: "China / East Asia to North America West Coast", usdPerFeu: 7_491 },
        { id: "FBX02", lane: "North America West Coast to China / East Asia", usdPerFeu: 347 },
      ],
      ttlSeconds: 3_600,
    };
    assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
    const [row] = body.throughput;
    assert.ok(row);
    assert.match(row.asOf, /^\d{4}-\d{2}$/, "throughput must be dated to its month");
    // The headline the card leads with, restated as arithmetic so a transcription
    // error in the figures fails here rather than on a page.
    const exported = row.loadedExport + row.emptyExport;
    assert.equal(exported, 460_467);
    assert.ok(row.emptyExport / exported > 0.75, "the empty share is the story");
    // No timestamp on a rate. Freightos publishes none, our `observed_at` is our
    // own read clock, and a card that renders it as "as of" is lying.
    for (const rate of body.rates) {
      assert.ok(!("observedAt" in rate), "a freight rate acquired a false timestamp");
    }
  });

  it("lets the radar draw a hole as a hole", () => {
    const body: RadarBody = {
      source: "none",
      fetchedAt: new Date(0).toISOString(),
      field: {
        minLat: 32.5,
        minLng: -124.5,
        cellLat: 0.25,
        cellLng: 0.25,
        rows: 2,
        cols: 2,
        // Dry, dry, raining, and *unknown* — a cell inside the coverage radius
        // of a radar whose RDA is down. `null` and `0` are different claims and
        // this is the type that keeps them apart.
        dbz: [0, 0, 47.5, null],
        observedAt: new Date(0).toISOString(),
        wetFraction: 0.25,
        stations: 16,
        stationsDown: 1,
      },
      ttlSeconds: 300,
    };
    const round = JSON.parse(JSON.stringify(body)) as RadarBody;
    assert.deepEqual(round, body);
    assert.equal(round.field?.dbz[3], null, "an unknown cell survived as anything but null");
    assert.equal(round.field?.dbz.length, (round.field?.rows ?? 0) * (round.field?.cols ?? 0));
  });

  it("never lets an empty sky be a bare empty array", () => {
    const body: BirdsBody = {
      source: "none",
      fetchedAt: new Date(0).toISOString(),
      observedAt: null,
      counties: [],
      statewide: {
        crossed: 393_290,
        peakAloft: 1_501_193,
        peakAt: "2026-08-22T06:20:00Z",
        meanAltitude: 726,
        heading: "south-east",
      },
      quiet: {
        reason: "daylight",
        message:
          "Nothing is aloft. BirdCast measures migration only after dark. Last night " +
          "393,290 birds crossed California heading south-east, peaking at 1,501,193 " +
          "aloft at 23:20 PDT, at a mean 726 metres.",
      },
      ttlSeconds: 600,
    };
    assert.deepEqual(JSON.parse(JSON.stringify(body)), body);
    // The empty state is the layer for most visitors, so an empty set without a
    // reason beside it is the defect rather than the normal case.
    assert.equal(body.counties.length, 0);
    assert.ok(body.quiet !== null, "an empty sky with no reason is indistinguishable from a dead feed");
    // And the statewide headline is the state row, never a sum over counties:
    // summing all 58 gives 2,360,086 against an authoritative 393,290.
    assert.equal(body.statewide?.crossed, 393_290);
  });

  it("reads the three new source ids the same defensive way as `devices`", async () => {
    // A browser meeting a server one version behind sees `undefined` for all
    // three and must conclude the box serves none of them.
    const health = {
      ok: true,
      service: "tera-api",
      version: "0.1.0",
      uptimeSeconds: 1,
      sources: { weather: "none", flights: "sim", satellites: "none", markers: "none", devices: "none" },
      auth: { mode: "none", entryUrl: null },
      regions: [],
      degraded: [],
    } as unknown as HealthBody;
    assert.equal(health.sources.vessels, undefined);
    assert.equal(health.sources.radar, undefined);
    assert.equal(health.sources.birds, undefined);
  });
});

describe("what the browser learns from /health", () => {
  it("reports the device source and the demotions to the interface", async () => {
    const access = await resolveAccess(
      deployment({
        "/health": () =>
          json(
            health({
              degraded: ["TERA_WEATHER_SOURCE=nws needs TERA_WEATHER_CONTACT.", "and another"],
            }),
          ),
      }),
    );
    assert.equal(access.feeds?.devices, true);
    // Built, served, and — until now — read by nobody. This is the whole point
    // of the field: "why is the weather always clear" answers itself.
    assert.deepEqual(access.degraded, [
      "TERA_WEATHER_SOURCE=nws needs TERA_WEATHER_CONTACT.",
      "and another",
    ]);
  });

  it("falls the safe way when the server is older than this client", async () => {
    const older = health();
    delete (older.sources as Partial<HealthBody["sources"]>).devices;
    delete (older as Partial<HealthBody>).degraded;
    const access = await resolveAccess(deployment({ "/health": () => json(older) }));
    // No field means no feed and no request. A poll against a box that never
    // heard of the route is a 404 per TTL per tab, forever.
    assert.equal(access.feeds?.devices, false);
    assert.deepEqual(access.degraded, []);
  });

  it("drops anything in degraded that is not a sentence", async () => {
    const access = await resolveAccess(
      deployment({ "/health": () => json(health({ degraded: [1, null, { a: 1 }, "real"] as never })) }),
    );
    // `[object Object]` in front of an operator who is already looking at this
    // list because something is wrong.
    assert.deepEqual(access.degraded, ["real"]);
  });

  it("has nothing to report about a deployment that does not exist", async () => {
    const access = await resolveAccess(deployment({}));
    assert.equal(access.tier, "member");
    assert.equal(access.feeds, null);
    assert.deepEqual(access.degraded, []);
  });
});

describe("a zero-config clone", () => {
  it("gets an empty device feed that does not claim to be live", async () => {
    const client = createTeraClient({ fetch: deployment({}) });
    const feed = await client.devices("hq");
    assert.deepEqual(feed.value, []);
    assert.equal(feed.live, false);
    assert.equal(feed.source, "none");
  });

  it("gets a studio that is alive anyway, from the simulator in this tab", () => {
    const source = createDeviceSource({ declarations: [DECLARATION], client: null });
    source.command({ deviceId: "mic-1", op: "power", value: true });
    let peak = -60;
    for (let i = 0; i < 400; i += 1) {
      source.tick(0.1);
      peak = Math.max(peak, source.current().states[0]?.levelDb ?? -60);
    }
    assert.ok(peak > -58, `peak ${peak}`);
    // Alive, and honest about it: `live` is "a deployment answered" and
    // `synthetic` is "nobody observed this", and both are what the panel shows.
    assert.equal(source.current().live, false);
    assert.equal(source.current().synthetic, true);
    assert.equal(source.current().source, "sim");
    source.stop();
  });
});

describe("the seam", () => {
  it("is nothing at all for an office that declares no devices", () => {
    const source = createDeviceSource({ declarations: [] });
    assert.deepEqual(source.current().states, []);
    assert.equal(source.current().source, "none");
    source.tick(1);
    assert.deepEqual(source.current().states, []);
  });

  it("reads the API when the deployment has a source and the viewer may read it", async (t) => {
    const body: DevicesBody = {
      officeId: "hq",
      devices: [{ id: "mic-1", kind: "mic", powered: true, observedAt: 1, synthetic: true }],
      observedAt: 1,
      source: "sim",
      synthetic: true,
      ttlSeconds: 5,
    };
    const client = createTeraClient({ fetch: deployment({ "/devices": () => json(body) }) });
    const readings: unknown[] = [];
    const source = createDeviceSource({
      declarations: [DECLARATION],
      client,
      officeId: "hq",
      serverHasDevices: true,
      onReading: (reading) => readings.push(reading),
    });
    t.after(() => source.stop());

    // Before anything lands, the panel has instruments to draw rather than an
    // empty box that would flicker into existence a poll later.
    assert.equal(source.current().states.length, 1);
    assert.equal(source.current().live, false);

    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    assert.equal(source.current().live, true);
    assert.equal(source.current().states[0]?.powered, true);
    assert.equal(readings.length, 1);

    // The server's clock, not ours: ticking must not advance a second set of
    // numbers over the top of a real feed.
    const before = JSON.stringify(source.current().states);
    source.tick(5);
    assert.equal(JSON.stringify(source.current().states), before);
  });

  it("gives an anonymous visitor the simulator, on a box that HAS devices", async (t) => {
    // The shipping bug this fixes, in one test. cloud-2 runs
    // `TERA_DEVICES_SOURCE=sim`, so `/health` reports a device source and
    // `serverHasDevices` is true — but `routes/devices.ts` refuses an anonymous
    // read, correctly, because the readings describe a room somebody is
    // standing in. Without the tier, the API strategy was chosen anyway: the GET
    // 401s, the feed maps to `live: false`, and every visitor to a studio was
    // shown permanently powered-off instruments while the poll backed off
    // exponentially against a 401 it could never pass. Three file headers
    // promised them a living simulated studio instead.
    let asked = 0;
    const client = createTeraClient({
      fetch: deployment({
        "/devices": () => {
          asked += 1;
          return new Response("no", { status: 401 });
        },
      }),
    });
    const source = createDeviceSource({
      declarations: [DECLARATION],
      client,
      officeId: "hq",
      serverHasDevices: true,
      viewerTier: "anon",
    });
    t.after(() => source.stop());

    await new Promise((resolve) => setTimeout(resolve, 0));
    // Not one request spent on a refusal that was knowable in advance.
    assert.equal(asked, 0);
    assert.equal(source.current().source, "sim");
    assert.equal(source.current().live, false);
    assert.equal(source.current().synthetic, true);

    // And alive: two different readings a fifth of a second apart, which is what
    // "living simulated studio" has to mean for it to be worth anything.
    const before = JSON.stringify(source.current().states);
    source.tick(0.2);
    assert.notEqual(JSON.stringify(source.current().states), before);
  });

  it("keeps the API for a signed-in viewer on the same deployment", async (t) => {
    const body: DevicesBody = {
      officeId: "hq",
      devices: [{ id: "mic-1", kind: "mic", powered: true, observedAt: 1, synthetic: true }],
      observedAt: 1,
      source: "sim",
      synthetic: true,
      ttlSeconds: 5,
    };
    const client = createTeraClient({ fetch: deployment({ "/devices": () => json(body) }) });
    const source = createDeviceSource({
      declarations: [DECLARATION],
      client,
      officeId: "hq",
      serverHasDevices: true,
      viewerTier: "member",
    });
    t.after(() => source.stop());
    await new Promise((resolve) => setTimeout(resolve, 0));
    await new Promise((resolve) => setTimeout(resolve, 0));
    assert.equal(source.current().live, true);
  });

  it("skips the API entirely when /health said this box has no devices", async (t) => {
    let asked = 0;
    const client = createTeraClient({
      fetch: deployment({
        "/devices": () => {
          asked += 1;
          return json({});
        },
      }),
    });
    const source = createDeviceSource({
      declarations: [DECLARATION],
      client,
      officeId: "hq",
      serverHasDevices: false,
    });
    t.after(() => source.stop());
    await new Promise((resolve) => setTimeout(resolve, 0));
    assert.equal(asked, 0);
    assert.equal(source.current().source, "sim");
  });

  it("reports a refused command rather than quietly applying it locally", async (t) => {
    // The one asymmetry between the two strategies, and it is deliberate: on a
    // real deployment a control that appears to work and changes nothing
    // anybody else can see is worse than one that says no.
    const client = createTeraClient({ fetch: deployment({}) });
    const source = createDeviceSource({
      declarations: [DECLARATION],
      client,
      officeId: "hq",
      serverHasDevices: true,
    });
    // Registered before the assertions, not after them: a watch left running by
    // a failed assertion keeps its back-off timer alive and hangs the runner
    // long after the failure it is hiding.
    t.after(() => source.stop());

    const result = await source.command({ deviceId: "mic-1", op: "power", value: true });
    assert.equal(result, null);
    // The instruments are still there, at rest and not live — a deployment that
    // has stopped answering is not an office that has no hardware in it.
    assert.equal(source.current().states.length, 1);
    assert.equal(source.current().states[0]?.powered, false);
    assert.equal(source.current().live, false);
  });

  it("refuses a command the declaration does not allow, before spending a request", async (t) => {
    let asked = 0;
    const client = createTeraClient({
      fetch: deployment({
        "/command": () => {
          asked += 1;
          return json({});
        },
      }),
    });
    const source = createDeviceSource({
      declarations: [DECLARATION],
      client,
      officeId: "hq",
      serverHasDevices: true,
    });
    t.after(() => source.stop());
    assert.equal(await source.command({ deviceId: "mic-1", op: "volume", value: 0.5 }), null);
    assert.equal(await source.command({ deviceId: "somebody-elses", op: "power", value: true }), null);
    assert.equal(asked, 0);
  });

  it("applies a command locally, and openly, on the simulated strategy", async () => {
    const source = createDeviceSource({ declarations: [DECLARATION], client: null });
    const state = await source.command({ deviceId: "mic-1", op: "power", value: true });
    assert.equal(state?.powered, true);
    assert.equal(state?.synthetic, true);
    source.stop();
  });

  it("publishes only when a reading a viewer could see has changed", () => {
    const readings: unknown[] = [];
    const source = createDeviceSource({
      declarations: [DECLARATION],
      client: null,
      onReading: (reading) => readings.push(reading),
    });
    // Powered off, so every step produces the same floor reading and only
    // `observedAt` moves — which is deliberately not in the signature.
    for (let i = 0; i < 50; i += 1) source.tick(0.1);
    assert.equal(readings.length, 0);

    void source.command({ deviceId: "mic-1", op: "power", value: true });
    source.tick(0.1);
    assert.ok(readings.length > 0);
    source.stop();
  });

  it("does no work at all once stopped", async () => {
    const source = createDeviceSource({ declarations: [DECLARATION], client: null });
    void source.command({ deviceId: "mic-1", op: "power", value: true });
    for (let i = 0; i < 20; i += 1) source.tick(0.1);
    source.stop();
    const frozen = JSON.stringify(source.current().states);
    for (let i = 0; i < 20; i += 1) source.tick(0.1);
    assert.equal(JSON.stringify(source.current().states), frozen);
    assert.equal(await source.command({ deviceId: "mic-1", op: "mute", value: true }), null);
  });

  it("survives a tab that was backgrounded for ten minutes", () => {
    const source = createDeviceSource({ declarations: [DECLARATION], client: null });
    void source.command({ deviceId: "mic-1", op: "power", value: true });
    // Six hundred seconds of `dt` would be six thousand steps in one frame.
    source.tick(600);
    // Still a valid reading, and it arrived without a hitch.
    const level = source.current().states[0]?.levelDb ?? 0;
    assert.ok(level >= -60 && level <= 0, `${level}`);
  });

  it("has a null source for anything that genuinely has nothing to say", () => {
    const source = createNullDeviceSource();
    assert.deepEqual(source.current().states, []);
    assert.equal(source.current().source, "none");
    assert.equal(source.current().synthetic, true);
    source.tick(1);
    source.refresh();
    source.setOccupancy(["desk-01"]);
    source.stop();
  });
});
