// The per-scene acceptance battery, runnable for ONE scene.
//
// The library-wide gates iterate the registry, so a new scene is covered the
// moment it is registered — but running them means rendering all thirty-six
// scenes and reading a page of results to find out whether the one you just
// wrote is alive. That is slow to run and expensive to read, and it is the loop
// you are in constantly while writing a scene.
//
// This runs the same acceptance criteria against a single scene and prints one
// line per criterion plus a single verdict. Open:
//
//   checks.html?scene=Aurora%20Veil
//
// The criteria are deliberately the same ones Phase 2, 5 and 7 apply — this is
// a filter over the existing gates, not a second, weaker set of them.

import { Engine } from '../engine/Engine.js';
import { sceneByName, scenes } from '../scenes/registry.js';
import { defaultValues, sampleValues, sweepValues, validateModule } from '../params/schema.js';
import { Rng } from '../engine/rng.js';
import { FeatureTrack, featureProviderFor } from '../audio/FeatureTrack.js';
import { synthesizeSectioned } from '../audio/synth.js';
import { frameLuminance, frameVariance, frameMaxDelta } from '../engine/hash.js';
import { peakFlashRate } from '../engine/flash.js';
import { generatePersonality } from '../look/Personality.js';
import { generateIdentity } from '../look/Identity.js';

const PALETTE = [
    [0.06, 0.03, 0.16], [0.85, 0.15, 0.55], [0.15, 0.75, 0.95],
    [0.98, 0.85, 0.35], [0.55, 0.25, 0.85], [0.2, 0.95, 0.6],
];

const SUMMARY = { meanCentroid: 0.5, meanFlatness: 0.25, dynamicRange: 0.5, bpm: 126, meanLoudness: 0.4 };

/**
 * @param {string} name  scene name as registered
 * @returns {{ok: boolean, lines: string[]}}
 */
