import { describe, expect, it } from 'vitest';
import { Camera, DUSK_PEAK, Lighting, tileToWorld } from '@rpg/engine';
import { GameLighting, type HeroLampDef } from '../Lighting';
import type { AreaDef } from '../../data/locations';

const LAMP: HeroLampDef = { color: 0xf2b45a, radius: 2.5, intensity: 0.5, flicker: 0.12 };

function fakeArea(sources?: AreaDef['lighting'], hazards?: AreaDef['hazards']): AreaDef {
    return {
        id: 'meadows', // AreaId — юнион; значение не важно, свет приходит из fakeArea-аргумента
        name: 'Тест',
        spawn: { x: 1, y: 1 },
        npcs: [],
        enemies: [],
        transitions: [],
        atmosphere: 'none',
        lighting: sources,
        hazards
    };
}

function makeGameLighting(
    area: AreaDef,
    opts?: { flags?: Set<string>; hero?: { x: number; y: number }; lamp?: HeroLampDef | null; time?: number }
) {
    const engineLighting = new Lighting({ width: 480, height: 270 });
    const camera = new Camera(480, 270);
    camera.snap(10, 10);
    const flags = opts?.flags ?? new Set<string>();
    let hero = opts?.hero ?? { x: 5, y: 5 };
    const clock = { hours: opts?.time ?? 12 };
    const gl = new GameLighting({
        area,
        lighting: engineLighting,
        layerTilePos: new Map([[11, [{ x: 2, y: 3 }, { x: 8, y: 3 }]]]), // TILES.HOUSE = 11
        camera,
        heroPos: () => hero,
        hasFlag: (f) => flags.has(f),
        timeHours: () => clock.hours,
        lamp: opts?.lamp === undefined ? null : opts.lamp
    });
    return { gl, engineLighting, camera, setHero: (p: { x: number; y: number }) => (hero = p), setTime: (h: number) => (clock.hours = h) };
}

describe('GameLighting: статика области', () => {
    it('ambient из данных применяется к движковому слою', () => {
        const { gl, engineLighting } = makeGameLighting(fakeArea({ ambient: 0x54586a }));
        expect(engineLighting.ambientColor).toBe(0x54586a);
        gl.destroy();
    });

    it('деф с at — один источник в центре ромба', () => {
        const { gl, engineLighting, camera } = makeGameLighting(
            fakeArea({ sources: [{ id: 'hearth', at: { x: 8, y: 0 }, intensity: 1, flicker: 0 }] })
        );
        gl.update();
        const frames = engineLighting.frames();
        expect(frames.length).toBe(1);
        expect(frames[0].id).toBe('hearth');
        // Позиция — ровно проекция центра ромба тайла (8.5, 0.5)
        const w = tileToWorld(8.5, 0.5);
        const s = camera.toScreen(w.x, w.y);
        expect(frames[0].x).toBeCloseTo(s.x, 3);
        expect(frames[0].y).toBeCloseTo(s.y, 3);
        gl.destroy();
    });

    it('деф с tiles — источник на каждый тайл id', () => {
        const { gl, engineLighting } = makeGameLighting(
            fakeArea({ sources: [{ id: 'window', tiles: [11], intensity: 0.4, flicker: 0 }] })
        );
        gl.update();
        const ids = engineLighting.frames().map((f) => f.id).sort();
        expect(ids).toEqual(['window@2,3', 'window@8,3']);
        gl.destroy();
    });

    it('whenFlag/notFlag управляют активностью', () => {
        const { gl, engineLighting } = makeGameLighting(
            fakeArea({
                sources: [
                    { id: 'off', at: { x: 1, y: 1 }, whenFlag: 'quest_bells_done', flicker: 0 },
                    { id: 'on', at: { x: 1, y: 1 }, notFlag: 'quest_bells_done', flicker: 0 }
                ]
            })
        );
        gl.update();
        expect(engineLighting.frames().map((f) => f.id)).toEqual(['on']);
        gl.destroy();
    });

    it('погасшие источники снимаются с движкового слоя (флаг-условие)', () => {
        const flags = new Set<string>();
        const { gl, engineLighting } = makeGameLighting(
            fakeArea({ sources: [{ id: 'a', at: { x: 1, y: 1 }, flicker: 0, notFlag: 'lit' }] }),
            { flags }
        );
        gl.update();
        expect(engineLighting.hasLight('a')).toBe(true);
        flags.add('lit');
        gl.update();
        expect(engineLighting.hasLight('a')).toBe(false);
        gl.destroy();
    });
});

describe('GameLighting: лампа героя', () => {
    it('следует за героем, id стабилен (ночь на светлой улице)', () => {
        const { gl, engineLighting, setHero } = makeGameLighting(fakeArea({ ambient: 0xd8dade }), {
            time: 21,
            lamp: LAMP
        });
        gl.update();
        const first = engineLighting.frames()[0];
        setHero({ x: 7.5, y: 9.5 });
        gl.update();
        const second = engineLighting.frames()[0];
        expect(first.id).toBe('lamp');
        expect(second.id).toBe('lamp');
        expect(second.x).not.toBe(first.x); // лампа переехала вместе с героем
        gl.destroy();
    });

    it('радиус в юнитах переводится в px', () => {
        const { gl, engineLighting } = makeGameLighting(fakeArea({ ambient: 0x54586a }), { lamp: LAMP });
        gl.update();
        // unitsToPx(2.5) = 2.5 * 32 = 80; scale = 80 / GLOW_BASE_PX(32) = 2.5
        expect(engineLighting.frames()[0].scale).toBeCloseTo(2.5, 6);
        gl.destroy();
    });

    it('на светлой улице днём лампы нет, ночью появляется', () => {
        const { gl, engineLighting, setTime } = makeGameLighting(fakeArea({ ambient: 0xd8dade }), {
            time: 12,
            lamp: LAMP
        });
        gl.update();
        expect(engineLighting.hasLight('lamp')).toBe(false);
        setTime(21);
        gl.update();
        expect(engineLighting.hasLight('lamp')).toBe(true);
        gl.destroy();
    });
});

