/**
 * Screens and foliage: the two places where a *texture binding* is the whole
 * fix, and where a silently-null map looks exactly like a design decision.
 *
 * Both defects on the live site had the same shape. Every display in the
 * building was one uniform glowing rectangle, and every leaf was a flat green
 * shard, and in both cases the geometry was fine and the material was carrying
 * nothing. The maps now exist (`screenUI`, `leafAlpha`); what these tests pin is
 * that they are bound to the right *slots*, because the difference between
 * `map` and `emissiveMap` is the difference between a monitor and a light box,
 * and the difference between `alphaMap` with `alphaTest` and no `alphaMap` at
 * all is the difference between a leaf and the shard.
 *
 * A stub canvas is installed for these: `TextureBin` returns `null` under Node
 * because there is nothing to draw on, which is correct behaviour and would make
 * every assertion in here vacuously true.
 */

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

import { createAssetContext, kit } from "../../assets/kit.ts";
import { MaterialRegistry } from "../../assets/materials.ts";
import { SCREEN_UI_VARIANTS } from "../../assets/textures.ts";
import "../../assets/office/index.ts";
import { seeded, withStubCanvas } from "./fakeCanvas.ts";

function meshes(root: THREE.Object3D): THREE.Mesh[] {
  const found: THREE.Mesh[] = [];
  root.traverse((object) => {
    if (object instanceof THREE.Mesh) found.push(object);
  });
  return found;
}

function materialsOf(root: THREE.Object3D): Map<string, THREE.MeshStandardMaterial> {
  const found = new Map<string, THREE.MeshStandardMaterial>();
  for (const mesh of meshes(root)) {
    const material = mesh.material as THREE.MeshStandardMaterial;
    found.set(material.name, material);
  }
  return found;
}

function disposeObject(root: THREE.Object3D): void {
  for (const mesh of meshes(root)) mesh.geometry.dispose();
}

