import { describe, expect, it } from 'vitest';
import type { DialogueGraph, TileMapData } from '@rpg/engine';
import { TILES, buildMeadowsMap, buildPondsMap, buildZvenetsMap } from '../map';
import { AREAS } from '../locations';
import { ROOM_TONES } from '../sfxSpecs';
import { NPCS } from '../npcs';
import { DIALOGUES } from '../dialogues';
import { FLAGS } from '../ids';
import { validateContent, validateDialogue, validateAudio, validateLocations, validateNpcs, validateReferences } from '../validate';

/** Карта целиком из проходимой травы (или с одиночной стеной). */
function flatMap(w: number, h: number, wall?: { x: number; y: number }): TileMapData {
    const tiles = new Array(w * h).fill(TILES.GRASS);
    if (wall) tiles[wall.y * w + wall.x] = TILES.HOUSE;
    return { width: w, height: h, tiles, blocked: [TILES.WATER, TILES.TREE, TILES.TOWER, TILES.HOUSE] };
}

/** Карты всех локаций (как их грузит BootScene). */
function realMaps(): Map<string, TileMapData> {
    return new Map([
        ['meadows', buildMeadowsMap()],
        ['ponds', buildPondsMap()],
        ['zvenets', buildZvenetsMap()]
    ]);
}

describe('validateContent — реальный контент чист', () => {
    it('ошибок нет (warn допустим)', () => {
        const errs = validateContent(realMaps()).filter((i) => i.severity === 'error');
        expect(errs).toEqual([]);
    });
});

describe('validateReferences — реестры флагов/варов', () => {
    it('реальный контент: только флаг знакомств вне контента (dead), ошибок нет', () => {
        const inv = validateReferences();
        expect(inv.filter((i) => i.severity === 'error')).toEqual([]);
        // got_cloth ставится диалогом и нигде не читается, но ставится — упомянут.
        // hint_bells ставится кодом сцены (подсказка прудов) — в контенте не упомянут.
        const dead = inv.filter((i) => i.id === 'flag-dead').map((i) => i.message);
        expect(dead).toEqual(['флаг «hint_bells» из реестра не упоминается в контенте']);
    });

    it('неизвестный флаг/вар в диалоге — error', () => {
        const g: DialogueGraph = { start: 'a', nodes: { a: { text: 'a', setFlags: ['quest_bels_done'], whenVar: { key: 'moats', op: 'ge', value: 1 } } } };
        const backup = DIALOGUES['test_ref'];
        (DIALOGUES as Record<string, DialogueGraph>)['test_ref'] = g;
        try {
            const inv = validateReferences();
            expect(inv.some((i) => i.id === 'flag-unknown' && i.message.includes('quest_bels_done'))).toBe(true);
            expect(inv.some((i) => i.id === 'var-unknown' && i.message.includes('moats'))).toBe(true);
        } finally {
            if (backup === undefined) delete (DIALOGUES as Record<string, unknown>)['test_ref'];
            else (DIALOGUES as Record<string, DialogueGraph>)['test_ref'] = backup;
        }
    });

    it('битая ссылка квеста на NPC/диалог — error', () => {
        // Симулируем опечатку: подменяем NPCS/QUESTS нельзя (const), поэтому
        // проверяем негатив через фейковый NPC-деф на уровне флага flagKey.
        const broken = { ...NPCS[0]!, flagKey: 'metEllder' as keyof typeof FLAGS };
        const backup = NPCS[0]!;
        (NPCS as unknown as { [0]: typeof broken })[0] = broken;
        try {
            const inv = validateReferences();
            expect(inv.some((i) => i.id === 'flag-unknown' && i.message.includes('metEllder'))).toBe(true);
        } finally {
            (NPCS as unknown as { [0]: typeof backup })[0] = backup;
        }
    });
});

describe('validateDialogue', () => {
    it('битый start и битый next — ошибки', () => {
        const g: DialogueGraph = {
            start: 'нет',
            nodes: {
                a: { text: 'a', next: 'нет2' },
                b: { text: 'b', choices: [{ text: 'x', next: 'нет3' }] }
            }
        };
        const ids = validateDialogue('test', g).map((i) => i.id);
        expect(ids).toContain('dialogue-start');
        expect(ids).toContain('dialogue-next');
    });

    it('узел-сирота — warn, связный граф чист', () => {
        const ok: DialogueGraph = { start: 'a', nodes: { a: { text: 'a', next: 'b' }, b: { text: 'b', end: true } } };
        expect(validateDialogue('ok', ok)).toEqual([]);
        const orphan: DialogueGraph = { start: 'a', nodes: { a: { text: 'a', end: true }, dead: { text: 'x', end: true } } };
        expect(validateDialogue('orphan', orphan).map((i) => i.id)).toEqual(['dialogue-orphan']);
    });
});

describe('validateNpcs', () => {
    it('проходимая карта без NPC в стенах — чисто; стена под NPC ловится', () => {
        const maps = new Map([['zvenets', flatMap(28, 20)]]);
        expect(validateNpcs(maps)).toEqual([]);
        // NPCS статичен — подменяем карту: стена на тайле Ирвина даст 'in-wall'.
        const elder = AREAS.zvenets.npcs[0]!;
        const walled = new Map([['zvenets', flatMap(28, 20, { x: elder.tile.x, y: elder.tile.y })]]);
        const inv = validateNpcs(walled);
        expect(inv.some((i) => i.id === 'in-wall' && i.message.includes(elder.id))).toBe(true);
    });
});

