import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  STUDIO_OPS_INACTION,
  STUDIO_OPS_MANIFEST,
  STUDIO_OPS_SCENARIOS,
  StudioOpsEnvironment,
  arenaChecksum,
  quantizeObservable,
  rollout,
  studioOpsEnergyPenalty,
  studioOpsNoisePenalty,
  studioOpsScriptedBaseline,
  studioOverflights,
  studioSkyAt,
  studioVehicleReadiness,
  studioWeatherAt,
  type StudioOpsAction,
  type StudioOpsObservation,
  type StudioOpsReward,
} from "../../index.ts";
import { Plan } from "../../interiors/plan.ts";
import { LUMBRIDGE_HQ } from "../../offices/lumbridge-hq.ts";
import { MATEO_COURT } from "../../offices/mateo-court.ts";

const SEEDS = [1, 0xdecafbad];

type Observation = StudioOpsObservation;

function scripted(observation: unknown): StudioOpsAction {
  return studioOpsScriptedBaseline(observation as Observation);
}

/** Runs a targeted policy and reports where the episode ended. */
function reach(
  scenarioId: string,
  seed: number,
  policy: (observation: Observation, step: number) => StudioOpsAction,
): { reason: string | null; steps: number; total: number } {
  const result = rollout(
    new StudioOpsEnvironment(),
    (observation, step) => policy(observation as Observation, step),
    { seed, scenario: scenarioId },
  );
  return { reason: result.final.info.terminalReason, steps: result.steps, total: result.total };
}

describe("studio-ops baselines", () => {
  it("keeps inaction below zero and never lets it reach the goal", () => {
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      for (const seed of SEEDS) {
        const idle = rollout(new StudioOpsEnvironment(), () => STUDIO_OPS_INACTION, {
          seed,
          scenario: definition.id,
        });
        assert.ok(idle.total < 0, `${definition.id}/${seed} inaction=${idle.total}`);
        assert.notEqual(idle.final.info.terminalReason, "job-complete");
      }
    }
  });

  it("proves a positive scripted completion that beats inaction on every scenario", () => {
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      for (const seed of SEEDS) {
        const idle = rollout(new StudioOpsEnvironment(), () => STUDIO_OPS_INACTION, {
          seed,
          scenario: definition.id,
        });
        const run = rollout(new StudioOpsEnvironment(), scripted, {
          seed,
          scenario: definition.id,
        });
        const label = `${definition.id}/${seed}`;
        assert.equal(run.final.info.terminalReason, "job-complete", label);
        assert.equal(run.final.terminated, true, label);
        assert.equal(run.final.truncated, false, label);
        assert.ok(run.total > 0, `${label} scripted=${run.total}`);
        assert.ok(run.total > idle.total, `${label} ${run.total} <= ${idle.total}`);
      }
    }
  });

  it("sums thirteen finite named components and authors no total", () => {
    const run = rollout(new StudioOpsEnvironment(), scripted, {
      seed: 5,
      scenario: "train-sf-clear-morning-desk-check",
    });
    const components = run.final.rewardComponents as StudioOpsReward;
    assert.deepEqual(
      Object.keys(components).sort(),
      Object.keys(STUDIO_OPS_MANIFEST.rewardComponents).sort(),
    );
    const sum = Object.values(components).reduce((total, value) => total + value, 0);
    assert.ok(Math.abs(sum - run.final.reward) < 1e-12);
    assert.ok(Object.values(components).every((value) => Number.isFinite(value)));
  });
});

