import { describe, expect, it } from 'vitest';
import { ENEMY_KINDS, withDefaults } from '../../../data/enemies';
import { EnemyBrain, NOISE_HEAR_LEVEL, NOISE_WAKE_LEVEL, type EnemySenses } from '../EnemyBrain';
import { CombatWorld } from '../CombatWorld';
import type { IsometricTileMap, AudioManager, EventBus } from '@rpg/engine';

/** Тик ИИ на 1/60 с; возвращает последнее намерение. */
const run = (b: EnemyBrain, seconds: number, senses: EnemySenses): ReturnType<EnemyBrain['update']> => {
    let intent: ReturnType<EnemyBrain['update']> = { type: 'idle' };
    for (let i = 0; i < Math.round(seconds * 60); i++) intent = b.update(1 / 60, senses);
    return intent;
};

/** Патрульные точки в юнитах и сенсоры «герой далеко и не виден». */
const points = [
    { x: 10.5, y: 7.5 },
    { x: 12.5, y: 9.5 }
];
const far = (pos: { x: number; y: number }): EnemySenses => ({
    dist: 30,
    dirToPlayer: { x: 1, y: 0 },
    pos,
    canSee: false,
    home: { x: 10.5, y: 7.5 },
    hpFraction: 1
});

describe('EnemyBrain: патруль', () => {
    it('с патрулём стартует бодрым в patrol, без — спит в пепле', () => {
        const patrolled = new EnemyBrain(withDefaults(ENEMY_KINDS.crawler), { points, pauseSec: 1 });
        expect(patrolled.state).toBe('patrol');
        expect(patrolled.asleep).toBe(false);
        const sleeper = new EnemyBrain(ENEMY_KINDS.crawler);
        expect(sleeper.state).toBe('dormant');
        expect(sleeper.asleep).toBe(true);
    });

    it('дошёл до точки — пауза, потом идёт к следующей (ping-pong)', () => {
        const b = new EnemyBrain(ENEMY_KINDS.heavy, { points, pauseSec: 1 });
        // Стоим на точке 0 — сразу пауза
        expect(b.update(1 / 60, far(points[0]!)).type).toBe('idle');
        run(b, 1.1, far(points[0]!));
        // Пауза истекла — движение к точке 1 (вниз-вправо, диагональ изометрии)
        const move = run(b, 0.05, far(points[0]!));
        expect(move.type).toBe('move');
        if (move.type === 'move') {
            expect(move.dir.x).toBeCloseTo(0.707, 1);
            expect(move.dir.y).toBeCloseTo(0.707, 1);
            expect(move.run).toBeUndefined(); // патруль шагом, не бегом
        }
        // Дошли до точки 1 — снова пауза и разворот назад
        run(b, 3.2, far(points[1]!));
        const back = run(b, 0.05, far(points[1]!));
        expect(back.type).toBe('move');
        if (back.type === 'move') expect(back.dir.x).toBeLessThan(0);
    });

    it('шум уровня 0.35 вырывает из патруля в настороженность', () => {
        const b = new EnemyBrain(ENEMY_KINDS.crawler, { points, pauseSec: 1 });
        const pos = { x: 10.5, y: 7.5 };
        b.update(1 / 60, { ...far(pos), noise: { origin: { x: 11.5, y: 8.5 }, level: NOISE_HEAR_LEVEL } });
        expect(b.state).toBe('wary');
        // Идёт к источнику шума
        const move = b.update(1 / 60, { ...far(pos), noise: null });
        expect(move.type).toBe('move');
    });

    it('тихий шум (ниже порога) не настораживает', () => {
        const b = new EnemyBrain(ENEMY_KINDS.crawler, { points, pauseSec: 1 });
        b.update(1 / 60, { ...far(points[0]!), noise: { origin: { x: 11, y: 8 }, level: 0.2 } });
        expect(b.state).toBe('patrol');
    });
});

