/** Тесты чистой части погоды: кламп spec'а, вектор ветра, детерминизм и
 *  заворачивание в окно у слоёв дождя/снега, обесцвечивание облачности. */
import { describe, expect, it } from 'vitest';
import { cloudDim } from '../daynight';
import {
    DEFAULT_WEATHER, RAIN_LAYERS, WeatherSystem, clampWeather, rainPoints, snowPoints, windVector,
} from '../weather';

const WIN = { minX: 0, maxX: 48, minZ: 0, maxZ: 32 };

/** Моменты респавна головы капли drop (y подпрыгивает с низа к верху слоя —
 *  обычное падение за шаг скана даёт сдвиг в доли юнита, не прыжок). */
function headWraps(spec: ReturnType<typeof clampWeather>, drop: number, tMax = 8): number[] {
    const wraps: number[] = [];
    let prev = -1;
    for (let t = 0; t <= tMax; t += 0.002) {
        const y = rainPoints(spec, t, WIN)[drop * 3]!.pos[1];
        if (prev >= 0 && y - prev > 20) wraps.push(t);
        prev = y;
    }
    return wraps;
}

describe('clampWeather', () => {
    it('частичный ввод дополняется дефолтом', () => {
        const w = clampWeather({ rain: 0.3 });
        expect(w.rain).toBe(0.3);
        expect(w.snow).toBe(0);
        expect(w.windStrength).toBe(0);
        expect(w.windDir).toBe(DEFAULT_WEATHER.windDir);
        expect(w.cloud).toBe(0);
    });
    it('выходит за 0..1 — клампится; NaN/не-число — дефолт', () => {
        expect(clampWeather({ rain: 5 }).rain).toBe(1);
        expect(clampWeather({ snow: -2 }).snow).toBe(0);
        expect(clampWeather({ windStrength: NaN }).windStrength).toBe(0);
        expect(clampWeather({ cloud: 'x' as unknown as number }).cloud).toBe(0);
    });
});

describe('windVector', () => {
    it('ноль силы — ноль дрейфа; сила 1 — 6 юнит/с по направлению', () => {
        expect(windVector({ ...DEFAULT_WEATHER, windStrength: 0 })).toEqual([0, 0]);
        const [x, z] = windVector({ ...DEFAULT_WEATHER, windStrength: 1, windDir: Math.PI / 2 });
        expect(x).toBeCloseTo(6);
        expect(z).toBeCloseTo(0);
        const [x2, z2] = windVector({ ...DEFAULT_WEATHER, windStrength: 0.5, windDir: 0 });
        expect(x2).toBeCloseTo(0);
        expect(z2).toBeCloseTo(3);
    });
});

