import { describe, expect, it } from 'vitest';
import {
    DEFAULT_DAY_NIGHT,
    DEFAULT_DUSK_COLOR,
    DUSK_PEAK,
    GLOW_BASE_PX,
    dayNightAmbient,
    dayNightFactor,
    dimColor,
    flickerFactor,
    lightFrame,
    lerpAmbient,
    pulseEnvelope,
    pulseTotal
} from '../lightSim';

describe('flickerFactor', () => {
    it('детерминирован: одинаковые (time, seed) — одинаковый результат', () => {
        const a = flickerFactor(1.234, 0.37, 0.3, 0.9);
        const b = flickerFactor(1.234, 0.37, 0.3, 0.9);
        expect(a).toBe(b);
    });

    it('разные seed дают разные значения', () => {
        const a = flickerFactor(0.5, 0.1, 0.3, 0.9);
        const b = flickerFactor(0.5, 0.9, 0.3, 0.9);
        expect(a).not.toBe(b);
    });

    it('в диапазоне [1-amount, 1] на сетке времени', () => {
        for (let i = 0; i < 180; i++) {
            const f = flickerFactor(i / 60, 0.25, 0.35, 0.9);
            expect(f).toBeGreaterThanOrEqual(1 - 0.35 - 1e-9);
            expect(f).toBeLessThanOrEqual(1 + 1e-9);
        }
    });

    it('amount = 0 — ровный свет', () => {
        for (let i = 0; i < 60; i++) {
            expect(flickerFactor(i / 60, 0.7, 0, 0.9)).toBe(1);
        }
    });
});

describe('lightFrame', () => {
    it('дефолты: intensity 1, radius 2*GLOW_BASE_PX', () => {
        const f = lightFrame({ id: 'l', x: 10, y: 20, color: 0xf2b45a }, 0);
        expect(f.tint).toBe(0xf2b45a);
        expect(f.scale).toBe(2);
        expect(f.alpha).toBeGreaterThan(0);
        expect(f.alpha).toBeLessThanOrEqual(1);
        expect(f.x).toBe(10);
        expect(f.y).toBe(20);
    });

    it('alpha = intensity * flickerFactor, scale = radius / GLOW_BASE_PX', () => {
        const def = { id: 'l', x: 0, y: 0, color: 0xffaa00, intensity: 1.5, radius: 64, flicker: 0, seed: 0 };
        const f = lightFrame(def, 1);
        expect(f.alpha).toBeCloseTo(1.5, 10);
        expect(f.scale).toBe(64 / GLOW_BASE_PX);
        // alpha клампится в адаптере, но чистый кадр может превышать 1
        expect(f.alpha).toBe(1.5);
    });

    it('выключенный источник — alpha 0', () => {
        const f = lightFrame({ id: 'l', x: 0, y: 0, color: 0xffffff, enabled: false }, 0);
        expect(f.alpha).toBe(0);
    });

    it('мерцание меняет alpha между шагами', () => {
        const def = { id: 'l', x: 0, y: 0, color: 0xffffff, intensity: 1, flicker: 0.5 };
        const a1 = lightFrame(def, 0).alpha;
        const a2 = lightFrame(def, 0.37).alpha;
        expect(a1).not.toBe(a2);
    });
});

describe('pulseEnvelope / pulseTotal', () => {
    it('трапеция: рост, плато, спад, нули за пределами', () => {
        const spec = { attack: 0.1, hold: 0.2, decay: 0.3 };
        expect(pulseEnvelope(0, spec)).toBe(0);
        expect(pulseEnvelope(0.05, spec)).toBeCloseTo(0.5, 10);
        expect(pulseEnvelope(0.15, spec)).toBe(1);
        expect(pulseEnvelope(0.25, spec)).toBe(1);
        expect(pulseEnvelope(0.45, spec)).toBeCloseTo(0.5, 10);
        expect(pulseEnvelope(0.7, spec)).toBe(0);
        expect(pulseTotal(spec)).toBeCloseTo(0.6, 10);
    });

    it('без hold — пик сразу после attack', () => {
        const spec = { attack: 0.1, decay: 0.2 };
        expect(pulseEnvelope(0.1, spec)).toBe(1);
        expect(pulseEnvelope(0.2, spec)).toBeCloseTo(0.5, 10);
        expect(pulseTotal(spec)).toBeCloseTo(0.3, 10);
    });

    it('attack = 0 — мгновенный пик', () => {
        const spec = { attack: 0, decay: 0.2 };
        expect(pulseEnvelope(0, spec)).toBe(0);
        expect(pulseEnvelope(0.1, spec)).toBeCloseTo(0.5, 10);
        expect(pulseEnvelope(0.2, spec)).toBe(0);
        expect(pulseTotal(spec)).toBeCloseTo(0.2, 10);
    });

    it('отрицательное время и значения вне диапазона — 0', () => {
        const spec = { attack: 0.1, decay: 0.2 };
        expect(pulseEnvelope(-0.5, spec)).toBe(0);
        expect(pulseEnvelope(10, spec)).toBe(0);
    });
});