describe("studio-ops terminals are each individually reachable", () => {
  it("completes the job", () => {
    assert.equal(reach("train-sf-clear-morning-desk-check", 1, scripted).reason, "job-complete");
  });

  it("stalls against resolved office collision", () => {
    // Straight into a wall, forever. The controller's own recovery gives up.
    const result = reach("train-la-overcast-inspection", 2, () => ({
      ...STUDIO_OPS_INACTION,
      z: 1,
    }));
    assert.equal(result.reason, "collision-stall");
  });

  it("hits the invalid-interaction limit", () => {
    const result = reach("train-la-overcast-inspection", 2, () => ({
      ...STUDIO_OPS_INACTION,
      interact: true,
    }));
    assert.equal(result.reason, "wrong-interaction-limit");
    // Exactly at the limit rather than somewhere after it.
    assert.equal(result.steps, 8);
  });

  it("exhausts the studio's energy reserve", () => {
    // Everything on and the car plugged into a post that draws from the same
    // reserve. This is the terminal the `energyReservePct` observation exists
    // to make visible; without it the failure would be unattributable.
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      const result = reach(definition.id, 3, () => ({
        ...STUDIO_OPS_INACTION,
        speakerVolume: 1,
        speakerPlay: true,
        vehiclePrecondition: true,
        vehicleCharge: true,
      }));
      assert.equal(result.reason, "battery-depleted", definition.id);
    }
  });

  it("misses the departure", () => {
    const result = reach("dev-sf-windy-evening-delivery", 1, () => STUDIO_OPS_INACTION);
    assert.equal(result.reason, "departure-missed");
    assert.equal(result.steps, 1000);
  });

  it("declares every one of them except the goal as a safety terminal", () => {
    assert.deepEqual([...STUDIO_OPS_MANIFEST.safetyTerminals], [
      "collision-stall",
      "wrong-interaction-limit",
      "battery-depleted",
      "departure-missed",
    ]);
  });
});

describe("studio-ops snapshot, trace and replay", () => {
  it("continues bit-exactly from a mid-episode snapshot on every scenario", () => {
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      const environment = new StudioOpsEnvironment();
      let observation = environment.reset(311, definition.id).observation;
      for (let index = 0; index < 25; index += 1) {
        observation = environment.step(scripted(observation)).observation;
      }
      const checkpoint = environment.snapshot();
      const action = scripted(observation);
      const expected = environment.step(action);
      const restored = environment.restore(checkpoint);
      assert.equal(restored.info.step, checkpoint.step, definition.id);
      // Deep-equal on the whole transition: observation, reward, every
      // component, both flags and the state checksum.
      assert.deepEqual(environment.step(action), expected, definition.id);
    }
  });

  it("replays a trace to the identical final checksum and cumulative reward", () => {
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      const environment = new StudioOpsEnvironment();
      let observation = environment.reset(311, definition.id).observation;
      for (let index = 0; index < 80; index += 1) {
        const result = environment.step(scripted(observation));
        observation = result.observation;
        if (result.terminated || result.truncated) break;
      }
      const trace = environment.trace();
      assert.equal(arenaChecksum({ ...trace, checksum: undefined }), trace.checksum);
      const replay = new StudioOpsEnvironment().replay(trace);
      assert.equal(replay.finalStateChecksum, trace.finalStateChecksum, definition.id);
      assert.equal(replay.cumulativeReward, trace.cumulativeReward, definition.id);
      assert.equal(replay.steps, trace.steps.length, definition.id);
    }
  });

  /** Twelve scripted steps, and the environment left mid-episode. */
  function midEpisode(): StudioOpsEnvironment {
    const environment = new StudioOpsEnvironment();
    let observation = environment.reset(19, "train-la-hot-afternoon-studio").observation;
    for (let index = 0; index < 12; index += 1) {
      observation = environment.step(scripted(observation)).observation;
    }
    return environment;
  }

  it("throws on a tampered snapshot, and refuses to keep running afterwards", () => {
    const environment = midEpisode();
    const snapshot = environment.snapshot();
    assert.throws(
      () => environment.restore({ ...snapshot, cumulativeReward: snapshot.cumulativeReward + 1 }),
      /checksum mismatch/,
    );
    // Re-signed, so only the source pins can catch it.
    const reserved = structuredClone(snapshot.simulation);
    reserved.energyReserveKWh = -1;
    const { checksum: _snapshotChecksum, ...snapshotCore } = {
      ...snapshot,
      simulation: reserved,
    };
    assert.throws(
      () => environment.restore({ ...snapshotCore, checksum: arenaChecksum(snapshotCore) }),
      /simulation snapshot is invalid/,
    );
    // And the environment is now un-reset rather than half-restored: the
    // episode bookkeeping was written before the simulation payload was
    // rejected, so continuing would produce a mixture of two episodes.
    assert.throws(() => environment.trace(), /must be reset/);
    assert.throws(() => environment.snapshot(), /must be reset/);
    // A fresh reset brings it back.
    assert.equal(environment.reset(19, "train-la-hot-afternoon-studio").info.step, 0);
  });

  it("throws on a tampered trace", () => {
    const environment = midEpisode();
    const trace = environment.trace();
    assert.throws(
      () => new StudioOpsEnvironment().replay({
        ...trace,
        cumulativeReward: trace.cumulativeReward + 1,
      }),
      /checksum mismatch/,
    );
    // A re-signed reward claim: the envelope's own checksum verifies, so the
    // only thing standing between this and an accepted rollout is that `replay`
    // recomputes the reward and compares it. This is the cheat the whole
    // envelope exists to refuse.
    const inflated = trace.steps.map((frame, index) =>
      index === 4 ? { ...frame, reward: frame.reward + 1 } : frame,
    );
    const { checksum: _rewardChecksum, ...rewardCore } = { ...trace, steps: inflated };
    assert.throws(
      () => new StudioOpsEnvironment().replay({
        ...rewardCore,
        checksum: arenaChecksum(rewardCore),
      }),
      /diverged at step 5/,
    );

    // And a re-signed action swap, which is refused wherever it first shows —
    // at the frame whose checksum no longer matches, or at the final state if
    // the swapped action happened to change nothing until the end.
    const swapped = trace.steps.map((frame, index) =>
      index === 4 ? { ...frame, action: { ...frame.action, speakerPlay: true } } : frame,
    );
    const { checksum: _traceChecksum, ...traceCore } = { ...trace, steps: swapped };
    assert.throws(
      () => new StudioOpsEnvironment().replay({
        ...traceCore,
        checksum: arenaChecksum(traceCore),
      }),
      /diverged at step|final state mismatch/,
    );
  });

  it("reproduces an episode exactly from the same seed, twice", () => {
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      const first = rollout(new StudioOpsEnvironment(), scripted, {
        seed: 88,
        scenario: definition.id,
      });
      const second = rollout(new StudioOpsEnvironment(), scripted, {
        seed: 88,
        scenario: definition.id,
      });
      assert.deepEqual(first, second, definition.id);
    }
  });
});

