import { describe, it, expect } from 'bun:test';
import { Game } from '../src/core/Game';
import { Rng } from '../src/core/rng';
import { Enemy } from '../src/core/entities/Enemy';
import { collidesWall } from '../src/core/systems/collision';
import { emptyInput, type InputState } from '../src/input/InputState';
import { DEFAULT_RULES, PRESETS } from '../src/core/rules';
import { MODE_RANGED, MODE_MELEE, OX, OY, TILE, COLS, RW } from '../src/config';

function input(patch: Partial<InputState> = {}): InputState {
  return { ...emptyInput(), ...patch };
}

describe('Game', () => {
  it('РЕГРЕССИЯ: конструируется без ошибок и игрок появляется не в стене', () => {
    const game = new Game(DEFAULT_RULES, new Rng(1));
    expect(game.curRoom).toBeDefined();
    expect(game.curRoom.type).toBe('spawn');
    expect(collidesWall(game.player.box, game.curRoom)).toBe(false);
  });

  it('600 пустых шагов не падают, игрок не застревает в стене, HP цело на спавне', () => {
    const game = new Game(DEFAULT_RULES, new Rng(2));
    const hp0 = game.player.hp;
    for (let i = 0; i < 600; i++) game.step(input());
    expect(collidesWall(game.player.box, game.curRoom)).toBe(false);
    expect(game.player.hp).toBe(hp0); // на спавне врагов нет
    expect(game.gameOver).toBe(false);
  });

  it('смена оружия по toggleWeapon', () => {
    const game = new Game(DEFAULT_RULES, new Rng(3));
    expect(game.player.mode).toBe(MODE_RANGED);
    game.consumeActions(input({ toggleWeapon: true }));
    expect(game.player.mode).toBe(MODE_MELEE);
    game.consumeActions(input({ toggleWeapon: true }));
    expect(game.player.mode).toBe(MODE_RANGED);
  });

  it('стрельба создаёт снаряд, который потом исчезает', () => {
    const game = new Game(DEFAULT_RULES, new Rng(4));
    game.step(input({ aimVec: { x: 1, y: 0 } }));
    expect(game.curRoom.tears.length).toBe(1);
    // Снаряд летит вправо и со временем гаснет (стена/время жизни).
    for (let i = 0; i < 200; i++) game.step(input());
    expect(game.curRoom.tears.length).toBe(0);
  });

  it('РЕГРЕССИЯ: комната без врагов (сокровищница) зачищается при входе — иначе двери не открыть', () => {
    // Ищем seed, где в карте есть сокровищница.
    let tested = false;
    for (let seed = 1; seed <= 200 && !tested; seed++) {
      const game = new Game(DEFAULT_RULES, new Rng(seed));
      for (const room of game.roomMap.rooms.values()) {
        if (room.type !== 'treasure') continue;
        game.cc = room.c;
        game.cr = room.r;
        game.enterRoom('up');
        expect(room.enemies.length).toBe(0);
        expect(room.cleared).toBe(true); // иначе игрок застрянет без дверей
        tested = true;
        break;
      }
    }
    expect(tested).toBe(true); // среди 200 seed сокровищница точно нашлась
  });

  it('РЕГРЕССИЯ: кнокбэк не выбрасывает врага сквозь стену (нет софт-лока)', () => {
    const game = new Game(DEFAULT_RULES, new Rng(7));
    // Переносим игру в любую боевую (normal) комнату.
    let placed = false;
    for (const room of game.roomMap.rooms.values()) {
      if (room.type !== 'normal') continue;
      game.cc = room.c;
      game.cr = room.r;
      game.enterRoom('up');
      placed = true;
      break;
    }
    expect(placed).toBe(true);

    const room = game.curRoom;
    // Один контролируемый враг у правой стены, кнокбэк направлен В стену.
    const e = new Enemy(OX + (COLS - 2) * TILE + TILE / 2, OY + 5 * TILE + TILE / 2, 'normal');
    e.knx = 50; // заведомо больше толщины стены (1 тайл)
    e.kny = 0;
    room.enemies = [e];

    for (let i = 0; i < 40; i++) game.step(emptyInput());

    expect(collidesWall(e.box, room)).toBe(false); // остался внутри и достижим
    expect(e.x).toBeLessThan(OX + RW);              // не вылетел за правую стену
  });

  it('правила уровня влияют на забег: HP игрока и размер карты', () => {
    const hardcore = PRESETS.find((p) => p.id === 'hardcore')!;
    const big = PRESETS.find((p) => p.id === 'big')!;

    const gHard = new Game(hardcore, new Rng(11));
    expect(gHard.player.maxHp).toBe(hardcore.player.maxHp); // меньше базовых 6

    // «Большой данжен» в среднем даёт больше комнат, чем «Стандарт».
    const avg = (rules: typeof big) => {
      let sum = 0;
      for (let s = 1; s <= 20; s++) sum += new Game(rules, new Rng(s)).roomMap.rooms.size;
      return sum / 20;
    };
    expect(avg(big)).toBeGreaterThan(avg(DEFAULT_RULES));
  });

  it('фикс-сид даёт одинаковую карту каждый раз — в т.ч. после reset()', () => {
    const daily = PRESETS.find((p) => p.id === 'daily')!;
    const a = new Game(daily);
    const keys = [...a.roomMap.rooms.keys()].sort();

    // другой экземпляр — та же карта
    const b = new Game(daily);
    expect([...b.roomMap.rooms.keys()].sort()).toEqual(keys);

    // РЕГРЕССИЯ: после рестарта по [R] фикс-сид воспроизводит тот же данжен
    a.gameOver = true;
    a.reset();
    expect([...a.roomMap.rooms.keys()].sort()).toEqual(keys);
  });

  it('reset() возвращает чистое состояние', () => {
    const game = new Game(DEFAULT_RULES, new Rng(5));
    game.player.hp = 1;
    game.gameOver = true;
    game.reset();
    expect(game.gameOver).toBe(false);
    expect(game.player.hp).toBe(game.player.maxHp);
    expect(game.cc).toBe(0);
    expect(game.cr).toBe(0);
  });

  it('РЕГРЕССИЯ: бесконечный спуск применяет усиление врагов при спавне комнат', () => {
    const endless = { ...PRESETS.find((p) => p.id === 'endless')!, seed: 100 };
    const game = new Game(endless);

    for (let targetFloor = 2; targetFloor <= 5; targetFloor++) {
      const bossRoom = [...game.roomMap.rooms.values()].find((room) => room.type === 'boss')!;
      bossRoom.cleared = true;
      game.step(input());
      expect(game.floor).toBe(targetFloor);
    }

    const bossRoom = [...game.roomMap.rooms.values()].find((room) => room.type === 'boss')!;
    game.cc = bossRoom.c;
    game.cr = bossRoom.r;
    game.enterRoom('up');

    const boss = bossRoom.enemies.find((e) => e.type === 'boss')!;
    expect(boss.maxHp).toBeGreaterThan(10);
  });

  it('снаряд берёт урон из выбранного оружия', () => {
    const game = new Game(DEFAULT_RULES, new Rng(12));
    game.player.addWeapon('boomerang');

    game.step(input({ aimVec: { x: 1, y: 0 } }));

    expect(game.curRoom.tears[0].type).toBe('boomerang');
    expect(game.curRoom.tears[0].damage).toBe(2);
  });

  it('РЕГРЕССИЯ: эффект снаряда не зависит от смены оружия после выстрела', () => {
    const game = new Game(DEFAULT_RULES, new Rng(13));
    game.player.addWeapon('staff');

    const enemy = new Enemy(game.player.x + 7, game.player.y, 'tank');
    game.curRoom.enemies = [enemy];

    game.step(input({ aimVec: { x: 1, y: 0 } }));
    expect(enemy.burnTimer).toBeGreaterThan(0);

    const hpAfterHit = enemy.hp;
    game.player.addWeapon('tears');
    for (let i = 0; i < 10; i++) game.step(input());

    expect(enemy.hp).toBeLessThan(hpAfterHit);
  });
});
