// Phase 12 — the SEED VARIETY TEST.
//
// Every other phase asks whether one video is correct. This one asks whether
// two videos are different, which is the failure the rest of the suite is
// structurally unable to see: a generator that ignores its seed passes
// determinism, flash safety, liveness and end-to-end rendering perfectly.
//
// The measurement lives in checks/variety/. The two checks that come first here
// are not about the generator at all — they are about the instrument. A
// structural variety score is worthless unless it can be shown to ignore the
// cheap axes (recolour, rotate) and to react to the expensive one (a different
// scene), so those are gated before any number derived from them is trusted.
//
// The library sweep in the middle is the map the rest is drawn on: the seed can
// only produce as much variety as the library holds, so "how many structurally
// distinct looks are there" is measured before "how many does a seed reach".

import { check, expect } from './framework.js';
import { song } from '../audio/songbank.js';
import { Show } from '../Show.js';
import { generateLook } from '../look/LookGenerator.js';
import { scenes } from '../scenes/registry.js';
import { frameDescriptor, rotate90, recolour } from './variety/descriptors.js';
import { descriptorDistance, signatureDistance } from './variety/signature.js';
import {
    measureVariety, measureSpecDiversity, signatureForScene, librarySweep,
} from './variety/report.js';

// `centre` is the bank's null hypothesis: the middle of every axis, a full
// six-stage arrangement. The gates used to run on a two-section synthetic whose
// only kinds were intro and outro, which left half the scene library unreachable
// and made every number here a measurement of that rather than of the seed.
export const varietyTrack = () => song('centre').track;

// The library sweep is the most expensive thing in the suite — every scene
// rendered — and three checks read it. Computed once per page load.
let cachedSweep = null;
export function librarySweepCached() {
    if (!cachedSweep) cachedSweep = librarySweep(varietyTrack(), { probes: 3 });
    return cachedSweep;
}

/** One frame of one seed, rendered the normal way. */
function sampleFrame(seed = 12001, width = 160, height = 90) {
    const track = varietyTrack();
    const show = new Show({ width, height });
    try {
        show.useTrack(track, generateLook(track, { seed }));
        const section = show.look.sections[Math.floor(show.look.sections.length / 2)];
        const frame = section.startFrame + Math.floor((section.endFrame - section.startFrame) / 2);
        show.engine.compositor.reset();
        for (let f = Math.max(0, frame - 20); f < frame; f++) show.renderFrame(f);
        return Uint8Array.from(show.readPixels(show.renderFrame(frame)));
    } finally {
        show.dispose();
    }
}

check(12, 'seed variety · the metric ignores colour and rotation', () => {
    const w = 160, h = 90;
    const pixels = sampleFrame(12001, w, h);
    const base = frameDescriptor(pixels, w, h);

    // Hue-rotated and brightened: the same picture in a different palette, which
    // is precisely the difference we refuse to count as variety.
    const graded = descriptorDistance(base, frameDescriptor(recolour(pixels), w, h));
    // Turned ninety degrees. Layout is EXCLUDED here by design — where structure
    // sits in the frame is real information, and a metric blind to it could not
    // see a library that centres everything.
    const spun = rotate90(pixels, w, h);
    const turned = descriptorDistance(base, frameDescriptor(spun.pixels, spun.width, spun.height));

    const structural = ['scale', 'orient', 'texture'];
    const worstGrade = Math.max(...structural.concat('layout').map((b) => graded[b]));
    const worstTurn = Math.max(...structural.map((b) => turned[b]));

    return expect(worstGrade < 0.05 && worstTurn < 0.02 && graded.colour > 0.2,
        `recolour moves structure ${worstGrade.toFixed(4)} (colour block ${graded.colour.toFixed(3)}) · ` +
        `rotate moves structure ${worstTurn.toFixed(4)}`);
});

check(12, 'seed variety · the metric separates different scenes from the same scene', () => {
    // The instrument's other half: it must react to the thing that IS a
    // difference. Two renders of one scene must land far below two renders of
    // two scenes, or a low variety score would just be a blind metric.
    const track = varietyTrack();
    const pool = scenes.filter((m) => m.role !== 'accent' && m.kind === 'fragment');
    const a = pool[0], b = pool[Math.floor(pool.length / 2)], c = pool[pool.length - 1];

    const sigA = signatureForScene(track, a, 777, { probes: 2 });
    const sigA2 = signatureForScene(track, a, 777, { probes: 2 });
    const sigB = signatureForScene(track, b, 777, { probes: 2 });
    const sigC = signatureForScene(track, c, 777, { probes: 2 });

    const same = signatureDistance(sigA, sigA2).total;
    const diff = Math.min(
        signatureDistance(sigA, sigB).total,
        signatureDistance(sigA, sigC).total,
        signatureDistance(sigB, sigC).total,
    );

    return expect(same < 0.01 && diff > same * 8,
        `same scene ${same.toFixed(4)} · different scenes ${diff.toFixed(4)} ` +
        `(${a.name} / ${b.name} / ${c.name})`);
}, { slow: true });

