import { ConfigMgr, MapJsonLoader } from '@data/ConfigMgr';
import { EnemyType, ItemType, WeaponType } from '@data/Interfaces';

// Import the real JSON delivered by task 2.1 — if these files are malformed
// the test suite will catch it on CI before any Cocos editor run.
import enemies from '../../assets/resources/configs/enemies.json';
import items from '../../assets/resources/configs/items.json';
import weapons from '../../assets/resources/configs/weapons.json';
import levels from '../../assets/resources/configs/levels.json';
import bosses from '../../assets/resources/configs/bosses.json';
import stories from '../../assets/resources/configs/stories.json';

function makeLoader(overrides: Partial<Record<string, unknown>> = {}) {
    const base: Record<string, unknown> = {
        'configs/enemies': enemies,
        'configs/items': items,
        'configs/weapons': weapons,
        'configs/levels': levels,
        'configs/bosses': bosses,
        'configs/stories': stories,
    };
    return new MapJsonLoader({ ...base, ...overrides });
}

describe('ConfigMgr — happy path with delivered JSON', () => {
    it('loads and validates the chapter-1 bundle', async () => {
        const mgr = new ConfigMgr(makeLoader());
        const bundle = await mgr.load();
        expect(bundle.enemies.length).toBe(4);
        expect(bundle.items.length).toBe(5);
        expect(bundle.weapons.length).toBe(2);
        expect(bundle.levels.length).toBe(5);
        expect(bundle.bosses.length).toBe(1);
        expect(bundle.stories.length).toBe(1);
    });

    it('resolves every enemy, item, weapon, level, boss, and story by id', async () => {
        const mgr = new ConfigMgr(makeLoader());
        await mgr.load();
        expect(mgr.enemy(EnemyType.QingRen).displayName).toBe('青忍');
        expect(mgr.item(ItemType.CrystalJade).displayName).toBe('水晶玉');
        expect(mgr.weapon(WeaponType.NinjaSword).canParry).toBe(true);
        expect(mgr.level('1-5').objective.kind).toBe('defeat_boss');
        expect(mgr.boss('shuang_huan_fang').phases.length).toBe(3);
expect(mgr.story('chapter_1_intro').pages.length).toBe(3);
    });

    it('throws when accessed before load()', () => {
        const mgr = new ConfigMgr(makeLoader());
        expect(() => mgr.enemy(EnemyType.QingRen)).toThrow(/load\(\)/);
    });
});

describe('ConfigMgr — validation rejects malformed configs', () => {
    it('rejects a config bundle that contains the forbidden "casual" token (req 13.6)', async () => {
        const polluted = JSON.parse(JSON.stringify(levels));
        polluted[0].displayName = 'casual'; // inject disallowed token
        const mgr = new ConfigMgr(makeLoader({ 'configs/levels': polluted }));
        await expect(mgr.load()).rejects.toThrow(/casual/);
    });

    it('rejects an enemy entry missing required fields', async () => {
        const bad = [{ id: 'qing_ren' }];
        const mgr = new ConfigMgr(makeLoader({ 'configs/enemies': bad }));
        await expect(mgr.load()).rejects.toThrow(/missing field/);
    });

    it('rejects an enemy with an unknown EnemyType id', async () => {
        const bad = JSON.parse(JSON.stringify(enemies));
        bad[0].id = 'not_a_real_enemy';
        const mgr = new ConfigMgr(makeLoader({ 'configs/enemies': bad }));
        await expect(mgr.load()).rejects.toThrow(/EnemyType/);
    });

    it('rejects a level referencing an unknown boss', async () => {
        const bad = JSON.parse(JSON.stringify(levels));
        bad[4].objective.bossId = 'ghost_boss';
        const mgr = new ConfigMgr(makeLoader({ 'configs/levels': bad }));
        await expect(mgr.load()).rejects.toThrow(/unknown boss/);
    });

    it('rejects a level spawn referencing an unknown enemy', async () => {
        const bad = JSON.parse(JSON.stringify(levels));
        bad[0].enemySpawns[0].type = 'white_ninja';
        const mgr = new ConfigMgr(makeLoader({ 'configs/levels': bad }));
        await expect(mgr.load()).rejects.toThrow(/unknown enemy/);
    });

    it('rejects boss phases that are not monotonically descending', async () => {
        const bad = JSON.parse(JSON.stringify(bosses));
        bad[0].phases = [
            { hpThreshold: 0.33, mode: 'a', actionIntervalSec: 1 },
            { hpThreshold: 1.0, mode: 'b', actionIntervalSec: 1 },
        ];
        const mgr = new ConfigMgr(makeLoader({ 'configs/bosses': bad }));
        await expect(mgr.load()).rejects.toThrow(/descending hpThreshold/);
    });

    it('rejects a story with fewer than 3 pages (req 19.2)', async () => {
        const bad = JSON.parse(JSON.stringify(stories));
        bad[0].pages = bad[0].pages.slice(0, 2);
        const mgr = new ConfigMgr(makeLoader({ 'configs/stories': bad }));
        await expect(mgr.load()).rejects.toThrow(/≥3 pages/);
    });

    it('rejects a story whose maxDurationSec exceeds the 30s budget (req 19.1)', async () => {
        const bad = JSON.parse(JSON.stringify(stories));
        bad[0].maxDurationSec = 45;
        const mgr = new ConfigMgr(makeLoader({ 'configs/stories': bad }));
        await expect(mgr.load()).rejects.toThrow(/30s budget/);
    });

    it('rejects a story with non-contiguous page indices', async () => {
        const bad = JSON.parse(JSON.stringify(stories));
        bad[0].pages[1].index = 5;
        const mgr = new ConfigMgr(makeLoader({ 'configs/stories': bad }));
        await expect(mgr.load()).rejects.toThrow(/contiguous/);
    });

    it('rejects an empty enemies list', async () => {
        const mgr = new ConfigMgr(makeLoader({ 'configs/enemies': [] }));
        await expect(mgr.load()).rejects.toThrow(/enemies list is empty/);
    });
});