describe("studio-ops couples its variables rather than stacking five tasks", () => {
  it("charges more for the same energy under more cloud, strictly", () => {
    // The reward's own implementation, over a fine grid: `advanceSimulation`
    // calls exactly this function with exactly these arguments.
    for (const loadKw of [0.42, 2.4, 13.9, 152]) {
      let previous = 0;
      for (let cloud = 0; cloud <= 1.0001; cloud += 0.02) {
        const penalty = studioOpsEnergyPenalty(loadKw, cloud);
        assert.ok(penalty < 0, `${loadKw}@${cloud}`);
        if (cloud > 0) {
          assert.ok(
            Math.abs(penalty) > Math.abs(previous),
            `magnitude did not rise at cloud=${cloud}, load=${loadKw}`,
          );
        }
        previous = penalty;
      }
    }
    // And it is monotone in the load as well, which is the other half of the
    // claim that this is an energy price and not a weather penalty.
    assert.ok(
      Math.abs(studioOpsEnergyPenalty(150, 0.5)) > Math.abs(studioOpsEnergyPenalty(2, 0.5)),
    );
  });

  it("charges more under an overcast sky end to end, even against a heavier load", () => {
    // Stronger than "all else equal": the hot LA afternoon draws strictly more
    // power than the overcast one (a 13 K climate gap against a 4 K one) and is
    // still charged less, because the cloud term dominates the load difference.
    const energyAt = (scenarioId: string): { energy: number; cloud: number } => {
      const environment = new StudioOpsEnvironment();
      environment.reset(1, scenarioId);
      const result = environment.step(STUDIO_OPS_INACTION);
      return {
        energy: (result.rewardComponents as StudioOpsReward).energy,
        cloud: (result.observation as Observation).cloudCover,
      };
    };
    const clear = energyAt("train-la-hot-afternoon-studio");
    const overcast = energyAt("train-la-overcast-inspection");
    assert.ok(clear.cloud < 0.25, `clear cloud=${clear.cloud}`);
    assert.ok(overcast.cloud > 0.7, `overcast cloud=${overcast.cloud}`);
    assert.ok(
      Math.abs(overcast.energy) > Math.abs(clear.energy),
      `${overcast.energy} vs ${clear.energy}`,
    );
  });

  it("charges more for playback the closer an aircraft is, and nothing when silent", () => {
    const base = {
      speakerPlaying: true,
      speakerVolume: 0.6,
      windKph: 5,
      micLive: false,
      deskOccupied: false,
    };
    let previous = 0;
    // Walking the aircraft in from beyond audible range to directly overhead.
    for (let slant = 5000; slant >= 0; slant -= 100) {
      const penalty = studioOpsNoisePenalty({ ...base, nearestAircraftSlantM: slant });
      if (slant < 5000) {
        assert.ok(penalty < previous, `did not worsen at slant=${slant}`);
      }
      previous = penalty;
    }
    assert.ok(previous < 0);
    // Beyond the overhead range it is flat, not negative: an aircraft that
    // cannot be heard costs nothing.
    assert.equal(studioOpsNoisePenalty({ ...base, nearestAircraftSlantM: 9000 }), 0);
    // And a silent speaker cannot be ruined by anything at all.
    assert.equal(
      studioOpsNoisePenalty({ ...base, speakerPlaying: false, nearestAircraftSlantM: 0 }),
      0,
    );
    assert.equal(
      studioOpsNoisePenalty({ ...base, speakerVolume: 0, nearestAircraftSlantM: 0 }),
      0,
    );
  });

  it("adds wind and microphone bleed to the same penalty", () => {
    const quiet = {
      speakerPlaying: true,
      speakerVolume: 0.5,
      nearestAircraftSlantM: 40_000,
      windKph: 5,
      micLive: false,
      deskOccupied: false,
    };
    assert.equal(studioOpsNoisePenalty(quiet), 0);
    assert.ok(studioOpsNoisePenalty({ ...quiet, windKph: 80 }) < 0);
    assert.ok(studioOpsNoisePenalty({ ...quiet, micLive: true, deskOccupied: true }) < 0);
    // Bleed needs both: a live microphone in an empty room is not on the take.
    assert.equal(studioOpsNoisePenalty({ ...quiet, micLive: true }), 0);
  });

  it("computes the noise component from the sky and wind it publishes", () => {
    // The wiring, end to end: whatever the observation says the sky and the
    // wind are doing is what the penalty was computed from. A regression that
    // read a stale step's weather would break here and nowhere else.
    const environment = new StudioOpsEnvironment();
    let observation = environment.reset(31, "train-la-hot-afternoon-studio")
      .observation as Observation;
    let sawOverhead = false;
    for (let index = 0; index < 700; index += 1) {
      const result = environment.step({
        ...STUDIO_OPS_INACTION,
        micMute: true,
        speakerVolume: 0.5,
        speakerPlay: true,
      });
      observation = result.observation as Observation;
      const expected = studioOpsNoisePenalty({
        speakerPlaying: observation.speakerPlaying,
        speakerVolume: observation.speakerVolume,
        nearestAircraftSlantM: observation.nearestAircraftSlantM,
        windKph: observation.windKph,
        micLive: observation.micPowered && !observation.micMuted,
        deskOccupied: observation.deskOccupied,
      });
      assert.equal((result.rewardComponents as StudioOpsReward).noise, expected, `step ${index}`);
      if (observation.aircraftOverheadCount > 0) sawOverhead = true;
      if (result.terminated || result.truncated) break;
    }
    assert.ok(sawOverhead, "no aircraft came overhead; the assertion above proved nothing");
  });

  it("couples the microphone to the robot's job rather than to a schedule", () => {
    // The non-negotiable property, observable: the desk in front of
    // `sf-desk-mic` is occupied exactly while the robot is working the station
    // that stands at it, and a mic left live through the rest of the episode is
    // charged for it.
    const environment = new StudioOpsEnvironment();
    let observation = environment.reset(1, "train-sf-clear-morning-desk-check")
      .observation as Observation;
    let occupied = 0;
    let wasted = 0;
    let ready = 0;
    for (let index = 0; index < 1200; index += 1) {
      const result = environment.step({ ...STUDIO_OPS_INACTION, ...scripted(observation), micMute: false });
      observation = result.observation as Observation;
      const components = result.rewardComponents as StudioOpsReward;
      if (observation.deskOccupied) occupied += 1;
      if (components.audioWaste < 0) wasted += 1;
      if (components.audioReady > 0) ready += 1;
      if (result.terminated || result.truncated) break;
    }
    assert.ok(occupied > 0, "the robot never reached the desk its microphone serves");
    assert.ok(ready >= occupied, "a live mic at an occupied desk must be paid for");
    assert.ok(wasted > 0, "a live mic at an empty desk must be charged for");
  });

  it("stops charging the mic the moment it is muted", () => {
    const environment = new StudioOpsEnvironment();
    let observation = environment.reset(1, "train-la-overcast-inspection")
      .observation as Observation;
    const hot = environment.step({ ...STUDIO_OPS_INACTION, micMute: false });
    assert.equal((hot.rewardComponents as StudioOpsReward).audioWaste < 0, true);
    observation = hot.observation as Observation;
    assert.equal(observation.deskOccupied, false);
    const muted = environment.step({ ...STUDIO_OPS_INACTION, micMute: true });
    assert.equal((muted.rewardComponents as StudioOpsReward).audioWaste, 0);
  });

  it("shapes the vehicle with a bounded potential that cannot be farmed", () => {
    // Potential-based: plugging and unplugging round-trips to zero rather than
    // paying twice, because the term is the *change* in a bounded readiness.
    const required = 61.6;
    assert.equal(studioVehicleReadiness(0, 21, required), 0.5);
    assert.equal(studioVehicleReadiness(required, 21, required), 1);
    assert.ok(studioVehicleReadiness(61, 21, required) < studioVehicleReadiness(61.5, 21, required));
    assert.ok(studioVehicleReadiness(61, 30, required) < studioVehicleReadiness(61, 22, required));
    // Bounded on both sides, so the shaping cannot diverge.
    for (const soc of [-10, 0, 50, 500]) {
      for (const cabin of [-40, 21, 90]) {
        const value = studioVehicleReadiness(soc, cabin, required);
        assert.ok(value >= 0 && value <= 1, `${soc}/${cabin}`);
      }
    }
  });
});