describe('rain/snow points', () => {
    // окно 48×32 = 1536 кв. юнитов; дождь многослойный (6 слоёв, плотности
    // 0.020/0.016/0.013/0.011/0.009/0.008 → 31+25+20+17+14+12 = 119 капель
    // × 3 точки штриха = 357); снег (0.010/0.008/0.007/0.006/0.006/0.0045
    // → 15+12+11+9+9+7 = 63 хлопьев)
    it('число точек = сумма слоёв (плотность × площадь × интенсивность); ноль — пусто', () => {
        const spec = { ...DEFAULT_WEATHER, rain: 1, snow: 0 };
        expect(rainPoints(spec, 0, WIN).length).toBe(357);
        expect(snowPoints({ ...spec, snow: 1 }, 0, WIN).length).toBe(63);
        expect(rainPoints({ ...spec, rain: 0 }, 0, WIN).length).toBe(0);
        expect(snowPoints({ ...spec, snow: 0 }, 0, WIN).length).toBe(0);
    });
    it('слои живут в своих полосах глубины и не совпадают колонками', () => {
        const spec = { ...DEFAULT_WEATHER, rain: 1 };
        const a = rainPoints(spec, 0, WIN, 0); // дальний слой
        const b = rainPoints(spec, 0, WIN, RAIN_LAYERS - 1); // ближний
        expect(a.length).toBeGreaterThan(0);
        expect(b.length).toBeGreaterThan(0);
        const sizes = (pts: ReturnType<typeof rainPoints>) => pts.filter((p) => p.size !== undefined).map((p) => p.size!);
        // ближний слой крупнее дальнего (аэроперспектива по слоям)
        expect(Math.max(...sizes(b))).toBeGreaterThan(Math.max(...sizes(a)));
        // колонки слоёв разведены: совпадений позиций нет
        const key = (p: ReturnType<typeof rainPoints>[number]) => `${Math.round(p.pos[0] * 10)}:${Math.round(p.pos[2] * 10)}`;
        const keysB = new Set(b.map(key));
        expect(a.every((p) => !keysB.has(key(p)))).toBe(true);
        // сумма слоёв = вызов без слоя
        const all = rainPoints(spec, 0, WIN);
        let sum = 0;
        for (let k = 0; k < RAIN_LAYERS; k++) sum += rainPoints(spec, 0, WIN, k).length;
        expect(all.length).toBe(sum);
    });
    it('детерминизм: тот же t — те же позы', () => {
        const spec = { ...DEFAULT_WEATHER, rain: 0.4, snow: 0.6, windStrength: 0.3 };
        const a = rainPoints(spec, 3.7, WIN);
        const b = rainPoints(spec, 3.7, WIN);
        expect(a).toEqual(b);
        expect(snowPoints(spec, 1.2, WIN)).toEqual(snowPoints(spec, 1.2, WIN));
    });
    it('время идёт — капли падают (y убывает, потом цикл)', () => {
        const spec = { ...DEFAULT_WEATHER, rain: 1 };
        const t0 = rainPoints(spec, 0, WIN)[0]!.pos[1];
        const t1 = rainPoints(spec, 0.5, WIN)[0]!.pos[1];
        expect(t1).toBeLessThan(t0);
    });
    it('позы внутри окна (wrap), альфа в 0..1', () => {
        const spec = { ...DEFAULT_WEATHER, snow: 1, windStrength: 1 };
        for (const p of snowPoints(spec, 17, WIN)) {
            expect(p.pos[0]).toBeGreaterThanOrEqual(WIN.minX);
            expect(p.pos[0]).toBeLessThanOrEqual(WIN.maxX);
            expect(p.pos[2]).toBeGreaterThanOrEqual(WIN.minZ);
            expect(p.pos[2]).toBeLessThanOrEqual(WIN.maxZ);
            expect(p.alpha).toBeGreaterThanOrEqual(0);
            expect(p.alpha).toBeLessThanOrEqual(1);
        }
    });
    it('высота слоя: капли и снег не ниже земли цикла (0..TOP)', () => {
        const spec = { ...DEFAULT_WEATHER, rain: 1, snow: 1 };
        for (const p of rainPoints(spec, 13, WIN)) expect(p.pos[1]).toBeGreaterThanOrEqual(0);
        for (const p of snowPoints(spec, 13, WIN)) expect(p.pos[1]).toBeGreaterThanOrEqual(0);
    });
    it('респавн меняет колонку: траектория не повторяется по циклам', () => {
        // скорость капли теперь зависит ещё и от глубины — момент полного
        // цикла находим по скану (два соседних респавна: та же фаза падения)
        const spec = { ...DEFAULT_WEATHER, rain: 1 };
        const w = headWraps(spec, 0);
        expect(w.length).toBeGreaterThanOrEqual(2);
        const a = rainPoints(spec, w[0]!, WIN)[0]!.pos;
        const b = rainPoints(spec, w[1]!, WIN)[0]!.pos;
        expect(b[1]).toBeCloseTo(a[1], 0); // та же фаза — верх слоя
        expect(b[0]).not.toBeCloseTo(a[0], 3); // но новая колонка (реролл)
    });
    it('поле дождя не квази-статично: через цикл переброшены почти все капли', () => {
        // Сдвиг респавна в узкой окрестности якоря оставлял каплю в той же
        // полосе — трек «горел» на экране вечно. Полный реролл перемещает
        // подавляющее большинство капель (совпадение колоны — редкость).
        // 5 с ≥ 2 полных циклов любой капли (слой 40, скорость ≥ ~17) —
        // к этому моменту у всех переброшен номер цикла падения.
        const spec = { ...DEFAULT_WEATHER, rain: 1 };
        const a = rainPoints(spec, 0, WIN);
        const b = rainPoints(spec, 5, WIN);
        let moved = 0;
        for (let i = 0; i < a.length; i += 3) { // головы штрихов
            if (Math.abs(a[i]!.pos[0] - b[i]!.pos[0]) > 2) moved++;
        }
        expect(moved).toBeGreaterThanOrEqual((a.length / 3) * 0.8);
    });
    it('внутри падения колонка неизменна (капля падает по прямой)', () => {
        // от сразу после респавна до середины падения — цикл не сменится
        // (длина цикла ≥ ~1 с, взяли 0.5)
        const spec = { ...DEFAULT_WEATHER, rain: 1 };
        const w = headWraps(spec, 0);
        const a = rainPoints(spec, w[0]! + 0.05, WIN)[0]!.pos;
        const b = rainPoints(spec, w[0]! + 0.55, WIN)[0]!.pos;
        expect(b[0]).toBeCloseTo(a[0]!, 6);
        expect(b[2]).toBeCloseTo(a[2]!, 6);
    });
    it('глубина: в поле есть дальний план (бледнее/мельче) и ближний (крупнее)', () => {
        const spec = { ...DEFAULT_WEATHER, rain: 1, snow: 1 };
        const drops = rainPoints(spec, 2, WIN);
        const sizes = drops.map((p) => p.size ?? 0);
        expect(Math.max(...sizes)).toBeGreaterThan(Math.min(...sizes) * 2);
        expect(Math.min(...drops.map((p) => p.alpha))).toBeLessThan(Math.max(...drops.map((p) => p.alpha)) * 0.5);
        const flakes = snowPoints(spec, 2, WIN);
        const fsz = flakes.map((p) => p.size ?? 0);
        expect(Math.max(...fsz)).toBeGreaterThan(Math.min(...fsz) * 2);
    });
});

