/**
 * The board cache: what stays in memory, and what gets thrown away first.
 *
 * This is the eviction policy behind the thing the owner actually feels. A board
 * switch used to cover the screen with an opaque card for 1,711 / 1,079 / 997 ms
 * while only 604 / 372 / 222 ms of it blocked the main thread — so two thirds of
 * every pause was a live page with nothing to draw, because `mountCity` disposed
 * the outgoing board before the incoming one existed. Keeping it is the fix, and
 * keeping it means deciding what may be kept.
 *
 * The properties worth a test are the ones whose failure is silent. An eviction
 * that frees the board on screen is a black canvas. An eviction that frees a
 * board and forgets to hand it back is a leak of a whole scene graph, because
 * `environmentRig`'s ledger is a strong reference and only the caller can call
 * `release`. A `put` that quietly drops an existing record is the same leak with
 * a different cause. None of those throw.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createBoardCache, residentCapacity } from "../engine/boards.ts";

interface Board {
  id: string;
}

const board = (id: string): Board => ({ id });

describe("resident capacity is a property of the device and nothing else", () => {
  it("keeps every board on a desktop and two on a handheld", () => {
    // Three is every board this build has, which is what makes the
    // Southland-to-Bay rebuild disappear as well as the return to California.
    assert.equal(residentCapacity(false), 3);
    // `deviceProfile()` changes only the pixel ratio and the shadow map, so a
    // phone carries exactly the same resident geometry as a laptop against a
    // much smaller WebGL budget. Two is California plus whichever metro is on
    // screen — provably enough, because the two metro rectangles do not
    // intersect and California contains both.
    assert.equal(residentCapacity(true), 2);
  });
});

describe("the cache keeps what it is told to keep", () => {
  it("returns a stored board without rebuilding anything", () => {
    const cache = createBoardCache<Board>({ capacity: 3 });
    const sf = board("sf");
    assert.deepEqual(cache.put("sf", sf), []);
    assert.equal(cache.get("sf"), sf);
    assert.equal(cache.has("sf"), true);
    assert.equal(cache.get("socal"), null);
    assert.equal(cache.size(), 1);
  });

  it("never evicts a pinned board, even past its ceiling", () => {
    // California is pinned because it is the cheapest board (5.86 MB of GPU
    // buffers against the Bay Area's 14.12) and is the root of every descent.
    const cache = createBoardCache<Board>({ capacity: 2, pinned: ["california"] });
    cache.put("california", board("california"));
    cache.put("sf", board("sf"));
    const evicted = cache.put("socal", board("socal"));
    assert.deepEqual(evicted.map((b) => b.id), ["sf"]);
    assert.equal(cache.has("california"), true);
    assert.equal(cache.has("socal"), true);
  });

  it("evicts the least recently shown, not the least recently built", () => {
    const cache = createBoardCache<Board>({ capacity: 2 });
    cache.put("california", board("california"));
    cache.put("sf", board("sf"));
    // Going back to California makes the Bay Area the stale one.
    cache.touch("california");
    const evicted = cache.put("socal", board("socal"));
    assert.deepEqual(evicted.map((b) => b.id), ["sf"]);
    assert.deepEqual(cache.ids(), ["california", "socal"]);
  });

  it("never evicts the board being put in", () => {
    // The failure this rules out is a black canvas: the incoming board is the
    // one about to go on the stage, and a capacity of one would otherwise make
    // it its own victim.
    const cache = createBoardCache<Board>({ capacity: 1 });
    cache.put("california", board("california"));
    const sf = board("sf");
    const evicted = cache.put("sf", sf);
    assert.deepEqual(evicted.map((b) => b.id), ["california"]);
    assert.equal(cache.get("sf"), sf);
  });

  it("hands back everything it drops, exactly once", () => {
    // `environmentRig.release(scene)` and `SceneHandle.dispose()` are the
    // caller's to run, and it can only run them on a record it is given. A
    // dropped record that is never returned is the whole scene graph retained.
    const cache = createBoardCache<Board>({ capacity: 2 });
    const dropped: string[] = [];
    for (const id of ["a", "b", "c", "d", "e"]) {
      for (const victim of cache.put(id, board(id))) dropped.push(victim.id);
    }
    assert.deepEqual(dropped, ["a", "b", "c"]);
    assert.deepEqual(cache.ids(), ["d", "e"]);
  });

  it("hands back a record it is replacing in place", () => {
    const cache = createBoardCache<Board>({ capacity: 3 });
    const first = board("sf");
    const second = board("sf");
    cache.put("sf", first);
    assert.deepEqual(cache.put("sf", second), [first]);
    assert.equal(cache.get("sf"), second);
    assert.equal(cache.size(), 1);
  });

  it("does not report a board as evicted when it is put back unchanged", () => {
    const cache = createBoardCache<Board>({ capacity: 3 });
    const sf = board("sf");
    cache.put("sf", sf);
    assert.deepEqual(cache.put("sf", sf), []);
    assert.equal(cache.get("sf"), sf);
  });

  it("takes one out on request and leaves the rest alone", () => {
    const cache = createBoardCache<Board>({ capacity: 3 });
    const sf = board("sf");
    cache.put("california", board("california"));
    cache.put("sf", sf);
    assert.equal(cache.remove("sf"), sf);
    assert.equal(cache.remove("sf"), null);
    assert.deepEqual(cache.ids(), ["california"]);
  });

  it("drains everything, including the pinned board", () => {
    const cache = createBoardCache<Board>({ capacity: 3, pinned: ["california"] });
    cache.put("california", board("california"));
    cache.put("sf", board("sf"));
    assert.deepEqual(cache.drain().map((b) => b.id), ["california", "sf"]);
    assert.equal(cache.size(), 0);
  });

  it("treats a capacity below one as one rather than throwing", () => {
    const cache = createBoardCache<Board>({ capacity: 0 });
    const sf = board("sf");
    assert.deepEqual(cache.put("sf", sf), []);
    assert.equal(cache.get("sf"), sf);
  });
});

describe("the two-deep case the product actually runs", () => {
  it("never makes the board on screen the victim, whatever the order", () => {
    /*
     * The bug this is here for was live and is the worst shape retention can
     * fail in. With a ceiling of two and California pinned, inserting the
     * *incoming* board while the outgoing one was still on the stage left the
     * outgoing board as the only legal victim — so a Bay-to-Southland switch on
     * a phone disposed the picture the visitor was looking at, blanked the
     * canvas, and only then swapped. `main.ts` fixes it by writing the cache
     * after `stage.setScene`, which makes the least-recently-*shown* board the
     * victim; this asserts the property the cache itself guarantees, which is
     * that the id being put is never evicted.
     */
    const cache = createBoardCache<Board>({
      capacity: residentCapacity(true),
      pinned: ["california"],
    });
    cache.put("california", board("california"));
    const sf = board("sf");
    cache.put("sf", sf);

    // The order `main.ts` now uses: the incoming board is shown first, so it is
    // the id being put and the outgoing one is the stale entry.
    const socal = board("socal");
    const evicted = cache.put("socal", socal);
    assert.deepEqual(evicted, [sf], "the board that has just been hidden is the victim");
    assert.equal(cache.get("socal"), socal, "and the one on screen is still resident");
  });

  it("rebuilds nothing on a Bay -> Southland -> Bay round trip on a handheld", () => {
    // California pinned, one metro slot. Coming back to the Bay Area rebuilds
    // it — that is the honest cost of two-deep on a phone — but coming back to
    // California, which is the root of every descent, never does.
    const cache = createBoardCache<Board>({
      capacity: residentCapacity(true),
      pinned: ["california"],
    });
    const california = board("california");
    cache.put("california", california);

    cache.put("sf", board("sf"));
    const droppedForSocal = cache.put("socal", board("socal"));
    assert.deepEqual(droppedForSocal.map((b) => b.id), ["sf"]);

    // Back to California: resident, so nothing is built and nothing is dropped.
    assert.equal(cache.get("california"), california);
    cache.touch("california");
    assert.equal(cache.has("socal"), true);
  });

  it("rebuilds nothing at all on a desktop, in any order", () => {
    const cache = createBoardCache<Board>({
      capacity: residentCapacity(false),
      pinned: ["california"],
    });
    const records = new Map<string, Board>();
    let rebuilt = 0;
    const visit = (id: string) => {
      if (cache.has(id)) {
        cache.touch(id);
        return;
      }
      rebuilt++;
      const record = board(id);
      records.set(id, record);
      assert.deepEqual(cache.put(id, record), []);
    };
    for (const id of ["california", "sf", "socal", "california", "sf", "socal", "sf"]) visit(id);
    assert.equal(rebuilt, 3, "three boards exist, so three builds and no more");
    for (const [id, record] of records) assert.equal(cache.get(id), record);
  });
});
