import { describe, it, expect } from 'bun:test';
import { World } from '@common/rpg/core/world';
import { QuestLog, Quests } from '@common/rpg/components/questLog';
import { QuestSystem } from '@common/rpg/systems/quest';
import { Variables } from '@common/rpg/components/variables';
import type { Quest } from '@common/rpg/types';

// ── helpers ───────────────────────────────────────────────────────────────────

function world() {
    const w = new World();
    w.addSystem(new QuestSystem());
    return w;
}

/** Minimal one-stage quest whose single objective checks `vars.done == true`. */
function simpleQuest(id = 'q1', actions: Quest['stages'][0]['actions'] = []): Quest {
    return {
        id,
        title: 'Test Quest',
        description: '',
        stages: [{
            id: 'stage0',
            description: 'Do the thing',
            objectives: [{ id: 'obj', description: 'Done?', condition: 'Variables.done == true' }],
            actions,
        }],
    };
}

/** Two-stage quest: stage 0 checks `vars.step >= 1`, stage 1 checks `vars.step >= 2`. */
function twoStageQuest(id = 'q2'): Quest {
    return {
        id,
        title: 'Two-Stage Quest',
        description: '',
        stages: [
            {
                id: 'stage0',
                description: 'Step 1',
                objectives: [{ id: 'obj0', description: 'Reach step 1', condition: 'Variables.step >= 1' }],
                actions: [],
            },
            {
                id: 'stage1',
                description: 'Step 2',
                objectives: [{ id: 'obj1', description: 'Reach step 2', condition: 'Variables.step >= 2' }],
                actions: [],
            },
        ],
    };
}

function makePlayer(w: World, quests: Quest[] = []) {
    const player = w.createEntity('player');
    const vars = player.add(new Variables());
    const questLog = player.add(new QuestLog(quests));
    return { player, vars, questLog };
}

// ── QuestLog — registration ───────────────────────────────────────────────────

describe('QuestLog — registration', () => {
    it('constructor registers quests as inactive', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.getState('q1')?.status).toBe('inactive');
    });

    it('addQuest registers a quest after construction', () => {
        const w = world();
        const { questLog } = makePlayer(w);
        questLog.addQuest(simpleQuest('late'));
        expect(questLog.getState('late')?.status).toBe('inactive');
    });

    it('addQuest ignores duplicates', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.addQuest(simpleQuest()); // duplicate
        expect(questLog.getQuest('q1')).toBeDefined();
    });

    it('getQuest returns the quest definition', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.getQuest('q1')?.title).toBe('Test Quest');
    });

    it('getQuest returns undefined for unknown id', () => {
        const w = world();
        const { questLog } = makePlayer(w);
        expect(questLog.getQuest('ghost')).toBeUndefined();
    });
});

// ── QuestLog — state transitions ──────────────────────────────────────────────

describe('QuestLog — transitions', () => {
    it('start sets status to active', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.start('q1')).toBeTrue();
        expect(questLog.getState('q1')?.status).toBe('active');
    });

    it('complete sets status to completed', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        expect(questLog.complete('q1')).toBeTrue();
        expect(questLog.getState('q1')?.status).toBe('completed');
    });

    it('fail sets status to failed', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        expect(questLog.fail('q1')).toBeTrue();
        expect(questLog.getState('q1')?.status).toBe('failed');
    });

    it('abandon returns quest to inactive', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        expect(questLog.abandon('q1')).toBeTrue();
        expect(questLog.getState('q1')?.status).toBe('inactive');
    });

    it('abandon resets stageIndex to 0', () => {
        const w = world();
        const { questLog } = makePlayer(w, [twoStageQuest()]);
        questLog.start('q2');
        questLog._advance('q2'); // move to stage 1
        questLog.abandon('q2');
        expect(questLog.getState('q2')?.stageIndex).toBe(0);
    });

    it('start returns false for unknown quest', () => {
        const w = world();
        const { questLog } = makePlayer(w);
        expect(questLog.start('ghost')).toBeFalse();
    });

    it('complete returns false when not active', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.complete('q1')).toBeFalse(); // inactive
    });

    it('fail returns false when not active', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.fail('q1')).toBeFalse();
    });

    it('cannot start an already active quest', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        expect(questLog.start('q1')).toBeFalse();
    });
});

// ── QuestLog — events ─────────────────────────────────────────────────────────

describe('QuestLog — events', () => {
    it("emits 'started' on start", () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [simpleQuest()]);
        const events: unknown[] = [];
        player.on('QuestLog.started', ({ data }) => events.push(data));
        questLog.start('q1');
        expect(events).toEqual([{ questId: 'q1' }]);
    });

    it("emits 'completed' on complete", () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [simpleQuest()]);
        const events: unknown[] = [];
        player.on('QuestLog.completed', ({ data }) => events.push(data));
        questLog.start('q1');
        questLog.complete('q1');
        expect(events).toEqual([{ questId: 'q1' }]);
    });

    it("emits 'failed' on fail", () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [simpleQuest()]);
        const events: unknown[] = [];
        player.on('QuestLog.failed', ({ data }) => events.push(data));
        questLog.start('q1');
        questLog.fail('q1');
        expect(events).toEqual([{ questId: 'q1' }]);
    });

    it("emits 'abandoned' on abandon", () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [simpleQuest()]);
        const events: unknown[] = [];
        player.on('QuestLog.abandoned', ({ data }) => events.push(data));
        questLog.start('q1');
        questLog.abandon('q1');
        expect(events).toEqual([{ questId: 'q1' }]);
    });
});