check(12, 'seed variety · every visualization in the library is a distinct look', () => {
    // Runs against ALL of them, not a sample. The existing per-scene gate does
    // a version of this on raw pixels, where two scenes that are the same image
    // in different colours pass comfortably; here colour is not counted, so a
    // structural twin is visible as one.
    //
    // Twins are reported rather than failed on a fixed distance: what matters is
    // that the library does not contain a CLUSTER of scenes that are one look
    // wearing several names, because the casting code will happily "vary" a
    // video by rotating between them.
    const sweep = librarySweepCached();
    const bigTwins = sweep.twins.filter((g) => g.length >= 3);

    return expect(bigTwins.length === 0,
        (bigTwins.length ? `structural twin groups: ${bigTwins.map((g) => g.join('≈')).join(' · ')} — ` : '') +
        `${sweep.scenes.length} scenes · median distance ${sweep.median.toFixed(3)} · ` +
        `closest ${sweep.closestPairs[0].name}≈${sweep.closestPairs[0].nearest} ` +
        `at ${sweep.closestPairs[0].distance.toFixed(3)}`);
}, { slow: true });

check(12, 'seed variety · the generator casts a different show for a different seed', () => {
    // Cheap, GPU-free, and the first thing to read when the rendered score
    // drops: this says whether the generator ever DECIDED to make two different
    // videos, before asking whether the pixels came out different.
    const d = measureSpecDiversity(varietyTrack(), { seeds: 24 });
    const problems = [];
    if (d.sceneSetDistance < 0.6) problems.push(`scene sets only ${d.sceneSetDistance.toFixed(2)} apart`);
    if (d.identicalCasts > 0) problems.push(`${d.identicalCasts} seed pairs cast identically`);
    if (d.libraryCoverage < 0.6) {
        problems.push(`only ${(d.libraryCoverage * 100).toFixed(0)}% of the library used ` +
            `(never cast: ${d.uncast.slice(0, 6).join(', ')})`);
    }
    if (d.director.unique < 2) problems.push('one director for every seed');

    return expect(problems.length === 0,
        problems.length ? problems.join(' · ')
            : `cast distance ${d.sceneSetDistance.toFixed(2)} · ` +
              `library ${(d.libraryCoverage * 100).toFixed(0)}% · ` +
              `${d.director.unique} directors · ${d.paletteScheme.unique} schemes · ` +
              `${d.signature.unique} signatures · ${d.grain.unique} grain modes`);
});

check(12, 'seed variety · different seeds render structurally different videos', () => {
    // THE gate. The threshold is a target the generator does not currently meet
    // — a failure here is the known open defect, not a flaky check. The detail
    // line carries the full breakdown so a run can be compared against the last
    // one while the number is being moved.
    const r = measureVariety(varietyTrack(), { seeds: 6, refScenes: 4, probes: 4 });
    const problems = [];

    // Against the floor: two seeds must differ by more than one seed differs
    // from itself across its own sections. Below this the seed is decoration.
    if (r.separation < 0.35) {
        problems.push(`separation ${r.separation.toFixed(2)} (floor ${r.floor.toFixed(3)}, ` +
            `observed ${r.observed.toFixed(3)}, ceiling ${r.ceiling.toFixed(3)})`);
    }
    // No seed may be shadowed by another. A healthy mean hides pairs that are
    // the same video, and a viewer only ever sees the pair.
    const shadowed = r.nearest.filter((d) => d < r.floor * 0.75).length;
    if (shadowed) problems.push(`${shadowed} seeds shadowed by another seed`);

    // Per block: it is not enough for the total to pass on colour-adjacent
    // motion while every frame is composed the same way.
    for (const [name, b] of Object.entries(r.byBlock)) {
        if (name === 'colour') continue;
        if (b.ratio < 0.25) problems.push(`${name} at ${(b.ratio * 100).toFixed(0)}% of achievable`);
    }

    const blocks = Object.entries(r.byBlock)
        .map(([n, b]) => `${n} ${(b.ratio * 100).toFixed(0)}%`).join(' · ');
    return expect(problems.length === 0,
        (problems.length ? problems.join(' · ') + ' — ' : '') +
        `separation ${r.separation.toFixed(2)} · ${blocks}`);
}, { slow: true });
