import { describe, it, expect } from 'vitest';
import { DialogueRunner, type DialogueView } from '../DialogueRunner';
import { evalConditions } from '../graph';
import { GameState } from '../../core/GameState';

/** Тестовый view: запоминает показанные реплики. */
interface ShownLine {
    speaker?: string;
    text: string;
    mood?: string;
    tags?: string[];
    choices: { index: number; text: string }[];
}

function makeView(): { view: DialogueView; shown: ShownLine[]; hidden: () => number } {
    const shown: ShownLine[] = [];
    let hideCount = 0;
    const view: DialogueView = {
        show: (n) => shown.push(n),
        hide: () => hideCount++
    };
    return { view, shown, hidden: () => hideCount };
}

describe('DialogueRunner', () => {
    it('линейный диалог: advance до конца, onFinish вызывается', () => {
        const state = new GameState();
        const { view, shown, hidden } = makeView();
        const dr = new DialogueRunner(state, view);
        let finished = 0;
        dr.onFinish = () => finished++;

        dr.start({
            start: 'a',
            nodes: {
                a: { text: 'Привет.', next: 'b' },
                b: { text: 'Пока.', next: 'c' },
                c: { text: '...' }
            }
        });

        expect(dr.active).toBe(true);
        expect(shown[0]?.text).toBe('Привет.');
        dr.advance();
        expect(shown[1]?.text).toBe('Пока.');
        dr.advance();
        expect(shown[2]?.text).toBe('...');
        dr.advance();
        expect(finished).toBe(1);
        expect(dr.active).toBe(false);
        expect(hidden()).toBe(1);
    });

    it('узел без next — конец диалога', () => {
        const state = new GameState();
        const dr = new DialogueRunner(state, makeView().view);
        let finished = 0;
        dr.onFinish = () => finished++;
        dr.start({ start: 'a', nodes: { a: { text: 'Конец' } } });
        dr.advance();
        expect(finished).toBe(1);
    });

    it('выборы: показываются только прошедшие условия, pick уходит по next', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);

        dr.start({
            start: 'q',
            nodes: {
                q: {
                    speaker: 'Ирвин',
                    text: 'Поможешь?',
                    choices: [
                        { text: 'Да', next: 'yes', setFlags: ['quest_taken'] },
                        { text: 'Нет', next: 'no' },
                        { text: 'Секрет', when: ['knows_secret'], next: 'secret' }
                    ]
                },
                yes: { text: 'Спасибо!' },
                no: { text: 'Жаль.' },
                secret: { text: 'Ты знаешь тайну.' }
            }
        });

        expect(shown[0]?.speaker).toBe('Ирвин');
        expect(shown[0]?.choices.map((c) => c.text)).toEqual(['Да', 'Нет']);

        dr.pick(0);
        expect(state.hasFlag('quest_taken')).toBe(true);
        // advance показывает следующий узел после применения эффекта выбора
        dr.advance();
        expect(shown[1]?.text).toBe('Спасибо!');
    });

    it('advance во время выбора игнорируется', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'q',
            nodes: { q: { text: 'Выбор?', choices: [{ text: 'A', next: 'a2' }], next: 'x' }, a2: { text: 'A' }, x: { text: 'X' } }
        });
        dr.advance();
        expect(shown.length).toBe(1); // реплика 'X' не показалась
    });

    it('узел-действие без текста применяет флаги и переходит дальше', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'give',
            nodes: {
                give: { setFlags: ['reward_given'], next: 'msg' },
                msg: { text: 'Готово.' }
            }
        });
        expect(shown[0]?.text).toBe('Готово.');
        expect(state.hasFlag('reward_given')).toBe(true);
    });

    it('when/whenNot фильтруют узлы: диалог идёт по другой ветке', () => {
        const state = new GameState();
        state.setFlag('was_here');
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'check',
            nodes: {
                check: { when: ['was_here'], text: 'Опять ты.', next: 'end' },
                first: { text: 'Первый раз.' },
                end: { text: 'Конец.' }
            }
        });
        // check прошёл условие
        expect(shown[0]?.text).toBe('Опять ты.');
    });

    it('когда узел не прошёл условие, диалог уходит по его next', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'check',
            nodes: {
                check: { when: ['vip'], text: 'Ветка VIP.', next: 'end' },
                end: { text: 'Финал.' }
            }
        });
        // check не прошёл (нет vip) — но у check есть next: показывается end
        expect(shown[0]?.text).toBe('Финал.');
        expect(dr.active).toBe(true);
    });

    it('узел-действие с whenNot переходит по next при проходе условия', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'hub',
            nodes: {
                hub: { whenNot: ['vip'], next: 'normal' },
                normal: { text: 'Обычная ветка.', next: 'end' },
                end: { text: 'Финал.' }
            }
        });
        // hub без текста: условие прошло, эффектов нет, next -> normal
        expect(shown[0]?.text).toBe('Обычная ветка.');
    });

    it('whenVar фильтрует по числовой переменной', () => {
        const state = new GameState();
        state.setVar('gold', 5);
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'q',
            nodes: {
                q: {
                    text: 'Торговец:',
                    choices: [
                        { text: 'Купить (10 зол.)', whenVar: { key: 'gold', op: 'ge', value: 10 }, next: 'buy' },
                        { text: 'Уйти', next: 'bye' }
                    ]
                },
                buy: { text: 'Куплено.' },
                bye: { text: 'Пока.' }
            }
        });
        expect(shown[0]?.choices.map((c) => c.text)).toEqual(['Уйти']);
    });

    it('setVars в выборах меняет состояние', () => {
        const state = new GameState();
        const dr = new DialogueRunner(state, makeView().view);
        dr.start({
            start: 'q',
            nodes: {
                q: { text: 'Сколько?', choices: [{ text: 'Дать 30', setVars: { gold: 30 }, next: 'ok' }] },
                ok: { text: 'Ок.' }
            }
        });
        dr.pick(0);
        expect(state.getNumber('gold')).toBe(30);
    });

    it('цикл из узлов без текста не зависает — защита по MAX_STEPS', () => {
        const state = new GameState();
        const dr = new DialogueRunner(state, makeView().view);
        dr.start({
            start: 'a',
            nodes: {
                a: { next: 'b' },
                b: { next: 'a' }
            }
        });
        expect(dr.active).toBe(false); // безопасно завершился
    });

    it('whenVars: несколько условий по переменным, AND', () => {
        const state = new GameState();
        state.setVar('gold', 5);
        state.setVar('trust', 2);
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'q',
            nodes: {
                q: {
                    text: 'Торговец:',
                    choices: [
                        {
                            text: 'Дорогой товар',
                            whenVars: [
                                { key: 'gold', op: 'ge', value: 10 },
                                { key: 'trust', op: 'ge', value: 1 }
                            ],
                            next: 'buy'
                        },
                        { text: 'Уйти', next: 'bye' }
                    ]
                },
                buy: { text: 'Куплено.' },
                bye: { text: 'Пока.' }
            }
        });
        // gold < 10 — первая ветка скрыта, хотя trust проходит
        expect(shown[0]?.choices.map((c) => c.text)).toEqual(['Уйти']);
        state.setVar('gold', 50);
        dr.start({
            start: 'q',
            nodes: {
                q: {
                    text: 'Торговец:',
                    choices: [
                        {
                            text: 'Дорогой товар',
                            whenVars: [
                                { key: 'gold', op: 'ge', value: 10 },
                                { key: 'trust', op: 'ge', value: 1 }
                            ],
                            next: 'buy'
                        },
                        { text: 'Уйти', next: 'bye' }
                    ]
                },
                buy: { text: 'Куплено.' },
                bye: { text: 'Пока.' }
            }
        });
        expect(shown[1]?.choices.map((c) => c.text)).toEqual(['Дорогой товар', 'Уйти']);
    });

    it('hasItem резолвится через DialogueWorld; без world — ложно', () => {
        const state = new GameState();
        const world = { hasItem: (id: string) => id === 'cloth' };
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view, { world });
        dr.start({
            start: 'q',
            nodes: {
                q: {
                    text: 'Торговец:',
                    choices: [
                        { text: 'Продать ткань', hasItem: ['cloth'], next: 'sell' },
                        { text: 'Продать соль', hasItem: ['salt'], next: 'nope' },
                        { text: 'Уйти', next: 'bye' }
                    ]
                },
                sell: { text: 'Продано.' },
                nope: { text: 'Нет соли.' },
                bye: { text: 'Пока.' }
            }
        });
        expect(shown[0]?.choices.map((c) => c.text)).toEqual(['Продать ткань', 'Уйти']);

        // узел-выбор без текста и без world: hasItem-варианты скрыты — конец диалога
        const dr2 = new DialogueRunner(state, makeView().view);
        dr2.start({
            start: 'q',
            nodes: {
                q: { choices: [{ text: 'Продать ткань', hasItem: ['cloth'], next: 'sell' }] },
                sell: { text: 'Продано.' }
            }
        });
        expect(dr2.active).toBe(false); // нет доступных вариантов — конец
    });

    it('evalConditions работает без рантайма на чистом GameState', () => {
        const state = new GameState();
        state.setFlag('met_elder');
        state.setVar('flowers', 3);
        const world = { hasItem: (id: string) => id === 'cloth' };

        expect(evalConditions({ when: ['met_elder'] }, state)).toBe(true);
        expect(evalConditions({ when: ['met_elder'], whenNot: ['quest_done'] }, state)).toBe(true);
        expect(evalConditions({ when: ['quest_done'] }, state)).toBe(false);
        expect(
            evalConditions(
                { whenVars: [{ key: 'flowers', op: 'ge', value: 3 }] },
                state,
                world
            )
        ).toBe(true);
        expect(
            evalConditions(
                { whenVars: [{ key: 'flowers', op: 'ge', value: 4 }] },
                state
            )
        ).toBe(false);
        expect(evalConditions({ hasItem: ['cloth'] }, state, world)).toBe(true);
        expect(evalConditions({ hasItem: ['cloth'] }, state)).toBe(false); // нет world
        expect(evalConditions({}, state)).toBe(true); // пустые условия — истина
    });

    it('do[] эмитится через onEffect в порядке следования, у узла и у выбора', () => {
        const state = new GameState();
        const { view } = makeView();
        const dr = new DialogueRunner(state, view);
        const emitted: { kind: string; at: string; choice?: number }[] = [];
        dr.onEffect = (op, at) => emitted.push({ kind: op.kind, at: at.nodeId, choice: at.choice });

        dr.start({
            start: 'hub',
            nodes: {
                hub: {
                    do: [{ kind: 'sound', id: 'bell' }],
                    text: 'Возьми?',
                    choices: [
                        { text: 'Да', next: 'ok', do: [{ kind: 'giveItem', id: 'cloth' }, { kind: 'toast', text: 'Ткань' }] }
                    ]
                },
                ok: { text: 'Держи.', do: [{ kind: 'custom', id: 'plant' }] }
            }
        });
        // эффекты узла при входе
        expect(emitted).toEqual([{ kind: 'sound', at: 'hub' }]);
        dr.pick(0);
        // сначала эффекты выбора, потом эффекты узла ok
        expect(emitted.map((e) => e.kind)).toEqual(['sound', 'giveItem', 'toast', 'custom']);
        expect(emitted[3]).toEqual({ kind: 'custom', at: 'ok' });
        expect(emitted[1].choice).toBe(0);
    });

    it('do[] без подписчика и с неизвестным kind не падают', () => {
        const state = new GameState();
        const dr = new DialogueRunner(state, makeView().view);
        dr.start({
            start: 'a',
            nodes: { a: { text: 'Тишина.', do: [{ kind: 'custom' as const, id: 'unknown_thing' }] } }
        });
        dr.advance();
        expect(dr.active).toBe(false);
        expect(state.hasFlag('nothing')).toBe(false);
    });

    it('path/result: путь показанных узлов и выборы; onFinish получает результат', () => {
        const state = new GameState();
        const { view } = makeView();
        const dr = new DialogueRunner(state, view);
        let finishedResult: unknown = null;
        dr.onFinish = (_graph, result) => {
            finishedResult = result;
        };

        dr.start({
            start: 'q',
            nodes: {
                q: { text: 'Вопрос?', next: 'mid' },
                mid: { text: 'Выбирай.', choices: [{ text: 'Ок', next: 'end' }, { text: 'Нет' }] },
                end: { text: 'Конец.' }
            }
        });
        expect(dr.path).toEqual(['q']);
        dr.advance();
        dr.pick(0);
        dr.advance();
        expect(dr.active).toBe(false);

        const expected = {
            lastNodeId: 'end',
            path: ['q', 'mid', 'end'],
            picks: [{ nodeId: 'mid', index: 0, text: 'Ок' }]
        };
        expect(dr.result).toEqual(expected);
        expect(finishedResult).toEqual(expected);
        // result переживает новый запуск и сбрасывается
        dr.start({ start: 'a', nodes: { a: { text: 'Заново' } } });
        expect(dr.result).toBeNull();
        expect(dr.path).toEqual(['a']);
    });

    it('mood/tags доходят до view', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view);
        dr.start({
            start: 'a',
            nodes: { a: { text: 'Грустно.', mood: 'sad', tags: ['epilogue'] } }
        });
        expect(shown[0]?.mood).toBe('sad');
        expect(shown[0]?.tags).toEqual(['epilogue']);
        expect(shown[0]?.text).toBe('Грустно.');
    });

    it('textKey резолвится через hooks.resolve; без хука — сам ключ', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view, {
            resolve: (key) => (key === 'elder.greet' ? 'Здравствуй, путник.' : key)
        });
        dr.start({
            start: 'a',
            nodes: {
                a: { textKey: 'elder.greet', speakerKey: 'name.elder', next: 'b' },
                b: { textKey: 'missing.key' }
            }
        });
        expect(shown[0]?.text).toBe('Здравствуй, путник.');
        expect(shown[0]?.speaker).toBe('name.elder'); // ключ без перевода — сам ключ
        dr.advance();
        expect(shown[1]?.text).toBe('missing.key');
    });

    it('узел только с textKey — реплика (не действие)', () => {
        const state = new GameState();
        const { view, shown } = makeView();
        const dr = new DialogueRunner(state, view, { resolve: () => 'Строка' });
        dr.start({ start: 'a', nodes: { a: { textKey: 'only.key' } } });
        expect(shown[0]?.text).toBe('Строка');
        expect(dr.active).toBe(true);
    });
});