describe('validateLocations', () => {
    it('переход в несуществующую область — ошибки target нет, guard на карту', () => {
        const maps = new Map([['meadows', flatMap(28, 28)]]);
        // AREAS статичен; проверяем чистую функцию через реальные данные:
        // meadows -> ponds/zvenets существуют, но их карт в maps нет —
        // guard на отсутствующую карту не роняет проверку.
        const inv = validateLocations(maps);
        expect(inv.every((i) => i.severity === 'warn' || i.where?.includes('meadows'))).toBe(true);
    });

    it('спавн в стене ловится', () => {
        const spawn = AREAS.meadows.spawn;
        const maps = new Map([['meadows', flatMap(28, 28, spawn)]]);
        const inv = validateLocations(maps);
        expect(inv.some((i) => i.id === 'in-wall' && i.message.includes('spawn'))).toBe(true);
    });
});
describe('validateAudio — аудио-ключи контента', () => {
    it('неизвестный ambience области — error', () => {
        const backup = AREAS.meadows.ambience;
        (AREAS.meadows as { ambience?: string }).ambience = 'ambience/tundra';
        try {
            const inv = validateAudio();
            expect(inv.some((i) => i.id === 'audio-key' && i.message.includes('ambience/tundra'))).toBe(true);
        } finally {
            (AREAS.meadows as { ambience?: string }).ambience = backup;
        }
    });

    it('sfx-ключ реакции вне спек-реестра — error', () => {
        const area = AREAS.meadows;
        const backup = area.interactables;
        (area as { interactables?: unknown }).interactables = [
            { id: 'x', kind: 'signpost', tile: { x: 1, y: 1 }, responses: [{ sound: 'sfx/nats' }] }
        ];
        try {
            const inv = validateAudio();
            expect(inv.some((i) => i.id === 'audio-key' && i.message.includes('sfx/nats'))).toBe(true);
        } finally {
            (area as { interactables?: unknown }).interactables = backup;
        }
    });

    it('roomtone с ключом-не-областью — error; реальные контент чист', () => {
        (ROOM_TONES as Record<string, unknown>).cave = { kind: 'hum', tone: 40, dur: 4, power: 2, peak: 0.2 };
        try {
            const inv = validateAudio();
            expect(inv.some((i) => i.id === 'audio-key' && i.message.includes('cave'))).toBe(true);
        } finally {
            delete (ROOM_TONES as Record<string, unknown>).cave;
        }
        expect(validateAudio().filter((i) => i.severity === 'error')).toEqual([]);
    });
});

describe('validateReferences — requiresItem и return-правило интерьеров', () => {
    it('переход с неизвестным requiresItem — error', () => {
        const t = AREAS.ponds.transitions[0]!;
        const backup = t.requiresItem;
        (t as { requiresItem?: string }).requiresItem = 'rope';
        try {
            const inv = validateReferences();
            expect(inv.some((i) => i.id === 'item-unknown' && i.message.includes('rope'))).toBe(true);
        } finally {
            (t as { requiresItem?: string }).requiresItem = backup;
        }
    });

    it('накат с неизвестным requiresItem — error', () => {
        const h = AREAS.ponds.hazards![0]!;
        const backup = h.requiresItem;
        (h as { requiresItem?: string }).requiresItem = 'mask';
        try {
            const inv = validateReferences();
            expect(inv.some((i) => i.id === 'item-unknown' && i.message.includes('mask'))).toBe(true);
        } finally {
            (h as { requiresItem?: string }).requiresItem = backup;
        }
    });

    it('неизвестный whenFlag источника света — error', () => {
        const s = AREAS.zvenets.lighting!.sources![0]!;
        const backup = s.whenFlag;
        (s as { whenFlag?: string }).whenFlag = 'evening_yes';
        try {
            const inv = validateReferences();
            expect(inv.some((i) => i.id === 'flag-unknown' && i.message.includes('evening_yes'))).toBe(true);
        } finally {
            (s as { whenFlag?: string }).whenFlag = backup;
        }
    });

    it('интерьер без return-перехода — error', () => {
        const area = AREAS.shop;
        const backup = area.transitions;
        (area as { transitions: typeof backup }).transitions = [];
        try {
            const inv = validateReferences();
            expect(inv.some((i) => i.id === 'interior-return' && i.where?.includes('shop'))).toBe(true);
        } finally {
            (area as { transitions: typeof backup }).transitions = backup;
        }
    });
});

describe('validateLocations — патрули и источники света', () => {
    it('точка патруля вне карты — out-of-bounds', () => {
        const e = AREAS.meadows.enemies[2]!;
        const backup = e.patrol;
        (e as { patrol?: typeof backup }).patrol = { points: [{ x: 19, y: 6 }, { x: 50, y: 0 }], pauseSec: 1 };
        try {
            const inv = validateLocations(realMaps());
            expect(inv.some((i) => i.id === 'out-of-bounds' && i.message.includes('патруль#1'))).toBe(true);
        } finally {
            (e as { patrol?: typeof backup }).patrol = backup;
        }
    });

    it('дубль id источника света и источник без at/tiles — error', () => {
        const area = AREAS.shop;
        const backup = area.lighting;
        (area as { lighting?: typeof backup }).lighting = {
            ambient: 0x5a5e70,
            sources: [
                { id: 'a', at: { x: 1.5, y: 1.5 } },
                { id: 'a', at: { x: 2.5, y: 2.5 } },
                { id: 'b' }
            ]
        };
        try {
            const inv = validateLocations(new Map([['shop', flatMap(10, 10)]]));
            expect(inv.filter((i) => i.id === 'light-id').length).toBe(1);
            expect(inv.some((i) => i.id === 'light-source-empty' && i.message.includes('b'))).toBe(true);
        } finally {
            (area as { lighting?: typeof backup }).lighting = backup;
        }
    });
});