// ── QuestLog — stage and objective access ─────────────────────────────────────

describe('QuestLog — stage access', () => {
    it('getStage returns current stage when active', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        expect(questLog.getStage('q1')?.id).toBe('stage0');
    });

    it('getStage returns undefined when inactive', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.getStage('q1')).toBeUndefined();
    });

    it('getObjectiveProgress returns objective status', () => {
        const w = world();
        const { player, vars, questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');

        const before = questLog.getObjectiveProgress('q1', player);
        expect(before).toHaveLength(1);
        expect(before![0].done).toBeFalse();

        vars.set({ key: 'done', value: true });
        const after = questLog.getObjectiveProgress('q1', player);
        expect(after![0].done).toBeTrue();
    });

    it('getObjectiveProgress returns undefined when not active', () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.getObjectiveProgress('q1', player)).toBeUndefined();
    });
});

// ── QuestLog — availability ───────────────────────────────────────────────────

describe('QuestLog — availability', () => {
    it('quest with no conditions is always available', () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [simpleQuest()]);
        expect(questLog.isAvailable('q1', player)).toBeTrue();
    });

    it('quest with unsatisfied condition is not available', () => {
        const w = world();
        const quest: Quest = { ...simpleQuest(), conditions: ['Variables.unlocked == true'] };
        const { player, questLog } = makePlayer(w, [quest]);
        expect(questLog.isAvailable('q1', player)).toBeFalse();
    });

    it('quest with satisfied condition is available', () => {
        const w = world();
        const quest: Quest = { ...simpleQuest(), conditions: ['Variables.unlocked == true'] };
        const { player, vars, questLog } = makePlayer(w, [quest]);
        vars.set({ key: 'unlocked', value: true });
        expect(questLog.isAvailable('q1', player)).toBeTrue();
    });
});

// ── QuestLog — variables / actions ────────────────────────────────────────────

describe('QuestLog — getVariables', () => {
    it('exposes quest status and stage index', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        const vars = questLog.getVariables();
        expect(vars['q1.status']).toBe('active');
        expect(vars['q1.stage']).toBe(0);
    });
});

describe('QuestLog — getActions', () => {
    it('exposes start action for inactive quest', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        expect('q1.start' in questLog.getActions()).toBeTrue();
    });

    it('exposes complete/fail/abandon actions for active quest', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        const actions = questLog.getActions();
        expect('q1.complete' in actions).toBeTrue();
        expect('q1.fail' in actions).toBeTrue();
        expect('q1.abandon' in actions).toBeTrue();
    });
});

// ── QuestLog — _advance ───────────────────────────────────────────────────────

describe('QuestLog — _advance', () => {
    it('moves to next stage', () => {
        const w = world();
        const { questLog } = makePlayer(w, [twoStageQuest()]);
        questLog.start('q2');
        questLog._advance('q2');
        expect(questLog.getState('q2')?.stageIndex).toBe(1);
        expect(questLog.getStage('q2')?.id).toBe('stage1');
    });

    it("emits 'stage' event when advancing", () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [twoStageQuest()]);
        const events: unknown[] = [];
        player.on('QuestLog.stage', ({ data }) => events.push(data));
        questLog.start('q2');
        questLog._advance('q2');
        expect((events[0] as any).questId).toBe('q2');
        expect((events[0] as any).index).toBe(1);
    });

    it('completes quest when advancing past last stage', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);
        questLog.start('q1');
        questLog._advance('q1');
        expect(questLog.getState('q1')?.status).toBe('completed');
    });

    it("emits 'completed' when advancing past last stage", () => {
        const w = world();
        const { player, questLog } = makePlayer(w, [simpleQuest()]);
        const events: unknown[] = [];
        player.on('QuestLog.completed', ({ data }) => events.push(data));
        questLog.start('q1');
        questLog._advance('q1');
        expect(events).toEqual([{ questId: 'q1' }]);
    });
});

// ── Quests.validate ───────────────────────────────────────────────────────────

describe('Quests.validate', () => {
    it('returns no errors for a valid quest with known actions', () => {
        const errors = Quests.validate(simpleQuest(), []);
        expect(errors).toHaveLength(0);
    });

    it('returns error for unknown action type in a stage', () => {
        const quest = simpleQuest('q', [{ type: 'unknown.action' }]);
        const errors = Quests.validate(quest, []);
        expect(errors.length).toBeGreaterThan(0);
        expect(errors[0]).toContain('unknown.action');
    });

    it('passes when action type is in the known actions list', () => {
        const quest = simpleQuest('q', [{ type: 'Variables.set' }]);
        const errors = Quests.validate(quest, ['Variables.set']);
        expect(errors).toHaveLength(0);
    });

    it('returns error for non-quest object', () => {
        const errors = Quests.validate({ broken: true }, []);
        expect(errors).toHaveLength(1);
    });
});