describe("studio-ops weather and sky are scenario parameters, deterministically evolved", () => {
  const parameters = STUDIO_OPS_SCENARIOS.resolve(7, "dev-sf-windy-evening-delivery").parameters;

  it("is a pure function of the scenario and the elapsed time", () => {
    for (const seconds of [0, 13.7, 60, 119.9]) {
      assert.deepEqual(
        studioWeatherAt(parameters, seconds),
        studioWeatherAt(parameters, seconds),
      );
    }
    assert.deepEqual(studioOverflights(parameters), studioOverflights(parameters));
  });

  it("actually moves inside one episode, in every field a policy can read", () => {
    const samples = [0, 20, 40, 60, 80, 100, 119].map((s) => studioWeatherAt(parameters, s));
    for (const field of ["cloudCover", "precipitation", "windKph", "windDirDeg"] as const) {
      const values = new Set(samples.map((sample) => sample[field]));
      assert.ok(values.size > 1, `${field} never changed`);
    }
    const sky = [0, 30, 60, 90, 119].map((s) => studioSkyAt(studioOverflights(parameters), s));
    assert.ok(new Set(sky.map((entry) => entry.nearestSlantM)).size > 1);
  });

  it("keeps every reading inside the range its own space declares", () => {
    for (let seconds = 0; seconds <= 120; seconds += 0.5) {
      const weather = studioWeatherAt(parameters, seconds);
      assert.ok(weather.cloudCover >= 0 && weather.cloudCover <= 1);
      assert.ok(weather.precipitation >= 0 && weather.precipitation <= 1);
      assert.ok(weather.windKph >= 0);
      assert.ok(weather.windDirDeg >= 0 && weather.windDirDeg < 360);
      assert.ok(weather.visibilityKm >= 0.2);
    }
  });

  it("never claims a profile was observed when it was invented", () => {
    // Every shipped scenario is a fixture, and the flag says so. The field
    // exists for an operator who freezes a real observation into one.
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      const resolved = STUDIO_OPS_SCENARIOS.resolve(3, definition.id);
      assert.equal(resolved.parameters.weatherReported, false, definition.id);
      const environment = new StudioOpsEnvironment();
      const observation = environment.reset(3, definition.id).observation as Observation;
      assert.equal(observation.weatherReported, false, definition.id);
    }
  });

  it("puts the sky and the weather in the scenario hash, where a trace can carry them", () => {
    const a = STUDIO_OPS_SCENARIOS.resolve(3, "train-la-overcast-inspection");
    const b = STUDIO_OPS_SCENARIOS.resolve(4, "train-la-overcast-inspection");
    assert.notEqual(a.hash, b.hash);
    assert.notEqual(a.parameters.aircraftScheduleSeed, b.parameters.aircraftScheduleSeed);
    assert.notEqual(a.parameters.cloudPhase, b.parameters.cloudPhase);
    // The jitter must not turn a named profile into a different one.
    assert.ok(a.parameters.cloudCoverBase > 0.85 && b.parameters.cloudCoverBase > 0.85);
  });
});

