/**
 * The join between the sea and the sky, asserted where it is decided.
 *
 * ## The defect
 *
 * A phone held upright, on the California board, drew a hard horizontal step
 * across the top **10.5%** of the frame: a flat, duller slab above a warm
 * gradient, meeting in one pixel. It survived every gate in the repo. It is a
 * shader constant, so no typecheck sees it; nothing here compiles GLSL, so no
 * test saw it; and it is above the terrain, so the performance budget and the
 * pixel-regression baselines were both unmoved.
 *
 * ## What causes it
 *
 * The sea is a flat plane. However wide it is made — and `SEA_SPAN_MULTIPLE`
 * puts its rim nine board spans out precisely so it never ends in a visible
 * edge — a camera standing off above it has its own **far plane** cut the water
 * at an elevation some degrees *below* the horizontal. In a plan view of the
 * state that angle is around twelve degrees. `atmosphere.ts` has fogged the
 * water to exactly the horizon colour by the time it gets there, which is the
 * whole mechanism by which water dissolves into sky instead of ending.
 *
 * So the dome, at that same angle, is the other half of that join. It has to be
 * painting the same number. It was not: the below-horizon darkening ramped to
 * its full 0.82 over the first 11.5 degrees, so the sea arrived at the horizon
 * colour and met a sky already a fifth darker.
 *
 * ## What is asserted
 *
 * The shader's own text, parsed. That is a weaker instrument than a rendered
 * frame and it is the strongest one available without a GL context, so what it
 * checks is the one number a picture proved wrong: how far below the horizontal
 * the dome still paints the horizon's own colour. A future edit is free to
 * change the curve, the depth, or the colour — it is not free to start
 * darkening inside the band where the sea is still being drawn.
 */
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { describe, it } from "node:test";

const source = readFileSync(new URL("../../engine/scenekit.ts", import.meta.url), "utf8");

/**
 * The lowest elevation, as a sine, at which a board can still be drawing sea.
 *
 * The camera may retreat two spans from the scene origin, and the sea's rim is
 * nine spans out, so the shallowest sight-line to the far water is about
 * `atan(2 / 9)` = 12.5 degrees below the horizontal. Rounded out to 25 degrees,
 * because the number that matters is the one a *fog* far plane produces rather
 * than the geometry's rim, and the far plane moves with the stand-off.
 */
const SEA_MAY_REACH_SINE = Math.sin((25 * Math.PI) / 180);

describe("the sky below the horizon", () => {
  /** The one line that decides it, isolated so a failure names the right thing. */
  const line = /color = mix\( color, uHorizon \* [\d.]+, ([^;]+)\);/.exec(source)?.[1]?.trim();

  it("still exists and is still a blend toward a darker horizon", () => {
    assert.notEqual(line, undefined, "the below-horizon term is gone from the sky shader");
    const factor = Number(/uHorizon \* ([\d.]+)/.exec(source)?.[1]);
    assert.ok(factor > 0 && factor <= 1, `below-horizon sky must not be brighter: ${factor}`);
  });

  it("holds the horizon's own colour through every angle the sea can reach", () => {
    const edges = /smoothstep\(\s*([\d.]+),\s*([\d.]+),\s*- ?height\s*\)/.exec(line ?? "");
    assert.notEqual(
      edges,
      null,
      `the below-horizon ramp is no longer a smoothstep in -height: ${line}`,
    );
    const start = Number(edges![1]);
    assert.ok(
      start > SEA_MAY_REACH_SINE,
      `the sky starts darkening ${((Math.asin(start) * 180) / Math.PI).toFixed(1)}deg below ` +
        `the horizontal, inside the ${((Math.asin(SEA_MAY_REACH_SINE) * 180) / Math.PI).toFixed(0)}deg ` +
        "band where the sea is still drawn — that is the seam",
    );
  });

  /**
   * The fix it replaced, named so it cannot be reintroduced as a tidy-up. A
   * `clamp(-height * k, 0, 1)` is a perfectly ordinary way to write this and it
   * is the exact shape that shipped the seam: at any k large enough to look
   * like a horizon it is fully dark before the sea has finished.
   */
  it("does not go back to a clamp that saturates in the first few degrees", () => {
    assert.ok(
      !/clamp\(\s*- ?height \* [\d.]+/.test(source),
      "the below-horizon term is a clamp again; see this suite's header",
    );
  });
});

/**
 * Not a shader assertion, and here because this file is where the trap is
 * documented: the sky shader is a **template literal**, and a backtick anywhere
 * inside it — including in a comment quoting a filename — terminates it and
 * turns the rest of the GLSL into JavaScript. It has cost this repo three
 * builds. `tsc` does catch it, so this is a faster and clearer message rather
 * than a new guarantee.
 */
describe("the sky shader's source text", () => {
  it("carries no backtick inside the shader template literals", () => {
    const shaders = [...source.matchAll(/(vertexShader|fragmentShader): `([\s\S]*?)`,\n/g)];
    assert.ok(shaders.length >= 2, "the sky shaders are no longer inline template literals");
    for (const [, which, body] of shaders) {
      assert.ok(!(body ?? "").includes("`"), `a backtick inside ${which} ends the literal early`);
    }
  });
});