// ── QuestSystem — objective completion ────────────────────────────────────────

describe('QuestSystem — objective completion', () => {
    it('completes single-stage quest when objective is satisfied', () => {
        const w = world();
        const { vars, questLog } = makePlayer(w, [simpleQuest()]);

        questLog.start('q1');
        vars.set({ key: 'done', value: true });
        w.update(1);

        expect(questLog.getState('q1')?.status).toBe('completed');
    });

    it('does not complete quest while objective is unsatisfied', () => {
        const w = world();
        const { questLog } = makePlayer(w, [simpleQuest()]);

        questLog.start('q1');
        w.update(1);

        expect(questLog.getState('q1')?.status).toBe('active');
    });

    it('does not process inactive quests', () => {
        const w = world();
        const { vars, questLog } = makePlayer(w, [simpleQuest()]);

        vars.set({ key: 'done', value: true });
        w.update(1);

        expect(questLog.getState('q1')?.status).toBe('inactive');
    });

    it('runs stage actions before advancing', () => {
        const w = world();
        // action sets vars.reward = true on the player entity
        const quest = simpleQuest('q1', [{ type: 'Variables.set', arg: { key: 'reward', value: true } }]);
        const { vars, questLog } = makePlayer(w, [quest]);

        questLog.start('q1');
        vars.set({ key: 'done', value: true });
        w.update(1);

        expect(questLog.getState('q1')?.status).toBe('completed');
        expect(vars.state.reward).toBe(true);
    });
});

describe('QuestSystem — multi-stage progression', () => {
    it('advances through stages as objectives are satisfied', () => {
        const w = world();
        const { vars, questLog } = makePlayer(w, [twoStageQuest()]);

        questLog.start('q2');

        // satisfy stage 0
        vars.set({ key: 'step', value: 1 });
        w.update(1);
        expect(questLog.getState('q2')?.stageIndex).toBe(1);
        expect(questLog.getState('q2')?.status).toBe('active');

        // satisfy stage 1
        vars.set({ key: 'step', value: 2 });
        w.update(1);
        expect(questLog.getState('q2')?.status).toBe('completed');
    });

    it('does not skip stages', () => {
        const w = world();
        const { vars, questLog } = makePlayer(w, [twoStageQuest()]);

        questLog.start('q2');
        vars.set({ key: 'step', value: 2 }); // would satisfy both stages
        w.update(1);

        // only advances one stage per tick
        expect(questLog.getState('q2')?.stageIndex).toBe(1);

        w.update(1); // second tick completes it
        expect(questLog.getState('q2')?.status).toBe('completed');
    });
});

describe('QuestSystem — fail conditions', () => {
    it('fails quest when failCondition is met', () => {
        const w = world();
        const quest: Quest = {
            id: 'q1',
            title: 'Timed Quest',
            description: '',
            stages: [{
                id: 'stage0',
                description: 'Do it',
                objectives: [{ id: 'obj', description: 'Done?', condition: 'Variables.done == true' }],
                actions: [],
                failConditions: ['Variables.failed == true'],
            }],
        };
        const { vars, questLog } = makePlayer(w, [quest]);

        questLog.start('q1');
        vars.set({ key: 'failed', value: true });
        w.update(1);

        expect(questLog.getState('q1')?.status).toBe('failed');
    });

    it('fail condition takes priority over objective completion', () => {
        const w = world();
        const quest: Quest = {
            id: 'q1',
            title: 'Conflict Quest',
            description: '',
            stages: [{
                id: 'stage0',
                description: 'Both',
                objectives: [{ id: 'obj', description: 'Done?', condition: 'Variables.done == true' }],
                actions: [],
                failConditions: ['Variables.done == true'], // same condition
            }],
        };
        const { vars, questLog } = makePlayer(w, [quest]);

        questLog.start('q1');
        vars.set({ key: 'done', value: true });
        w.update(1);

        expect(questLog.getState('q1')?.status).toBe('failed'); // fail checked first
    });
});

describe('QuestSystem — multiple quests', () => {
    it('tracks multiple quests independently', () => {
        const w = world();
        const player = w.createEntity('player');
        const vars = player.add(new Variables());

        const q1: Quest = {
            id: 'q1', title: 'Q1', description: '',
            stages: [{ id: 's', description: '', objectives: [{ id: 'o', description: '', condition: 'Variables.done1 == true' }], actions: [] }],
        };
        const q2: Quest = {
            id: 'q2', title: 'Q2', description: '',
            stages: [{ id: 's', description: '', objectives: [{ id: 'o', description: '', condition: 'Variables.done2 == true' }], actions: [] }],
        };

        const log = player.add(new QuestLog([q1, q2]));
        log.start('q1');
        log.start('q2');

        vars.set({ key: 'done1', value: true }); // only q1's condition satisfied
        w.update(1);

        expect(log.getState('q1')?.status).toBe('completed');
        expect(log.getState('q2')?.status).toBe('active');
    });
});