describe("studio-ops quantises everything a transcendental touched", () => {
  it("reports no observation finer than the quantum", () => {
    // The cross-runtime defence, checked where it has to hold: if any of these
    // carried full double precision, a verifier on other hardware could reject
    // an honest rollout over the last bit of a sine.
    const environment = new StudioOpsEnvironment();
    let observation = environment.reset(2, "train-la-hot-afternoon-studio")
      .observation as Observation;
    const fields = [
      "sunAltitudeDeg", "sunAzimuthDeg", "hourOfDay", "cloudCover", "precipitation",
      "visibilityKm", "windKph", "windDirDeg", "nearestAircraftSlantM",
      "vehicleSocPct", "vehicleCabinC", "energyReservePct",
    ] as const;
    for (let index = 0; index < 400; index += 1) {
      for (const field of fields) {
        const value = observation[field];
        assert.equal(value, quantizeObservable(value), `${field} at step ${index}`);
      }
      const result = environment.step(scripted(observation));
      observation = result.observation as Observation;
      if (result.terminated || result.truncated) break;
    }
  });

  it("puts no Date anywhere near the checksum", () => {
    // `canonicalJson` throws on one, so this is a live proof rather than a
    // convention: the snapshot survives being hashed.
    const environment = new StudioOpsEnvironment();
    environment.reset(2, "train-sf-clear-morning-desk-check");
    environment.step(STUDIO_OPS_INACTION);
    const snapshot = environment.snapshot();
    assert.match(arenaChecksum(snapshot.simulation), /^fnv1a64:/);
    assert.equal(typeof JSON.parse(JSON.stringify(snapshot.simulation)), "object");
  });
});