describe('lerpAmbient', () => {
    it('краи и середина по каналам', () => {
        expect(lerpAmbient(0x000000, 0xffffff, 0)).toBe(0x000000);
        expect(lerpAmbient(0x000000, 0xffffff, 1)).toBe(0xffffff);
        expect(lerpAmbient(0xff0000, 0x00ff00, 0.5)).toBe(0x808000); // round(127.5) = 128
    });
});

describe('dayNightAmbient (золотой час)', () => {
    it('краи: k=0 — день, k=1 — ночь', () => {
        expect(dayNightAmbient(0xf7f4ea, 0x3e4658, 0)).toBe(0xf7f4ea);
        expect(dayNightAmbient(0xf7f4ea, 0x3e4658, 1)).toBe(0x3e4658);
    });

    it('на пике (k = DUSK_PEAK) ambient равен закатному тону', () => {
        expect(dayNightAmbient(0xf7f4ea, 0x3e4658, DUSK_PEAK)).toBe(DEFAULT_DUSK_COLOR);
        expect(dayNightAmbient(0xf7f4ea, 0x3e4658, DUSK_PEAK, 0xffcc88)).toBe(0xffcc88); // кастомный тон
    });

    it('до пика — лерп день→закат, после — закат→ночь', () => {
        expect(dayNightAmbient(0xf7f4ea, 0x3e4658, DUSK_PEAK / 2)).toBe(
            lerpAmbient(0xf7f4ea, DEFAULT_DUSK_COLOR, 0.5)
        );
        expect(dayNightAmbient(0xf7f4ea, 0x3e4658, DUSK_PEAK + (1 - DUSK_PEAK) / 2)).toBe(
            lerpAmbient(DEFAULT_DUSK_COLOR, 0x3e4658, 0.5)
        );
    });

    it('непрерывен и монотонен по k (по каждому каналу)', () => {
        let prevR = 256;
        for (let k = 0; k <= 1.0001; k += 0.01) {
            const c = dayNightAmbient(0xf7f4ea, 0x3e4658, k);
            const r = (c >> 16) & 0xff;
            expect(r).toBeLessThanOrEqual(prevR); // день светлее ночи по R
            prevR = r;
        }
        // стык на пике: значения слева и справа совпадают
        expect(dayNightAmbient(0xf7f4ea, 0x3e4658, DUSK_PEAK - 0.001)).toBeCloseTo(
            dayNightAmbient(0xf7f4ea, 0x3e4658, DUSK_PEAK + 0.001),
            1
        );
    });
});

describe('dimColor', () => {
    it('краи: полная яркость и ноль', () => {
        expect(dimColor(0x54586a, 1)).toBe(0x54586a);
        expect(dimColor(0x54586a, 0)).toBe(0x000000);
    });

    it('монотонность по яркости', () => {
        let prev = 0;
        for (let k = 0; k <= 1.001; k += 0.1) {
            const c = dimColor(0x808080, k);
            expect(c).toBeGreaterThanOrEqual(prev);
            prev = c;
        }
    });
});
describe('dayNightFactor', () => {
    it('день — 0', () => {
        expect(dayNightFactor(8)).toBe(0);
        expect(dayNightFactor(12)).toBe(0);
        expect(dayNightFactor(17.9)).toBe(0);
    });

    it('закат 18–20 — линейный рост', () => {
        expect(dayNightFactor(18)).toBe(0);
        expect(dayNightFactor(19)).toBeCloseTo(0.5, 10);
        expect(dayNightFactor(20)).toBe(1);
    });

    it('ночь — 1', () => {
        expect(dayNightFactor(23)).toBe(1);
        expect(dayNightFactor(0)).toBe(1);
        expect(dayNightFactor(5.9)).toBe(1);
    });

    it('рассвет 6–8 — линейный спад', () => {
        expect(dayNightFactor(6)).toBe(1);
        expect(dayNightFactor(7)).toBeCloseTo(0.5, 10);
        expect(dayNightFactor(8)).toBe(0);
    });

    it('стык 24/0 непрерывен, отрицательные и большие часы ок', () => {
        expect(dayNightFactor(24)).toBe(1);
        expect(dayNightFactor(25)).toBe(dayNightFactor(1));
        expect(dayNightFactor(-4)).toBe(1);
        expect(dayNightFactor(-16)).toBe(0); // -16 ≡ 8
    });

    it('монотонность на переходах', () => {
        for (let t = 18; t < 20; t += 0.1) {
            expect(dayNightFactor(t + 0.1)).toBeGreaterThanOrEqual(dayNightFactor(t));
        }
        for (let t = 6; t < 8; t += 0.1) {
            expect(dayNightFactor(t + 0.1)).toBeLessThanOrEqual(dayNightFactor(t));
        }
    });

    it('кастомный spec', () => {
        const spec = { dawn: 0, day: 1, dusk: 10, night: 12 };
        expect(dayNightFactor(5, spec)).toBe(0);
        expect(dayNightFactor(11, spec)).toBeCloseTo(0.5, 10);
        expect(dayNightFactor(12, spec)).toBe(1);
        expect(DEFAULT_DAY_NIGHT.dusk).toBe(18); // дефолт не тронут
    });
});
