/**
 * Aerial fog on a follow camera, which never fires OrbitControls `change`.
 *
 * Drive writes `camera.position` and `controls.target` then `lookAt`, with no
 * `controls.update()`. That is structurally silent: the orbit `change` listener
 * that used to own aerial fog never runs, and a fast descent stepped the haze
 * at one hertz off the clock. Crow and aircraft call `setPose`, which does
 * `update()`, but Drive is the case that cannot be rescued by that event.
 *
 * This file is that silence, and the frame-pump path that still moves the fog:
 * read the pose, ask `aerialFog` for the pair, `setFogDistances`. `main.ts`
 * wires the same three calls to the city's tick; the source test in
 * `sceneWiring.test.ts` pins that the app actually does it.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";

(globalThis as unknown as { window: unknown }).window = {
  matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
  innerWidth: 1_200,
  innerHeight: 800,
  devicePixelRatio: 1,
  addEventListener() {},
  removeEventListener() {},
};

const { createSceneKit } = await import("../../engine/scenekit.ts");
const { aerialFog, aerialFogMoved } = await import("../../engine/atmosphere.ts");

function fakeDom(): HTMLElement {
  return {
    style: {},
    clientWidth: 1_200,
    clientHeight: 800,
    addEventListener() {},
    removeEventListener() {},
    setPointerCapture() {},
    releasePointerCapture() {},
    getBoundingClientRect: () => ({
      left: 0, top: 0, width: 1_200, height: 800, right: 1_200, bottom: 800, x: 0, y: 0,
    }),
    getRootNode: () => ({ addEventListener() {}, removeEventListener() {} }),
    ownerDocument: { addEventListener() {}, removeEventListener() {} },
  } as unknown as HTMLElement;
}

/** A lighting rig that exists only to install `scene.fog`. */
function foggedLight(near: number, far: number) {
  return {
    sun: { direction: [0, 1, 0] as [number, number, number], color: 0xffffff, intensity: 1 },
    hemisphere: { sky: 0xffffff, ground: 0x888888, intensity: 0.4 },
    ambient: { color: 0xffffff, intensity: 0.2 },
    sky: { top: 0x88aaff, horizon: 0xaaccff },
    fog: { color: 0xaaccff, near, far },
  };
}

describe("a follow-camera pose that does not fire change still updates fog", () => {
  it("Drive writes the pose without controls.update, and the frame pump still pulls the fog in", () => {
    const scene = new THREE.Scene();
    const kit = createSceneKit({ scene, dom: fakeDom() });
    const metresPerUnit = 100;
    const ceiling = { near: 1_150, far: 3_900 };

    kit.setPose({
      position: new THREE.Vector3(0, 400, 400),
      target: new THREE.Vector3(0, 0, 0),
    });
    const highView = {
      altitudeMetres: (kit.camera.position.y - kit.controls.target.y) * metresPerUnit,
      standoffMetres: kit.camera.position.distanceTo(kit.controls.target) * metresPerUnit,
    };
    const highFog = aerialFog({ ceiling, metresPerUnit, ...highView });
    kit.applyLighting(foggedLight(highFog.near, highFog.far));
    assert.ok(scene.fog instanceof THREE.Fog);
    const farAtHeight = scene.fog.far;

    let changeFired = 0;
    kit.controls.addEventListener("change", () => {
      changeFired += 1;
    });

    // Drive follow: write the pose, look at the target, no controls.update().
    kit.camera.position.set(0, 20, 20);
    kit.controls.target.set(0, 0, 0);
    kit.camera.lookAt(kit.controls.target);
    assert.equal(changeFired, 0, "Drive follow must not fire OrbitControls change");

    const lowView = {
      altitudeMetres: (kit.camera.position.y - kit.controls.target.y) * metresPerUnit,
      standoffMetres: kit.camera.position.distanceTo(kit.controls.target) * metresPerUnit,
    };
    assert.ok(
      aerialFogMoved(lowView.altitudeMetres, highView.altitudeMetres),
      "a drop this large is past the 2% throttle; the test is not measuring a no-op",
    );
    const lowFog = aerialFog({ ceiling, metresPerUnit, ...lowView });
    kit.setFogDistances(lowFog.near, lowFog.far);

    assert.ok(scene.fog instanceof THREE.Fog);
    assert.equal(scene.fog.near, lowFog.near);
    assert.equal(scene.fog.far, lowFog.far);
    assert.ok(
      scene.fog.far < farAtHeight,
      `a lower camera must pull the fog in (${scene.fog.far} after ${farAtHeight})`,
    );
    assert.equal(changeFired, 0, "applying fog must not need a change event");

    kit.dispose();
  });
});
