/**
 * 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 {
  DeviceCommandBody,
  DeviceCommandResultBody,
  DevicesBody,
  HealthBody,
} 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("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("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();
  });
});
