import 'dart:typed_data';

import 'package:test/test.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';

void main() {
  test(
    'captureSaveState and restoreSaveState round-trip live session state',
    () {
      final engine = _buildEngine();
      engine.init();

      engine.player
        ..health = 47
        ..ammo = 33
        ..score = 1200
        ..lives = 5
        ..x = 10.5
        ..y = 11.5
        ..angle = 1.25
        ..hasGoldKey = true
        ..hasMachineGun = true
        ..weapons[WeaponType.machineGun] = MachineGun()
        ..currentWeapon = MachineGun();

      engine.currentLevel[8][8] = 0;
      engine.currentLevel[5][6] = 98;

      final door = engine.doorManager.doors.values.first
        ..state = DoorState.opening
        ..offset = 0.42
        ..openTime = 1337;

      final pushwall = engine.pushwallManager.pushwalls.values.first
        ..dirX = 1
        ..dirY = 0
        ..offset = 0.5
        ..tilesMoved = 1;
      engine.pushwallManager.activePushwall = pushwall;

      final guard =
          EntityRegistry.spawn(
                  MapObject.guardStart,
                  12.5,
                  13.5,
                  Difficulty.medium,
                  engine.data.sprites.length,
                  registry: engine.data.registry,
                )!
                as Guard
            ..health = 17
            ..isAlerted = true
            ..state = EntityState.attacking
            ..currentFrame = 2
            ..lastActionTime = 222;

      final droppedAmmo = SmallAmmoCollectible(x: 7.5, y: 9.5)
        ..spriteIndex = 999 % engine.data.sprites.length;

      engine.entities = <Entity>[guard, droppedAmmo];

      final snapshot = engine.captureSaveState();

      engine.player
        ..health = 1
        ..ammo = 0
        ..score = 0
        ..x = 2.5
        ..y = 2.5
        ..hasGoldKey = false;
      engine.currentLevel[8][8] = 55;
      door
        ..state = DoorState.closed
        ..offset = 0.0
        ..openTime = 0;
      engine.pushwallManager.activePushwall = null;
      engine.entities = <Entity>[];

      engine.restoreSaveState(snapshot);

      expect(engine.currentGameIndex, 0);
      expect(engine.currentEpisodeIndex, 0);
      expect(engine.currentLevelIndex, 0);

      expect(engine.player.health, 47);
      expect(engine.player.ammo, 33);
      expect(engine.player.score, 1200);
      expect(engine.player.lives, 5);
      expect(engine.player.x, closeTo(10.5, 0.001));
      expect(engine.player.y, closeTo(11.5, 0.001));
      expect(engine.player.angle, closeTo(1.25, 0.001));
      expect(engine.player.hasGoldKey, isTrue);
      expect(engine.player.hasMachineGun, isTrue);
      expect(engine.player.currentWeapon.type, WeaponType.machineGun);

      expect(engine.currentLevel[8][8], 0);
      expect(engine.currentLevel[5][6], 98);

      final restoredDoor = engine.doorManager.doors.values.first;
      expect(restoredDoor.state, DoorState.opening);
      expect(restoredDoor.offset, closeTo(0.42, 0.001));
      expect(restoredDoor.openTime, 1337);

      expect(engine.pushwallManager.activePushwall, isNotNull);
      expect(
        engine.pushwallManager.activePushwall!.offset,
        closeTo(0.5, 0.001),
      );
      expect(engine.pushwallManager.activePushwall!.tilesMoved, 1);
      expect(engine.pushwallManager.activePushwall!.dirX, 1);

      expect(engine.entities, hasLength(2));
      expect(engine.entities.first, isA<Guard>());
      final restoredGuard = engine.entities.first as Guard;
      expect(restoredGuard.health, 17);
      expect(restoredGuard.isAlerted, isTrue);
      expect(restoredGuard.state, EntityState.attacking);
      expect(restoredGuard.currentFrame, 2);
      expect(engine.entities.last, isA<SmallAmmoCollectible>());
    },
  );
}

class _TestInput extends Wolf3dInput {
  @override
  void update() {}
}

class _SilentAudio implements EngineAudio {
  @override
  WolfensteinData? activeGame;

  @override
  Future<void> debugSoundTest() async {}

  @override
  Future<void> init() async {}

  @override
  void dispose() {}

  @override
  void playLevelMusic(Music music) {}

  @override
  void playMenuMusic() {}

  @override
  void playSoundEffect(SoundEffect effect) {}

  @override
  void playSoundEffectId(int sfxId) {}

  @override
  void stopMusic() {}

  @override
  Future<void> stopAllAudio() async {}
}

WolfEngine _buildEngine() {
  final wallGrid = _buildGrid();
  final objectGrid = _buildGrid();
  _fillBoundaries(wallGrid, 2);

  objectGrid[2][2] = MapObject.playerEast;
  objectGrid[4][4] = MapObject.pushwallTrigger;
  wallGrid[2][3] = 90;
  wallGrid[4][4] = 5;

  return WolfEngine(
    data: WolfensteinData(
      version: GameVersion.retail,
      dataVersion: DataVersion.unknown,
      registry: RetailAssetRegistry(),
      walls: [
        _solidSprite(1),
        _solidSprite(1),
        _solidSprite(2),
        _solidSprite(2),
      ],
      sprites: List.generate(436, (_) => _solidSprite(255)),
      sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
      adLibSounds: const [],
      music: const [],
      vgaImages: const [],
      episodes: [
        Episode(
          name: 'Episode 1',
          levels: [
            WolfLevel(
              name: 'Level 1',
              wallGrid: wallGrid,
              areaGrid: List.generate(64, (_) => List.filled(64, -1)),
              objectGrid: objectGrid,
              music: Music.level01,
            ),
          ],
        ),
      ],
    ),
    difficulty: Difficulty.medium,
    startingEpisode: 0,
    frameBuffer: FrameBuffer(64, 64),
    input: _TestInput(),
    onGameWon: () {},
    engineAudio: _SilentAudio(),
  );
}

SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));

void _fillBoundaries(SpriteMap grid, int wallId) {
  for (int i = 0; i < 64; i++) {
    grid[0][i] = wallId;
    grid[63][i] = wallId;
    grid[i][0] = wallId;
    grid[i][63] = wallId;
  }
}

Sprite _solidSprite(int paletteIndex) {
  return Sprite(Uint8List.fromList(<int>[paletteIndex]));
}
