// Phase 0 gate — the determinism spine.
//
// These are cheap now and impossible to retrofit later: every phase after this
// one assumes that frame N is a pure function of (seed, params, frame index).

import { check, expect, expectBelow } from './framework.js';
import { Engine } from '../engine/Engine.js';
import { frameDistance, downsample } from '../engine/hash.js';
import { Rng } from '../engine/rng.js';
import { nebula } from '../scenes/shader/nebula.js';
import { defaultValues } from '../params/schema.js';

const TEST_PALETTE = [
    [0.05, 0.02, 0.15],
    [0.85, 0.15, 0.55],
    [0.15, 0.75, 0.95],
    [0.98, 0.85, 0.35],
];

function makeEngine(width = 320, height = 180) {
    const engine = new Engine({ width, height });
    engine.timeline.setDuration(60);
    engine.setLayerSpecs([{
        module: nebula,
        params: defaultValues(nebula),
        seed: 12345,
        opacity: 1,
        blend: 'normal',
        palette: TEST_PALETTE,
    }]);
    return engine;
}

check(0, 'repeat run produces identical frames', () => {
    const engine = makeEngine();
    try {
        const a = engine.hashRun(0, 300);
        const b = engine.hashRun(0, 300);
        const mismatches = a.filter((h, i) => h !== b[i]).length;
        return expect(mismatches === 0, `${mismatches}/300 frames differed between runs`);
    } finally {
        engine.dispose();
    }
});

check(0, 'fresh engine reproduces the same frames', () => {
    // A second Engine instance must agree with the first: proves nothing is
    // carried in module-level or GPU state between constructions.
    const a = makeEngine();
    const b = makeEngine();
    try {
        const ha = a.hashRun(0, 60);
        const hb = b.hashRun(0, 60);
        const mismatches = ha.filter((h, i) => h !== hb[i]).length;
        return expect(mismatches === 0, `${mismatches}/60 frames differed between engines`);
    } finally {
        a.dispose(); b.dispose();
    }
});

check(0, 'simulated dropped frames change nothing', () => {
    // Renders the same frame indices but with irregular gaps in between, the way
    // a stuttering browser would. Because dt is fixed and features are indexed by
    // frame, output must be byte-identical to the smooth run.
    const engine = makeEngine();
    try {
        const smooth = engine.hashRun(0, 120);

        engine.compositor.reset();
        const stuttered = [];
        for (let i = 0; i < 120; i++) {
            // Burn some GPU work between frames without advancing the timeline.
            if (i % 7 === 0) engine.renderFrame(i);
            const target = engine.renderFrame(i);
            stuttered.push(engine.hashCurrent(target));
        }
        const mismatches = smooth.filter((h, i) => h !== stuttered[i]).length;
        return expect(mismatches === 0, `${mismatches}/120 frames differed under simulated stutter`);
    } finally {
        engine.dispose();
    }
});

check(0, 'seek equals sequential playback', () => {
    const engine = makeEngine();
    try {
        const sequential = engine.hashRun(0, 100);
        engine.compositor.reset();
        const direct = engine.hashCurrent(engine.renderFrame(99));
        // With feedback off (Phase 0 default) a direct seek must match exactly.
        return expect(direct === sequential[99],
            `seek→99 ${direct} vs sequential ${sequential[99]}`);
    } finally {
        engine.dispose();
    }
});

check(0, 'resolution independence (320x180 vs 1280x720)', () => {
    const small = new Engine({ width: 320, height: 180 });
    const large = new Engine({ width: 1280, height: 720 });
    try {
        for (const e of [small, large]) {
            e.timeline.setDuration(60);
            e.setLayerSpecs([{
                module: nebula, params: defaultValues(nebula), seed: 12345,
                opacity: 1, blend: 'normal', palette: TEST_PALETTE,
            }]);
        }
        const frames = [10, 90, 200];
        let worst = 0;
        for (const f of frames) {
            small.compositor.reset(); large.compositor.reset();
            const st = small.renderFrame(f);
            const lt = large.renderFrame(f);
            const sp = Uint8Array.from(small.readPixels(st));
            const lp = Uint8Array.from(large.readPixels(lt));
            const reduced = downsample(lp, 1280, 720, 4);
            worst = Math.max(worst, frameDistance(sp, reduced.pixels));
        }
        // 4x downsampling of a noise-bearing shader will never be exact; this
        // threshold catches genuine pixel-space dependence, not filtering error.
        return expectBelow(worst, 0.06, 'worst mean channel distance');
    } finally {
        small.dispose(); large.dispose();
    }
});

check(0, 'seeded rng is reproducible and independent per stream', () => {
    const a = new Rng(42);
    const b = new Rng(42);
    const seqA = Array.from({ length: 500 }, () => a.next());
    const seqB = Array.from({ length: 500 }, () => b.next());
    if (seqA.some((v, i) => v !== seqB[i])) return expect(false, 'same seed diverged');

    const parent = new Rng(7);
    const c1 = parent.fork('layer:0').next();
    const parent2 = new Rng(7);
    const c2 = parent2.fork('layer:0').next();
    const c3 = parent2.fork('layer:1').next();
    if (c1 !== c2) return expect(false, 'fork(label) not stable');
    if (c1 === c3) return expect(false, 'different fork labels collided');

    const inRange = seqA.every((v) => v >= 0 && v < 1);
    return expect(inRange, `500 draws reproducible, forks independent, range ok=${inRange}`);
});

check(0, 'compositor reset clears all history', () => {
    const engine = makeEngine();
    try {
        engine.compositor.setFeedback({ amount: 0.8, decay: 0.95 });
        engine.hashRun(0, 30);
        const afterHistory = engine.hashRun(0, 30);   // hashRun resets first
        const fresh = makeEngine();
        fresh.compositor.setFeedback({ amount: 0.8, decay: 0.95 });
        const freshHashes = fresh.hashRun(0, 30);
        fresh.dispose();
        const mismatches = afterHistory.filter((h, i) => h !== freshHashes[i]).length;
        return expect(mismatches === 0,
            `${mismatches}/30 frames differed after reset — feedback history leaked`);
    } finally {
        engine.dispose();
    }
});