describe('GameLighting: день/ночь по времени', () => {
    it('цель ambient — микс день/ночь по кривой суток', () => {
        const { gl, engineLighting, setTime } = makeGameLighting(
            fakeArea({ ambient: 0xd8dade, nightAmbient: 0x6a7288 }),
            { time: 12 }
        );
        gl.update();
        expect(engineLighting.ambientColor).toBe(0xd8dade); // день
        setTime(19); // k = 0.5 — середина заката
        gl.update();
        expect(engineLighting.ambientColor).not.toBe(0xd8dade); // микс, не день
        expect(engineLighting.ambientColor).not.toBe(0x6a7288); // и не ночь
        setTime(21);
        gl.update();
        expect(engineLighting.ambientColor).toBe(0x6a7288); // ночь
        setTime(10);
        gl.update();
        expect(engineLighting.ambientColor).toBe(0xd8dade); // и обратно к дню
        gl.destroy();
    });

    it('без nightAmbient в данных время не трогает ambient', () => {
        const { gl, engineLighting } = makeGameLighting(fakeArea({ ambient: 0xd8dade }), { time: 21 });
        gl.update();
        expect(engineLighting.ambientColor).toBe(0xd8dade);
        gl.destroy();
    });

    it('золотой час: на пике заката ambient — закатный тон (свой в данных)', () => {
        const { gl, engineLighting, setTime } = makeGameLighting(
            fakeArea({ ambient: 0xf7f4ea, nightAmbient: 0x3e4658, duskColor: 0xe8a060 }),
            { time: 18 + 2 * DUSK_PEAK } // k = DUSK_PEAK
        );
        gl.update();
        expect(engineLighting.ambientColor).toBe(0xe8a060);
        setTime(12); // день — обратно из заката
        gl.update();
        expect(engineLighting.ambientColor).toBe(0xf7f4ea);
        gl.destroy();
    });

    it('конструктор сразу применяет ночной микс (без мелькания дня)', () => {
        const { gl, engineLighting } = makeGameLighting(
            fakeArea({ ambient: 0xd8dade, nightAmbient: 0x6a7288 }),
            { time: 23 }
        );
        expect(engineLighting.ambientColor).toBe(0x6a7288);
        gl.destroy();
    });

    it('whenNight: ночные источники горят ночью, гаснут днём', () => {
        const { gl, engineLighting, setTime } = makeGameLighting(
            fakeArea({ sources: [{ id: 'win', at: { x: 1, y: 1 }, flicker: 0, whenNight: true }] }),
            { time: 21 }
        );
        gl.update();
        expect(engineLighting.hasLight('win')).toBe(true);
        setTime(12);
        gl.update();
        expect(engineLighting.hasLight('win')).toBe(false);
        gl.destroy();
    });
});

describe('GameLighting: тёмные ауры хазардов', () => {
    it('пятно на каждый тайл зоны, снятие при исчезновении зоны', () => {
        const hazards = [{ name: 'накат', tiles: [{ x: 3, y: 4 }, { x: 4, y: 4 }], requiresItem: 'cloth' as const }];
        const { gl, engineLighting } = makeGameLighting(fakeArea(undefined, hazards));
        gl.update();
        expect(engineLighting.hasDarkSpot('hazard@3,4')).toBe(true);
        expect(engineLighting.hasDarkSpot('hazard@4,4')).toBe(true);
        gl.destroy();
        // Пятна сняты destroy
        expect(engineLighting.hasDarkSpot('hazard@3,4')).toBe(false);
    });
});

describe('GameLighting: runtime API и снапшот', () => {
    it('setAmbient пробрасывается в движковый слой с лерпом', () => {
        const { gl, engineLighting } = makeGameLighting(fakeArea({ ambient: 0xffffff }));
        gl.setAmbient(0x000000, 1);
        engineLighting.update(0.5);
        expect(engineLighting.ambientColor).toBe(0x000000);
        expect(engineLighting.children[0].tint).not.toBe(0xffffff);
        expect(engineLighting.children[0].tint).not.toBe(0x000000);
        gl.destroy();
    });

    it('snapshot: ambient + виньетка + источники с пост-мерцанием', () => {
        const { gl } = makeGameLighting(
            fakeArea({ ambient: 0x334455, sources: [{ id: 'h', at: { x: 1, y: 1 }, intensity: 0.8, flicker: 0 }] })
        );
        gl.update();
        gl.setVignette(0.35);
        const s = gl.snapshot();
        expect(s.ambient).toBe(0x334455);
        expect(s.vignette).toBe(0.35);
        expect(s.sources.length).toBe(1);
        expect(s.sources[0].id).toBe('h');
        expect(s.sources[0].intensity).toBeCloseTo(0.8, 3);
        expect(Number.isFinite(s.sources[0].x)).toBe(true);
        gl.destroy();
    });

    it('destroy снимает все источники', () => {
        const { gl, engineLighting } = makeGameLighting(fakeArea(), { lamp: LAMP });
        gl.update();
        gl.destroy();
        expect(engineLighting.frames().length).toBe(0);
    });
});