describe('EnemyBrain: настороженность', () => {
    it('увидел героя в aggroRange — погоня', () => {
        const b = new EnemyBrain(ENEMY_KINDS.crawler, { points, pauseSec: 1 });
        const pos = { x: 10.5, y: 7.5 };
        b.update(1 / 60, { ...far(pos), noise: { origin: pos, level: 0.5 } });
        expect(b.state).toBe('wary');
        b.update(1 / 60, { dist: 3, dirToPlayer: { x: 1, y: 0 }, pos, canSee: true });
        expect(b.state).toBe('chase');
    });

    it('герой не показался за warySec — возврат к дому', () => {
        const b = new EnemyBrain(ENEMY_KINDS.heavy, { points, pauseSec: 1 }); // warySec 3
        const pos = { x: 10.5, y: 7.5 };
        b.update(1 / 60, { ...far(pos), noise: { origin: { x: 11, y: 8 }, level: 0.5 } });
        run(b, 1, { ...far({ x: 11, y: 8 }), noise: null });
        expect(b.state).toBe('wary');
        run(b, 2.2, { ...far({ x: 11, y: 8 }), noise: null });
        expect(b.state).toBe('return');
        // Идёт к дому и на месте — снова в патруль
        run(b, 3, far({ x: 10.7, y: 7.7 }));
        expect(b.state).toBe('patrol');
    });
});

describe('EnemyBrain: погоня и отступление', () => {
    it('потерял героя из виду дольше deaggroSec — возврат', () => {
        const b = new EnemyBrain(ENEMY_KINDS.crawler);
        b.hearLoud();
        run(b, 0.7, { dist: 3, dirToPlayer: { x: 1, y: 0 } });
        expect(b.state).toBe('chase');
        // Герой скрылся: 2.5 с без LOS — и враг уходит домой (позицию двигаем руками)
        let pos = { x: 10, y: 10 };
        for (let i = 0; i < Math.round(2.6 * 60); i++) {
            b.update(1 / 60, { dist: 3, dirToPlayer: { x: 1, y: 0 }, canSee: false, pos, home: { x: 5, y: 5 } });
            pos.x -= 0.007;
        }
        expect(b.state).toBe('return');
    });

    it('герой дальше 1.5 aggro — не гоняется по всей карте', () => {
        const b = new EnemyBrain(ENEMY_KINDS.crawler);
        b.hearLoud();
        run(b, 0.7, { dist: 3, dirToPlayer: { x: 1, y: 0 } });
        run(b, 0.1, { dist: 7, dirToPlayer: { x: 1, y: 0 } }); // 1.5 * aggro 4 = 6
        expect(b.state).toBe('return');
    });

    it('мало hp — бежит прочь бегом, оторвался — успокоился', () => {
        const b = new EnemyBrain(withDefaults(ENEMY_KINDS.heavy)); // fleeBelowHpFraction 0.35
        const chase: EnemySenses = { dist: 1, dirToPlayer: { x: 1, y: 0 } };
        b.hearLoud();
        run(b, 0.7, chase);
        run(b, 0.1, { ...chase, hpFraction: 0.3 });
        expect(b.state).toBe('flee');
        const flee = b.update(1 / 60, { ...chase, hpFraction: 0.3 });
        expect(flee.type).toBe('move');
        if (flee.type === 'move') {
            expect(flee.dir.x).toBe(-1); // прочь от героя
            expect(flee.run).toBe(true);
        }
        // Герой отстал на fleeRange (4.5) — отбой в настороженность
        run(b, 0.1, { dist: 4.6, dirToPlayer: { x: 1, y: 0 }, hpFraction: 0.3 });
        expect(b.state).toBe('wary');
    });

    it('возврат без патруля — дошёл до дома и снова спит', () => {
        const b = new EnemyBrain(ENEMY_KINDS.crawler);
        b.hearLoud();
        run(b, 0.7, { dist: 3, dirToPlayer: { x: 1, y: 0 } });
        run(b, 2.6, { dist: 3, dirToPlayer: { x: 1, y: 0 }, canSee: false, home: { x: 5, y: 5 } });
        run(b, 5, { dist: 30, dirToPlayer: { x: 1, y: 0 }, canSee: false, pos: { x: 4.8, y: 4.8 }, home: { x: 5, y: 5 } });
        expect(b.state).toBe('dormant');
        expect(b.asleep).toBe(true);
    });
});

