import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
  CALIFORNIA_AIR_ROUTE,
  createSceneAircraft,
  type SceneAircraftOptions,
} from "../aircraft/index.ts";

function options(overrides: Partial<SceneAircraftOptions> = {}): SceneAircraftOptions {
  return {
    project: (lat, lng) => [(lng + 121) * 20, -(lat - 36) * 20],
    groundAt: () => 2,
    route: CALIFORNIA_AIR_ROUTE,
    initialPosition: { lat: 33.9416, lng: -118.4085 },
    initialAltitudeM: 1_000,
    initialHeadingDeg: 0,
    fixedStepSeconds: 0.1,
    altitudeSceneUnitsPerMetre: 0.005,
    visualSceneUnitsPerMetre: 0.1,
    ...overrides,
  };
}

describe("playable scene aircraft", () => {
  it("authors KLAX to KSFO, not downtown stand-ins", () => {
    assert.equal(CALIFORNIA_AIR_ROUTE[0]?.id, "klax");
    assert.equal(CALIFORNIA_AIR_ROUTE.at(-1)?.id, "ksfo");
    const start = CALIFORNIA_AIR_ROUTE[0]!;
    const end = CALIFORNIA_AIR_ROUTE.at(-1)!;
    assert.ok(Math.abs(start.lat - 33.9416) < 0.02 && Math.abs(start.lng + 118.4085) < 0.02);
    assert.ok(Math.abs(end.lat - 37.6213) < 0.02 && Math.abs(end.lng + 122.379) < 0.02);
  });

  it("projects a stable root and stays inert until activated", () => {
    const aircraft = createSceneAircraft(options());
    const root = aircraft.root;
    assert.equal(root.name, "playable-scene-aircraft");
    assert.ok(root.getObjectByName("electric-aircraft"));
    assert.deepEqual(root.position.toArray(), [(-118.4085 + 121) * 20, 7, -(33.9416 - 36) * 20]);
    assert.equal(root.scale.x, 0.1);
    aircraft.setActions({ throttle: 1, roll: 0.8 });
    aircraft.tick(0.2);
    assert.equal(aircraft.state().elapsedSteps, 0);
    assert.equal(aircraft.root, root);
    aircraft.setActive(true);
    assert.equal(aircraft.actions().throttle, 0, "activation clears stale input");
    aircraft.setActions({ throttle: 1, roll: 0.8 });
    aircraft.tick(0.2);
    assert.equal(aircraft.state().elapsedSteps, 2);
    assert.equal(aircraft.state().mode, "manual");
    assert.equal(aircraft.view.position, aircraft.view.position);
    assert.deepEqual(aircraft.view.position.toArray(), root.position.toArray());
    aircraft.dispose();
  });

  it("maps heading, pitch, and right-bank roll under Tera's -Z convention", () => {
    const aircraft = createSceneAircraft(options({ active: true }));
    aircraft.setActions({ throttle: 0.7, pitch: 1, roll: 1, yaw: 0.2 });
    aircraft.tick(1);
    const state = aircraft.state();
    assert.equal(aircraft.root.rotation.order, "YXZ");
    assert.ok(Math.abs(aircraft.root.rotation.x - state.pitchDeg * Math.PI / 180) < 1e-12);
    assert.ok(Math.abs(aircraft.root.rotation.y + state.headingDeg * Math.PI / 180) < 1e-12);
    assert.ok(Math.abs(aircraft.root.rotation.z + state.rollDeg * Math.PI / 180) < 1e-12);
    assert.ok(aircraft.root.rotation.z < 0, "positive controller roll presents right wing down");
    aircraft.dispose();
  });

  it("animates articulated surfaces and fans from authoritative state", () => {
    const aircraft = createSceneAircraft(options({ active: true }));
    const leftAileron = aircraft.root.getObjectByName("electric-aircraft.aileron-left");
    const rightAileron = aircraft.root.getObjectByName("electric-aircraft.aileron-right");
    const leftFan = aircraft.root.getObjectByName("electric-aircraft.fan-left");
    assert.ok(leftAileron && rightAileron && leftFan);
    aircraft.setActions({ throttle: 1, roll: 0.8, pitch: 0.4, yaw: -0.3 });
    aircraft.tick(0.5);
    assert.ok(leftAileron.rotation.x > 0);
    assert.ok(rightAileron.rotation.x < 0);
    assert.notEqual(leftFan.rotation.z, 0);
    aircraft.dispose();
  });

  it("publishes defensive finite chase poses for north and east headings", () => {
    const north = createSceneAircraft(options({ initialHeadingDeg: 0 }));
    const northPose = north.followPose();
    assert.ok(northPose.position.z > north.root.position.z, "camera is south/behind northbound flight");
    assert.ok(northPose.target.z < north.root.position.z);
    northPose.position.x = 999;
    assert.notEqual(north.followPose().position.x, 999);
    north.dispose();

    const east = createSceneAircraft(options({ initialHeadingDeg: 90 }));
    const eastPose = east.followPose();
    assert.ok(eastPose.position.x < east.root.position.x);
    assert.ok(eastPose.target.x > east.root.position.x);
    assert.ok(eastPose.position.toArray().every(Number.isFinite));
    assert.ok(eastPose.target.toArray().every(Number.isFinite));
    east.dispose();
  });

  it("clears edge actions, resumes assistance, and resets controller plus rig", () => {
    const aircraft = createSceneAircraft(options({ active: true }));
    const spawn = aircraft.state();
    aircraft.setActions({ throttle: 1, roll: 1, modeRequest: "manual", reset: false });
    aircraft.tick(0.2);
    assert.equal(aircraft.actions().modeRequest, "none");
    assert.equal(aircraft.actions().throttle, 1);
    aircraft.setActions({ modeRequest: "assisted" });
    aircraft.tick(0.1);
    assert.equal(aircraft.state().mode, "assisted");
    aircraft.reset();
    assert.deepEqual(aircraft.state(), spawn);
    assert.deepEqual(aircraft.actions(), {
      throttle: 0,
      yaw: 0,
      pitch: 0,
      roll: 0,
      modeRequest: "none",
      reset: false,
    });
    aircraft.dispose();
  });

  it("disposes idempotently and refuses invalid projection/scaling", () => {
    const aircraft = createSceneAircraft(options());
    const parent = new THREE.Group();
    parent.add(aircraft.root);
    aircraft.dispose();
    aircraft.dispose();
    assert.equal(aircraft.root.parent, null);
    const before = aircraft.state();
    aircraft.tick(1);
    assert.deepEqual(aircraft.state(), before);
    assert.throws(() => createSceneAircraft(options({ visualSceneUnitsPerMetre: 0 })), RangeError);
    assert.throws(() => createSceneAircraft(options({ project: () => [Infinity, 0] })), RangeError);
    assert.throws(() => createSceneAircraft(options({ groundAt: () => Number.NaN })), RangeError);
  });
});