describe('cloudDim (облачность)', () => {
    it('k=0 — цвет без изменений, k=1 — затемнение на долю dim', () => {
        expect(cloudDim(0xf7f4ea, 0, 0.7, 0.25)).toBe(0xf7f4ea);
        const c = cloudDim(0xf7f4ea, 1, 0.7, 0.25);
        expect(c).toBeLessThan(0xf7f4ea);
    });
    it('обесцвечивание: каналы сходятся к серому при полной облачности', () => {
        // насыщенный красный при sat=1 и dim=0 становится серым той же яркости
        const c = cloudDim(0xff0000, 1, 1, 0);
        const r = (c >> 16) & 255, g = (c >> 8) & 255, b = c & 255;
        expect(r).toBe(g);
        expect(g).toBe(b);
    });
});

describe('WeatherSystem (без WebGL — только спецификация)', () => {
    it('set клампит и current отдаёт нормализованную погоду', () => {
        const sys = new WeatherSystem({
            addParticles: () => ({ update: () => {}, setTint: () => {}, dispose: () => {} }),
            addFog: () => ({ update: () => {}, dispose: () => {} }),
            viewAxes: () => ({ camPos: [0, 0, 0], target: [0, 0, 0], forward: [0, 0, 1], halfW: 1, halfH: 1 }),
            setCloud: () => {},
            visibleWindow: () => ({ minX: 0, maxX: 0, minZ: 0, maxZ: 0 }),
        } as never);
        sys.set({ rain: 5, windStrength: -1, cloud: 0.5 });
        expect(sys.current.rain).toBe(1);
        expect(sys.current.windStrength).toBe(0);
        expect(sys.current.cloud).toBe(0.5);
        expect(sys.info()).toEqual({ rainDrops: 0, snowFlakes: 0 }); // счётчики — после update
    });
    it('update считает фактические капли/снежинки от окна', () => {
        const sys = new WeatherSystem({
            addParticles: () => ({ update: () => {}, setTint: () => {}, dispose: () => {} }),
            setFog: () => {},
            setCloud: () => {},
            visibleWindow: () => ({ minX: 0, maxX: 48, minZ: 0, maxZ: 32 }),
        } as never);
        sys.set({ rain: 1 });
        sys.update(1);
        // окно погоды — след кадра + маржа 16: (48+32)×(32+32) = 5120 кв. юнитов;
        // 6 слоёв дождя: 0.020/0.016/0.013/0.011/0.009/0.008 × 5120
        // → 102+82+67+56+46+41 = 394 капли
        expect(sys.info()).toEqual({ rainDrops: 394, snowFlakes: 0 });
        sys.set({ rain: 0, snow: 0 }); // пустая погода прячет слои и счётчики
        expect(sys.info()).toEqual({ rainDrops: 0, snowFlakes: 0 });
    });
});