/**
 * The opening move, held to the two things it must never do.
 *
 * Neither of these is a look-at-the-picture question — those were answered with
 * `scripts/look.mjs` and cannot be asserted — but both are the kind of thing
 * that would break silently and be noticed months later in a screenshot.
 *
 *  1. **The hero seat only ever brings a camera down.** A board's whole-board
 *     shot is authored between 32 and 41 degrees above the ground and is much
 *     better seen from lower; a *room's* viewpoint is often eye height, and
 *     lifting a camera that is standing inside a building to twenty-nine
 *     degrees is a ceiling shot of somebody's desk. `Math.min` is the whole
 *     guard and it is one character from being wrong.
 *  2. **Neither pose is allowed past the orbit's ceiling.** `setPose` hands the
 *     camera to `OrbitControls`, which clamps to `maxDistance` on its next
 *     update, so a pose beyond it is not a wider shot — it is a move that
 *     starts wherever the clamp happened to land.
 *
 * Plus the invariant that makes the move a move: the start stands further off
 * and higher than the rest, and looks at the same point.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { Vector3 } from "three";
import { arrivalStart, heroPose } from "../engine/scene.ts";

/** Elevation above the horizontal, in degrees, of a pose about its target. */
function elevationDeg(pose: { position: Vector3; target: Vector3 }): number {
  const d = pose.position.clone().sub(pose.target);
  return (Math.asin(d.y / d.length()) * 180) / Math.PI;
}

function reach(pose: { position: Vector3; target: Vector3 }): number {
  return pose.position.distanceTo(pose.target);
}

/** A pose at `elevation` degrees and `distance` units from the origin. */
function pose(elevationDegrees: number, distance: number) {
  const e = (elevationDegrees * Math.PI) / 180;
  return {
    target: new Vector3(10, 4, -6),
    position: new Vector3(
      10 + Math.cos(e) * distance * 0.6,
      4 + Math.sin(e) * distance,
      -6 + Math.cos(e) * distance * 0.8,
    ),
  };
}

describe("the opening move", () => {
  it("brings a high board pose down and leaves its target alone", () => {
    // California's chapter 01: 408 out, 352 up, so 40.8 degrees.
    const authored = pose(40.8, 538);
    const hero = heroPose(authored, 1_000);
    assert.ok(elevationDeg(hero) < elevationDeg(authored) - 8, elevationDeg(hero).toFixed(1));
    assert.ok(elevationDeg(hero) > 20, elevationDeg(hero).toFixed(1));
    assert.deepEqual(hero.target.toArray(), authored.target.toArray());
  });

  it("gives the three boards one answer, not three", () => {
    // 40.8 California, 34.7 San Francisco, 32.1 Southern California.
    const elevations = [40.8, 34.7, 32.1].map((deg) =>
      elevationDeg(heroPose(pose(deg, 400), 1_000)),
    );
    for (const e of elevations) assert.ok(Math.abs(e - elevations[0]!) < 0.001, String(e));
  });

  it("never raises a camera that is already low — an office keeps its seat", () => {
    const eyeLevel = pose(7, 12);
    const hero = heroPose(eyeLevel, 40);
    assert.ok(
      elevationDeg(hero) <= elevationDeg(eyeLevel) + 0.001,
      `${elevationDeg(hero)} vs ${elevationDeg(eyeLevel)}`,
    );
  });

  it("stands further off and higher than the pose it will land on", () => {
    const rest = heroPose(pose(40.8, 538), 5_000);
    const start = arrivalStart(rest, 5_000);
    assert.ok(reach(start) > reach(rest));
    assert.ok(elevationDeg(start) > elevationDeg(rest));
    assert.deepEqual(start.target.toArray(), rest.target.toArray());
  });

  it("keeps both poses inside the orbit's own ceiling", () => {
    const ceiling = 300;
    const authored = pose(40.8, 538);
    const rest = heroPose(authored, ceiling);
    assert.ok(reach(rest) <= ceiling, String(reach(rest)));
    assert.ok(reach(arrivalStart(rest, ceiling)) <= ceiling, String(reach(arrivalStart(rest, ceiling))));
  });

  it("is a no-op on a degenerate pose rather than a NaN", () => {
    const degenerate = { target: new Vector3(1, 2, 3), position: new Vector3(1, 2, 3) };
    const hero = heroPose(degenerate, 100);
    assert.deepEqual(hero.position.toArray(), [1, 2, 3]);
    assert.deepEqual(hero.target.toArray(), [1, 2, 3]);
  });
});
