/** Тесты чистой части молнии: огибающая мерцания, геометрия разряда,
 *  жизненный цикл вспышки системы (без WebGL — стаб рендера). */
import { describe, expect, it } from 'vitest';
import {
    LIGHTNING_DURATION, LIGHTNING_FLASH, LIGHTNING_TOP, LightningSystem,
    boltPoints, lightningEnvelope, lightningEnvLight,
} from '../lightning';

describe('lightningEnvLight (подсветка дождя/тумана)', () => {
    it('ноль без вспышки, растёт с k и ночью сильнее', () => {
        expect(lightningEnvLight(0, 0)).toEqual([0, 0, 0]);
        const day = lightningEnvLight(1, 0);
        const night = lightningEnvLight(1, 1);
        expect(day[2]).toBeCloseTo(LIGHTNING_FLASH * 0.2); // доля вспышки в env
        expect(night[2]).toBeCloseTo(day[2] * 2); // ночью ×(1+nightK)
    });
});

describe('lightningEnvelope', () => {
    it('пик в момент удара, всё в 0..1', () => {
        expect(lightningEnvelope(0)).toBeCloseTo(1);
        for (let dt = 0; dt < LIGHTNING_DURATION; dt += 0.01) {
            const e = lightningEnvelope(dt);
            expect(e).toBeGreaterThan(0);
            expect(e).toBeLessThanOrEqual(1);
        }
    });
    it('мерцает (рестрайки): не монотонна', () => {
        // спад главного разряда, затем рестрайк поднимает огибающую снова
        expect(lightningEnvelope(0.18)).toBeGreaterThan(lightningEnvelope(0.12));
    });
    it('после длительности — ноль', () => {
        expect(lightningEnvelope(LIGHTNING_DURATION)).toBe(0);
        expect(lightningEnvelope(LIGHTNING_DURATION + 5)).toBe(0);
        expect(lightningEnvelope(-1)).toBe(0);
    });
});

describe('boltPoints (геометрия разряда)', () => {
    const at: [number, number, number] = [10, 2, -5];
    it('детерминизм: тот же seed — та же геометрия; seed меняет форму', () => {
        expect(boltPoints(7, at)).toEqual(boltPoints(7, at));
        expect(boltPoints(7, at)).not.toEqual(boltPoints(8, at));
    });
    it('канал от земли вверх до LIGHTNING_TOP, снос ограничен', () => {
        const pts = boltPoints(42, at);
        expect(pts.length).toBeGreaterThanOrEqual(16);
        for (const p of pts) {
            expect(p.pos[1]).toBeGreaterThanOrEqual(at[1]);
            expect(p.pos[1]).toBeLessThanOrEqual(at[1] + LIGHTNING_TOP);
            // зигзаг — сумма синусов (ограничен) плюс ветвь — узкий коридор
            // вокруг точки удара
            expect(Math.abs(p.pos[0] - at[0])).toBeLessThan(LIGHTNING_TOP * 0.12);
            expect(Math.abs(p.pos[2] - at[2])).toBeLessThan(LIGHTNING_TOP * 0.12);
        }
   });
    it('есть ветви (более бледные и тонкие) и узлы полной яркости', () => {
        const pts = boltPoints(42, at);
        expect(pts.some((p) => p.alpha < 1)).toBe(true); // ветви
        expect(pts.some((p) => p.alpha === 1)).toBe(true); // канал
    });
});

describe('LightningSystem (без WebGL — стаб рендера)', () => {
    /** Стаб рендера: свет и окно записываются, точки слоя копятся. */
    function stub() {
        const flashes: Array<{ at: readonly [number, number, number] | null; k: number }> = [];
        return {
            flashes,
            renderer: {
                addParticles: () => ({ update: () => {}, setTint: () => {}, dispose: () => {} }),
                setLightning: (at: readonly [number, number, number] | null, k: number) => flashes.push({ at, k }),
                visibleWindow: () => ({ minX: 0, maxX: 48, minZ: 0, maxZ: 32 }),
                focusPoint: () => [20, 1, 14] as [number, number, number],
            } as never,
        };
    }
    it('strike без точки — случайная точка окна; вспышка стартует сразу', () => {
        const s = stub();
        const sys = new LightningSystem(s.renderer);
        sys.strike(); // первый кадр вспышки анимируется сразу
        const info = sys.info();
        expect(info.active).toBe(true);
        expect(info.strikes).toBe(1);
        expect(info.flash).toBeGreaterThan(0);
        expect(info.at![0]).toBeGreaterThanOrEqual(0);
        expect(info.at![0]).toBeLessThanOrEqual(48);
        expect(s.flashes.at(-1)!.k).toBeGreaterThan(0);
    });
    it('по истечении длительности вспышка гаснет (свет выключен, слой пуст)', () => {
        const s = stub();
        const sys = new LightningSystem(s.renderer);
        sys.strike([5, 0, 5]);
        sys.update(LIGHTNING_DURATION + 0.1);
        const info = sys.info();
        expect(info.active).toBe(false);
        expect(info.flash).toBe(0);
        expect(s.flashes.at(-1)).toEqual({ at: null, k: 0 });
    });
    it('детерминизм удара: тот же номер — та же точка; новый удар — другая', () => {
        const a = stub();
        const b = stub();
        const sysA = new LightningSystem(a.renderer);
        const sysB = new LightningSystem(b.renderer);
        sysA.strike();
        sysB.strike();
        expect(sysA.info().at).toEqual(sysB.info().at); // номер удара 1 у обоих
        const first = sysA.info().at;
        sysA.strike(); // удар 2 — реролл точки от номера удара
        expect(sysA.info().at).not.toEqual(first);
    });
    it('dispose посреди вспышки гасит свет и снапшот (свет не замирает)', () => {
        const s = stub();
        const sys = new LightningSystem(s.renderer);
        sys.strike([5, 0, 5]); // вспышка активна
        expect(sys.info().active).toBe(true);
        sys.dispose();
        expect(s.flashes.at(-1)).toEqual({ at: null, k: 0 }); // свет погашен
        expect(sys.info()).toMatchObject({ active: false, flash: 0 });
    });
});