import { describe, expect, it } from 'vitest';
import { treeModel, boulderModel } from '../generators';
import { decodeModel } from '../format';
import { VoxelWorld } from '../../world/world';
import { stampModel } from '../stamp';

const NEI = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]] as const;

describe('генераторы деревьев', () => {
    it('детерминированы: один (вид, сид) → идентичная модель', () => {
        for (const v of [0, 1, 2] as const) {
            expect(JSON.stringify(treeModel(v, 7))).toBe(JSON.stringify(treeModel(v, 7)));
        }
    });

    it('разные сиды дают разные данные', () => {
        expect(treeModel(0, 1).data).not.toBe(treeModel(0, 2).data);
    });

    it('варианты различаются при одном сиде', () => {
        const models = [treeModel(0, 5), treeModel(1, 5), treeModel(2, 5)];
        expect(new Set(models.map((m) => JSON.stringify(m))).size).toBe(3);
    });

    for (const v of [0, 1, 2] as const) {
        it(`вид ${v}: непустая, ствол есть и связан с кроной`, () => {
            const g = decodeModel(treeModel(v, 123));
            expect(g.count()).toBeGreaterThan(0);
            let trunk = 0, linked = false;
            for (let z = 0; z < g.sz; z++)
                for (let y = 0; y < g.sy; y++)
                    for (let x = 0; x < g.sx; x++) {
                        if (g.get(x, y, z) !== 4) continue;
                        trunk++;
                        for (const [dx, dy, dz] of NEI) {
                            const n = g.get(x + dx, y + dy, z + dz);
                            if (n === 3 || n === 5) linked = true;
                        }
                    }
            expect(trunk).toBeGreaterThan(0);
            expect(linked).toBe(true);
        });
    }
});

describe('генераторы валунов', () => {
    it('детерминированы; разные сиды различаются', () => {
        expect(JSON.stringify(boulderModel(1, 9))).toBe(JSON.stringify(boulderModel(1, 9)));
        expect(boulderModel(1, 9).data).not.toBe(boulderModel(1, 10).data);
    });

    for (const v of [0, 1, 2] as const) {
        it(`вид ${v}: непустой, стоит на земле, свет на верхушках столбцов`, () => {
            const g = decodeModel(boulderModel(v, 77));
            expect(g.count()).toBeGreaterThan(0);
            let ground = 0;
            for (let z = 0; z < g.sz; z++)
                for (let x = 0; x < g.sx; x++) {
                    let top = -1;
                    for (let y = 0; y < g.sy; y++) {
                        const c = g.get(x, y, z);
                        if (c !== 0) { if (y === 0) ground++; top = y; }
                    }
                    if (top >= 0) expect(g.get(x, top, z)).toBe(7); // свет сверху
                }
            expect(ground).toBeGreaterThan(0);
        });
    }

    it('вариант = размер: s растёт с видом', () => {
        expect(boulderModel(0, 3).size[0]).toBe(5);
        expect(boulderModel(2, 3).size[0]).toBe(7);
    });
});

describe('штамповка в мир', () => {
    /** Пустой мир с «землёй» y=0..1 в 2×2 чанках. */
    function groundWorld(): VoxelWorld {
        const w = new VoxelWorld(24);
        for (let x = 0; x < 32; x++) for (let z = 0; z < 32; z++) w.set(x, 0, z, 2);
        w.takeDirty();
        return w;
    }

    it('ставит все воксели модели и возвращает их число', () => {
        const w = groundWorld();
        const m = boulderModel(0, 5);
        const expected = decodeModel(m).count();
        expect(stampModel(w, m, 4, 2, 4)).toBe(expected);
        // в области штампа столько непустых вокселей, сколько в модели
        let nonzero = 0;
        const [sx, sy, sz] = m.size;
        for (let ly = 0; ly < sy; ly++)
            for (let lz = 0; lz < sz; lz++)
                for (let lx = 0; lx < sx; lx++) if (w.get(4 + lx, 2 + ly, 4 + lz) !== 0) nonzero++;
        expect(nonzero).toBe(expected);
    });

    it('клиппинг обрезает выходящую за границу часть', () => {
        const w = groundWorld();
        const m = treeModel(0, 5);
        const full = decodeModel(m);
        // maxX=6 (half-open): столбцы x≥6 мира не ставятся
        const placed = stampModel(w, m, 2, 2, 2, { maxX: 6 });
        let clippedOut = 0;
        for (let ly = 0; ly < full.sy; ly++)
            for (let lz = 0; lz < full.sz; lz++)
                for (let lx = 0; lx < full.sx; lx++)
                    if (full.get(lx, ly, lz) !== 0 && 2 + lx >= 6) clippedOut++;
        expect(placed).toBe(full.count() - clippedOut);
        expect(placed).toBeLessThan(full.count());
    });

    it('полный клиппинг → 0 поставленных, мир не растёт', () => {
        const w = groundWorld();
        const chunks = w.chunkCount;
        const m = treeModel(1, 5);
        expect(stampModel(w, m, 100, 2, 100, { minX: 0, maxX: 32, minZ: 0, maxZ: 32 })).toBe(0);
        expect(w.chunkCount).toBe(chunks);
    });

    it('вне вертикали мира (y) не ставится', () => {
        const w = new VoxelWorld(4);
        const m = treeModel(1, 5);
        expect(stampModel(w, m, 0, 3, 0)).toBeLessThan(decodeModel(m).count());
    });
});