export function runSceneGate(name) {
    const module = sceneByName(name);
    const lines = [];
    if (!module) {
        return {
            ok: false,
            lines: [`FAIL  no scene named "${name}" — registered: ${scenes.map((m) => m.name).join(', ')}`],
        };
    }

    let ok = true;
    const record = (pass, label, detail) => {
        ok = ok && pass;
        lines.push(`${pass ? 'PASS' : 'FAIL'}  ${label.padEnd(26)} ${detail}`);
    };

    const errors = validateModule(module);
    record(errors.length === 0, 'schema', errors.length ? errors.join(' · ') : 'valid');

    const track = FeatureTrack.fromAudioBuffer(
        synthesizeSectioned({ bpm: 128, duration: 60, changeAt: 30 }), { fps: 60 });

    const engine = new Engine({ width: 192, height: 108 });
    engine.timeline.setDuration(track.duration);
    engine.setFeatureProvider(featureProviderFor(track));

    const personality = generatePersonality(SUMMARY, new Rng(9001));
    const draw = (params, frame, seed = 4242) => {
        engine.setLayerSpecs([{
            module, params, seed, opacity: 1, blend: 'normal', palette: PALETTE, personality,
        }]);
        engine.compositor.reset();
        return Uint8Array.from(engine.readPixels(engine.renderFrame(frame)));
    };

    try {
        // --- alive -------------------------------------------------------
        const base = draw(defaultValues(module), 600);
        const lum = frameLuminance(base);
        const variance = frameVariance(base);
        record(lum > 0.004 && variance > 0.0008, 'renders something',
            `luminance ${lum.toFixed(4)} · variance ${variance.toFixed(4)}`);

        // --- animates ----------------------------------------------------
        const later = draw(defaultValues(module), 600 + 120);
        const motion = frameMaxDelta(base, later);
        record(motion > 3, 'animates', `max channel delta ${motion} over 2s`);

        // --- deterministic -----------------------------------------------
        const again = draw(defaultValues(module), 600);
        const repeat = frameMaxDelta(base, again);
        record(repeat <= 1, 'deterministic', `repeat delta ${repeat}/255`);

        // --- distinct from every other scene -------------------------------
        let closest = 255;
        let closestName = '';
        for (const other of scenes) {
            if (other === module || other.kind !== 'fragment') continue;
            engine.setLayerSpecs([{
                module: other, params: defaultValues(other), seed: 4242,
                opacity: 1, blend: 'normal', palette: PALETTE, personality,
            }]);
            engine.compositor.reset();
            const d = frameMaxDelta(base, Uint8Array.from(engine.readPixels(engine.renderFrame(600))));
            if (d < closest) { closest = d; closestName = other.name; }
        }
        record(closest >= 24, 'distinct', `closest ${closestName} at ${closest} (floor 24)`);

        // --- param sweep ---------------------------------------------------
        const dead = [];
        for (const [pname, def] of Object.entries(module.params || {})) {
            if (def.type === 'palette') continue;
            for (const value of sweepValues(def, 4)) {
                const params = { ...defaultValues(module), [pname]: value };
                const pixels = draw(params, 700);
                const l = frameLuminance(pixels);
                const v = frameVariance(pixels);
                if (!(l > 0.002) || !(v > 0.0002) || l > 0.97) {
                    dead.push(`${pname}=${Array.isArray(value) ? value.join(',') : value}`);
                }
            }
        }
        record(dead.length === 0, 'param sweep',
            dead.length ? `dead/blown at ${dead.slice(0, 4).join(', ')}` : 'all values live');

        // --- flash rate ------------------------------------------------------
        const hot = sampleValues(module, new Rng(77), { energy: 0.95, density: 0.9, motion: 0.9 });
        engine.setLayerSpecs([{
            module, params: hot, seed: 99, opacity: 1, blend: 'normal', palette: PALETTE, personality,
        }]);
        engine.compositor.reset();
        const luminance = [];
        for (let f = 600; f < 900; f++) {
            luminance.push(frameLuminance(engine.readPixels(engine.renderFrame(f))));
        }
        const rate = peakFlashRate(luminance, 60);
        record(rate <= 3, 'flash rate', `${rate}/s at aggressive settings (ceiling 3)`);

        // --- identity response -----------------------------------------------
        // The migration gate. A scene that declares it consumes the cast must
        // produce a DIFFERENT PICTURE when the song's cast changes — otherwise
        // `consumes` is a comment and the whole inversion is unverifiable at
        // library scale. Deliberately a much larger threshold than the trait
        // check: a trait may be honoured subtly, but content is the subject.
        for (const artifact of module.consumes || []) {
            const other = generatePersonality(SUMMARY, new Rng(9001));
            // Two identities as far apart as the generator can make them.
            const alt = generateIdentity(
                { ...SUMMARY, meanFlatness: 0.6, meanCentroid: 0.85, bpm: 168 },
                new Rng(31337), 6);
            // Force the always-visible ink decisions on. A field scene's whole
            // migration may be `inkValue`, and posterisation is off for most
            // identities — without pinning it the probe would sometimes hand the
            // scene two identities that ask it for the same picture and then
            // fail it for complying.
            alt.ink = { ...alt.ink, posterize: 4, fill: 'hatch', outline: 0.8, weight: 0.8 };
            other.identity = alt;
            if (artifact === 'cast') {
                // The protagonist's geometry is read from the signature form.
                other.shape = { sides: 8, roundness: 0.02, elongation: 1.4, tilt: 0.9 };
            }

            engine.setLayerSpecs([{
                module, params: defaultValues(module), seed: 4242,
                opacity: 1, blend: 'normal', palette: PALETTE, personality: other,
            }]);
            engine.compositor.reset();
            const changed = frameMaxDelta(base,
                Uint8Array.from(engine.readPixels(engine.renderFrame(600))));
            record(changed > 24, `consumes: ${artifact}`,
                `delta ${changed}/255 (floor 24)`);
        }

        // --- personality response --------------------------------------------
        // Every declared trait must move the image; a trait declared and ignored
        // gets the scene cast in tracks it cannot express.
        for (const trait of module.traits || []) {
            const other = generatePersonality(SUMMARY, new Rng(9001));
            if (trait === 'shape') other.shape = { sides: 6, roundness: 0.05, elongation: 1.3, tilt: 0.7 };
            if (trait === 'camera') other.camera = { ...other.camera, driftAngle: 1.1, driftRate: 0.06, sway: 0.06, swayRate: 0.2, spin: 0.05, breathe: 0.05 };
            if (trait === 'space') other.space = { horizon: 0.68, depth: 0.9, washAngle: 2.4, wash: 0.5 };
            if (trait === 'style') other.style = { lineWeight: 0.95, softness: 0.9, texture: 0.5, symmetry: 4 };

            engine.setLayerSpecs([{
                module, params: defaultValues(module), seed: 4242,
                opacity: 1, blend: 'normal', palette: PALETTE, personality: other,
            }]);
            engine.compositor.reset();
            const changed = frameMaxDelta(base,
                Uint8Array.from(engine.readPixels(engine.renderFrame(600))));
            record(changed > 1, `trait: ${trait}`, `delta ${changed}/255`);
        }
    } finally {
        engine.dispose();
    }

    return { ok, lines };
}