describe("screen content and leaf cutouts", () => {
  it("lights the display through its own drawing rather than flooding the panel", () => {
    const restore = withStubCanvas();
    const materials = new MaterialRegistry({ quality: "high" });
    try {
      for (const id of ["tera:screen.monitor", "tera:screen.wall-display"]) {
        const object = kit.build(id, createAssetContext({ materials, rand: seeded() }));
        const byName = materialsOf(object);

        const content = byName.get("screenContent");
        assert.ok(content, `${id} draws no lit content layer`);
        assert.ok(content.map, `${id}: the display carries no map`);
        assert.ok(content.emissiveMap, `${id}: the display carries no emissiveMap`);
        // Same texture in both slots. Two different ones would mean the lit
        // pixels and the drawn pixels disagree, which reads as a ghost image.
        assert.equal(content.map, content.emissiveMap);
        // White emissive, because `emissive` multiplies `emissiveMap`: anything
        // else tints the drawn interface a second time on top of `color`.
        assert.equal(content.emissive.getHex(), 0xffffff);
        assert.ok(content.emissiveIntensity > 0);

        // And the dark panel behind it has to still be there and still be dark:
        // it is the black border of glass, and it is what stops the content
        // running to the edge of the bezel.
        const dark = byName.get("screenDisplay");
        assert.ok(dark, `${id} lost its dark panel`);
        assert.ok(byName.get("screenBezel"), `${id} lost its bezel`);
        assert.notEqual(dark, content);

        disposeObject(object);
      }
    } finally {
      materials.dispose();
      restore();
    }
  });

  it("takes its layout from the authored colorKey, not from per-instance chance", () => {
    // `furnish.ts` draws `ctx.rand` once per *kind*, so a screen cannot roll for
    // a layout: twelve monitors in one batch would roll once between them. The
    // seam is `colorKey`, which is already part of the batch key — so two packs
    // asking for the same key must get the same layout, and different keys must
    // be able to get different ones.
    const restore = withStubCanvas();
    const materials = new MaterialRegistry({ quality: "high" });
    try {
      const layoutFor = (colorKey: string | undefined): THREE.Texture | null => {
        const object = kit.build(
          "tera:screen.monitor",
          createAssetContext({ materials, rand: seeded(), colorKey }),
        );
        // `variant()` names a non-zero layout `screenContent:<hex>:<n>`, so the
        // lookup is by prefix: the point of the seam is that a keyed screen gets
        // a *different material*, and asserting on the base name would only ever
        // find the unkeyed one.
        const content = [...materialsOf(object)].find(([name]) =>
          name.startsWith("screenContent"),
        )?.[1];
        assert.ok(content, "no lit content layer on a keyed screen");
        const map = content.map;
        disposeObject(object);
        return map;
      };

      const plain = layoutFor(undefined);
      assert.equal(layoutFor(undefined), plain, "an unkeyed screen is not stable");

      const keyed = new Set<THREE.Texture | null>();
      for (const key of ["ui-a", "ui-b", "ui-c", "ui-d", "ui-e", "ui-f", "ui-g", "ui-h"]) {
        const first = layoutFor(key);
        assert.equal(layoutFor(key), first, `"${key}" is not stable between builds`);
        keyed.add(first);
      }
      assert.ok(
        keyed.size >= 3,
        `eight distinct keys produced ${keyed.size} layouts out of ${SCREEN_UI_VARIANTS} — the seam is not reaching the texture`,
      );
    } finally {
      materials.dispose();
      restore();
    }
  });

  it("keeps a display readable at `low` quality, where there are no maps at all", () => {
    // `low` is the setting that makes an office open on an integrated GPU and it
    // draws no textures. The asset must still build, and the display must still
    // be a lit surface rather than an untextured white slab that clips.
    const materials = new MaterialRegistry({ quality: "low" });
    try {
      const object = kit.build(
        "tera:screen.monitor",
        createAssetContext({ materials, rand: seeded() }),
      );
      const content = materialsOf(object).get("screenContent");
      assert.ok(content, "no content layer at low quality");
      assert.equal(content.map, null);
      assert.ok(content.emissiveIntensity > 0, "the panel stopped emitting at low quality");
      disposeObject(object);
    } finally {
      materials.dispose();
    }
  });

  it("cuts every leaf out of its quad instead of drawing the quad", () => {
    const restore = withStubCanvas();
    const materials = new MaterialRegistry({ quality: "high" });
    try {
      for (const id of ["tera:plant.potted", "tera:plant.tall"]) {
        const object = kit.build(id, createAssetContext({ materials, rand: seeded() }));
        const leaf = materialsOf(object).get("foliage");
        assert.ok(leaf, `${id} has no foliage material`);
        assert.ok(leaf.alphaMap, `${id}: the leaves are still bare rectangles`);
        assert.equal(leaf.alphaTest, 0.5, `${id}: the cutout threshold is not the drawn one`);
        // Cutout, not blend. A transparent leaf loses its depth write, its sort
        // order and — the one that shows — its leaf-shaped shadow.
        assert.equal(leaf.transparent, false, `${id}: foliage went transparent`);
        assert.equal(leaf.depthWrite, true);
        assert.equal(leaf.side, THREE.DoubleSide);
        disposeObject(object);
      }
    } finally {
      materials.dispose();
      restore();
    }
  });

  it("survives `low` quality with the cutout intact, because a plant has no cheap fallback", () => {
    // "No maps at low quality" is a statement about *shading* cost. An alpha
    // cutout is one fetch and a discard, and the alternative at low quality is
    // not a cheaper plant — it is the shard the whole change exists to remove.
    const restore = withStubCanvas();
    const materials = new MaterialRegistry({ quality: "low" });
    try {
      const object = kit.build(
        "tera:plant.tall",
        createAssetContext({ materials, rand: seeded() }),
      );
      const leaf = materialsOf(object).get("foliage");
      assert.ok(leaf?.alphaMap, "the leaf cutout was dropped at low quality");
      disposeObject(object);
    } finally {
      materials.dispose();
      restore();
    }
  });

  it("runs +V along the leaf, so the cutout lands the right way up", () => {
    // `leafAlpha` is drawn tip-at-top with the stem at the bottom. If a leaf quad
    // ever loses its 0..1 V range — or gets it reversed — the cutout arrives
    // upside down and every plant grows stems out of its tips, which is a
    // failure nobody would think to look for in a UV.
    const restore = withStubCanvas();
    const materials = new MaterialRegistry({ quality: "high" });
    try {
      const object = kit.build(
        "tera:plant.potted",
        createAssetContext({ materials, rand: seeded() }),
      );
      const leaf = meshes(object).find(
        (mesh) => (mesh.material as THREE.Material).name === "foliage",
      );
      assert.ok(leaf);
      const uv = leaf.geometry.getAttribute("uv");
      assert.ok(uv, "the leaf cards carry no UVs to sample the cutout with");
      let minV = Infinity;
      let maxV = -Infinity;
      for (let i = 0; i < uv.count; i++) {
        minV = Math.min(minV, uv.getY(i));
        maxV = Math.max(maxV, uv.getY(i));
      }
      assert.ok(Math.abs(minV) < 1e-6, `leaf V starts at ${minV}`);
      assert.ok(Math.abs(maxV - 1) < 1e-6, `leaf V ends at ${maxV}`);
      disposeObject(object);
    } finally {
      materials.dispose();
      restore();
    }
  });
});