describe("studio-ops wraps the simulators the renderer drives", () => {
  it("simulates exactly the devices the shipped packs declare and Plan resolved", () => {
    // Not a headless copy of the device list: the ids the scenarios name are
    // the pack's own, and they are the ones `Plan` accepted.
    const plans = {
      "lumbridge-hq": new Plan(LUMBRIDGE_HQ, { depth: "public", warn: false }),
      "mateo-court": new Plan(MATEO_COURT, { depth: "public", warn: false }),
    };
    for (const definition of STUDIO_OPS_SCENARIOS.definitions) {
      const plan = plans[definition.parameters.officeId];
      const mic = plan.device(definition.parameters.micId);
      const speaker = plan.device(definition.parameters.speakerId);
      assert.ok(mic, `${definition.id} names a microphone the plan did not resolve`);
      assert.ok(speaker, `${definition.id} names a speaker the plan did not resolve`);
      assert.equal(mic.kind, "mic");
      assert.equal(speaker.kind, "speaker");
      /**
       * Every reading an episode sees is invented here, whatever the *pack*
       * says about where its numbers come from in production.
       *
       * This used to assert `mic.provenance === "simulated"`, which stopped
       * being the right question the day `mateo-court`'s two studio instruments
       * were promoted to `first-party-sensor`. The environment builds
       * `createSimulatedDevices(setup.declarations, …)` unconditionally — there
       * is no bridge, no fetch and no network anywhere in this file — so a
       * first-party declaration is a description of hardware that this simulator
       * then makes up numbers for. What has to hold is that such a declaration
       * still carries the sentence that says so, which is exactly the field
       * `validateDeviceDeclaration` now requires of it.
       */
      const sentence = mic.provenance === "simulated"
        ? mic.disclosure
        : declaredMic(definition.parameters.officeId, definition.parameters.micId)
          ?.simulatedDisclosure ?? "";
      assert.match(sentence.toLowerCase(), /simulat/, definition.id);
    }
  });

  /** The authored declaration behind a resolved device, for the fields `Plan` does not carry. */
  function declaredMic(officeId: string, micId: string) {
    const pack = officeId === "lumbridge-hq" ? LUMBRIDGE_HQ : MATEO_COURT;
    for (const level of pack.levels) {
      for (const device of level.floorplan.devices ?? []) {
        if (device.id === micId) return device;
      }
    }
    return undefined;
  }

  it("names the same simulator stack in its manifest as it imports", () => {
    assert.equal(STUDIO_OPS_MANIFEST.id, "studio-ops-v1");
    for (const fragment of [
      "Plan",
      "robotActivity",
      "createSimulatedDevices",
      "createSimulatedVehicleTelemetry",
      "solarPosition",
    ]) {
      assert.ok(STUDIO_OPS_MANIFEST.simulator.includes(fragment), fragment);
    }
  });

  it("observes the robot job, the hardware, the weather, the car and the sky at once", () => {
    // The point of the environment, as a shape assertion: forty-four fields
    // across five groups, none of them constant across the catalogue.
    const environment = new StudioOpsEnvironment();
    const observation = environment.reset(1, "dev-la-marine-layer-loft-delivery")
      .observation as Observation;
    assert.equal(Object.keys(observation).length, STUDIO_OPS_MANIFEST.observationFields.length);
    assert.deepEqual(
      Object.keys(observation).sort(),
      [...STUDIO_OPS_MANIFEST.observationFields].sort(),
    );
    assert.equal(observation.officeId, "mateo-court");
    assert.equal(observation.levelId, "level-2");
    assert.equal(observation.micPowered, true);
    assert.equal(observation.energyReservePct, 100);
    assert.equal(observation.stepsToDeparture, 1000);
  });

  it("keeps the loft scenario's desk unreachable, on purpose", () => {
    // Every LA device is on level 1 and this job runs on level 2, so the
    // correct play is to mute and get on with it. A policy that has only seen a
    // reachable desk has not learned the difference between "unmute when
    // somebody arrives" and "unmute".
    const environment = new StudioOpsEnvironment();
    let observation = environment.reset(1, "dev-la-marine-layer-loft-delivery")
      .observation as Observation;
    for (let index = 0; index < 400; index += 1) {
      const result = environment.step(scripted(observation));
      observation = result.observation as Observation;
      assert.equal(observation.deskOccupied, false, `step ${index}`);
      if (result.terminated || result.truncated) break;
    }
  });
});

describe("studio-ops device commands settle instead of churning", () => {
  it("stops re-commanding a setpoint the instrument has already reached", () => {
    // A gain of 12.005 dB is reported back as 12.01, so an action compared
    // against the reading would disagree with itself forever and be charged a
    // churn cost on every step for holding a control still. `normalizeAction`
    // rounds to the precision the instrument reports, which makes the setpoint
    // a fixed point.
    const environment = new StudioOpsEnvironment();
    environment.reset(1, "train-la-overcast-inspection");
    const action: StudioOpsAction = {
      ...STUDIO_OPS_INACTION,
      micGain: 12.005,
      micMute: true,
      speakerVolume: 0.2225,
    };
    const first = environment.step(action);
    const settled = environment.step(action);
    const again = environment.step(action);
    const controlOf = (result: typeof first): number =>
      (result.rewardComponents as StudioOpsReward).control;
    assert.ok(controlOf(first) < 0, "the first step really does issue commands");
    assert.equal(controlOf(settled), controlOf(again));
    assert.equal(controlOf(settled), 0);
    const observation = again.observation as Observation;
    assert.equal(observation.micGainDb, 12.01);
    assert.equal(observation.speakerVolume, 0.223);
  });
});