// ---------- CombatWorld: пороги шума ----------

/** Фейковая карта: данные 20×20 травы, проходима всюду (Pixi не нужен). */
function fakeMap(): IsometricTileMap {
    const data = {
        width: 20,
        height: 20,
        tiles: new Array(400).fill(0),
        blocked: [],
        tall: undefined
    };
    return { data, isWalkable: () => true } as unknown as IsometricTileMap;
}

function fakeWorld(playerPos: { x: number; y: number }): CombatWorld {
    return new CombatWorld({
        map: fakeMap(),
        events: { emit: () => {} } as unknown as EventBus,
        audio: { play: () => Promise.resolve() } as unknown as AudioManager,
        getPlayerPos: () => playerPos,
        damagePlayer: () => {}
    });
}

describe('CombatWorld: шум будит только от гулкого', () => {
    it('шаги героя (0.35) не будят спящего, гулкий звон (1.0) будит', () => {
        const hero = { x: 20, y: 20 };
        const w = fakeWorld(hero);
        const e = w.spawnEnemy('crawler', { x: 10.5, y: 10.5 });
        const en = w.enemies.get(e)!;
        expect(en.brain.asleep).toBe(true);

        w.noise({ x: 10.5, y: 10.5 }, 0.35); // шаги прямо рядом
        w.update(1 / 60);
        expect(en.brain.asleep).toBe(true);

        w.noise({ x: 10.5, y: 10.5 }, NOISE_WAKE_LEVEL); // гулкий удар
        expect(en.brain.asleep).toBe(false);
        expect(en.brain.state).toBe('rise');
    });

    it('бодрый патрульный слышит тихий шум в hearRadius и настораживается', () => {
        const hero = { x: 20, y: 20 };
        const w = fakeWorld(hero);
        const e = w.spawnEnemy('heavy', { x: 10.5, y: 10.5 }, {
            patrol: { points: [{ x: 10, y: 10 }, { x: 12, y: 12 }], pauseSec: 1 }
        });
        const en = w.enemies.get(e)!;
        expect(en.brain.state).toBe('patrol');
        // Шаги героя в 1 юните — в hearRadius 4
        w.noise({ x: 11.5, y: 10.5 }, 0.35);
        w.update(1 / 60);
        expect(en.brain.state).toBe('wary');
    });

    it('шум вне радиуса слуха не слышен', () => {
        const hero = { x: 20, y: 20 };
        const w = fakeWorld(hero);
        const e = w.spawnEnemy('crawler', { x: 10.5, y: 10.5 }, {
            patrol: { points: [{ x: 10, y: 10 }, { x: 12, y: 12 }], pauseSec: 1 }
        });
        const en = w.enemies.get(e)!;
        // hearRadius ползуна 3.5, шум в 20 юнитах
        w.noise({ x: 18.5, y: 10.5 }, 0.5);
        w.update(1 / 60);
        expect(en.brain.state).toBe('patrol');
    });

    it('инварианты боевого мира пусты на честном ИИ', () => {
        const hero = { x: 20, y: 20 };
        const w = fakeWorld(hero);
        w.spawnEnemy('crawler', { x: 10.5, y: 10.5 }, {
            patrol: { points: [{ x: 10, y: 10 }, { x: 12, y: 12 }], pauseSec: 1 }
        });
        for (let i = 0; i < 600; i++) w.update(1 / 60);
        expect(w.agentInvariants()).toEqual([]);
    });
});