import 'dart:typed_data';

import 'package:test/test.dart';
import 'package:wolf_3d_dart/src/rendering/renderer_backend.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('standard VGA HUD renders current player lives value', () {
    final engine = _buildEngine();
    engine.init();
    engine.player.lives = 7;

    final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
    renderer.drawHudForTest(engine);

    final expectedDigitIndex = engine.data.registry.hud
        .resolve(HudKey.digit7)
        ?.vgaIndex;
    expect(expectedDigitIndex, isNotNull);

    final livesDigitCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 176 && call.startX == 112,
      orElse: () => throw StateError('Lives digit was not rendered.'),
    );

    expect(livesDigitCall.imageIndex, expectedDigitIndex);
  });

  test('standard VGA HUD renders gold key icon when collected', () {
    final engine = _buildEngine();
    engine.init();
    engine.player.hasGoldKey = true;

    final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
    renderer.drawHudForTest(engine);

    final expectedGoldKeyIndex = engine.data.registry.hud
        .resolve(HudKey.goldKeyIcon)
        ?.vgaIndex;
    final expectedNoKeyIndex = engine.data.registry.hud
        .resolve(HudKey.noKeyIcon)
        ?.vgaIndex;
    expect(expectedGoldKeyIndex, isNotNull);
    expect(expectedNoKeyIndex, isNotNull);

    final goldKeyCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 164 && call.startX == 240,
      orElse: () => throw StateError('Gold key slot was not rendered.'),
    );
    final silverKeyCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 180 && call.startX == 240,
      orElse: () => throw StateError('Silver key slot was not rendered.'),
    );

    expect(goldKeyCall.imageIndex, expectedGoldKeyIndex);
    expect(silverKeyCall.imageIndex, expectedNoKeyIndex);
  });

  test('standard VGA HUD face follows current health band', () {
    final engine = _buildEngine();
    engine.init();
    engine.player.health = 20;

    final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
    renderer.drawHudForTest(engine);

    final expectedFaceIndex = engine.data.registry.hud
        .faceForHealth(engine.player.health)
        ?.vgaIndex;
    expect(expectedFaceIndex, isNotNull);

    final faceCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 164 && call.startX == 136,
      orElse: () => throw StateError('Face slot was not rendered.'),
    );

    expect(faceCall.imageIndex, expectedFaceIndex);
  });

  test('chaingun pickup shows GOTGATLING face then returns to health face', () {
    final engine = _buildEngine();
    engine.init();
    engine.player.health = 100;

    const int chainGunSfxId = 38;
    const int chainGunDurationMs = 350;
    engine.data.sounds[chainGunSfxId] = PcmSound(
      Uint8List(chainGunDurationMs * 7),
    );

    final chainGun = WeaponCollectible(
      x: engine.player.x,
      y: engine.player.y,
      mapId: MapObject.chainGun,
    );
    engine.entities.add(chainGun);
    engine.tick(const Duration(milliseconds: 16));

    final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
    renderer.drawHudForTest(engine);

    final expectedGotGatlingIndex = engine.data.registry.hud
        .resolve(HudKey.faceGotGatling)
        ?.vgaIndex;
    expect(expectedGotGatlingIndex, isNotNull);

    final gotGatlingFaceCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 164 && call.startX == 136,
      orElse: () => throw StateError('Face slot was not rendered.'),
    );
    expect(gotGatlingFaceCall.imageIndex, expectedGotGatlingIndex);

    engine.player.tick(Duration(milliseconds: chainGunDurationMs + 50));

    final rendererAfterTimeout = _HudProbeRenderer(
      vgaImages: engine.data.vgaImages,
    );
    rendererAfterTimeout.drawHudForTest(engine);

    final expectedHealthFaceIndex = engine.data.registry.hud
        .faceForHealth(engine.player.health)
        ?.vgaIndex;
    expect(expectedHealthFaceIndex, isNotNull);

    final healthFaceCall = rendererAfterTimeout.drawCalls.firstWhere(
      (call) => call.startY == 164 && call.startX == 136,
      orElse: () =>
          throw StateError('Face slot was not rendered after timeout.'),
    );
    expect(healthFaceCall.imageIndex, expectedHealthFaceIndex);
  });

  test(
    'standard VGA HUD face animates between health-band frames over time',
    () {
      final engine = _buildEngine();
      engine.init();
      engine.player.health = 100;

      final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
      renderer.drawHudForTest(engine);
      final initialFaceCall = renderer.drawCalls.firstWhere(
        (call) => call.startY == 164 && call.startX == 136,
        orElse: () => throw StateError('Initial face slot was not rendered.'),
      );

      engine.player.tick(const Duration(milliseconds: 4500));

      final rendererAfter = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
      rendererAfter.drawHudForTest(engine);
      final animatedFaceCall = rendererAfter.drawCalls.firstWhere(
        (call) => call.startY == 164 && call.startX == 136,
        orElse: () => throw StateError('Animated face slot was not rendered.'),
      );

      expect(
        animatedFaceCall.imageIndex,
        isNot(equals(initialFaceCall.imageIndex)),
      );
    },
  );

  test('fatal mutant hit shows mutant death face', () {
    final engine = _buildEngine();
    engine.init();
    engine.player.health = 10;
    engine.player.takeDamage(20, attackerType: EnemyType.mutant);

    final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
    renderer.drawHudForTest(engine);

    final expectedMutantDeathFaceIndex = engine.data.registry.hud
        .resolve(HudKey.faceMutantDeath)
        ?.vgaIndex;
    expect(expectedMutantDeathFaceIndex, isNotNull);

    final faceCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 164 && call.startX == 136,
      orElse: () => throw StateError('Face slot was not rendered.'),
    );
    expect(faceCall.imageIndex, expectedMutantDeathFaceIndex);
  });

  test('god mode face renders in spear demo HUD mapping', () {
    final engine = _buildEngine(
      version: GameVersion.spearOfDestinyDemo,
      registry: SpearDemoAssetRegistry(),
    );
    engine.init();
    engine.player.health = 90;
    engine.player.setGodModeFaceEnabled(true);

    final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
    renderer.drawHudForTest(engine);

    final expectedGodModeFaceIndex = engine.data.registry.hud
        .resolve(HudKey.faceGodMode)
        ?.vgaIndex;
    expect(expectedGodModeFaceIndex, isNotNull);

    final faceCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 164 && call.startX == 136,
      orElse: () => throw StateError('Face slot was not rendered.'),
    );
    expect(faceCall.imageIndex, expectedGodModeFaceIndex);
  });

  test('god mode face falls back to health face when unmapped', () {
    final engine = _buildEngine();
    engine.init();
    engine.player.health = 90;
    engine.player.setGodModeFaceEnabled(true);

    final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
    renderer.drawHudForTest(engine);

    final expectedHealthFaceIndex = engine.data.registry.hud
        .faceForHealth(engine.player.health)
        ?.vgaIndex;
    expect(expectedHealthFaceIndex, isNotNull);

    final faceCall = renderer.drawCalls.firstWhere(
      (call) => call.startY == 164 && call.startX == 136,
      orElse: () => throw StateError('Face slot was not rendered.'),
    );
    expect(faceCall.imageIndex, expectedHealthFaceIndex);
  });
}

class _HudProbeRenderer extends RendererBackend<int> {
  final List<VgaImage> vgaImages;
  final List<_HudDrawCall> drawCalls = <_HudDrawCall>[];

  _HudProbeRenderer({required this.vgaImages});

  void drawHudForTest(WolfEngine engine) {
    drawStandardVgaHud(engine);
  }

  @override
  void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
    drawCalls.add(
      _HudDrawCall(
        imageIndex: vgaImages.indexOf(image),
        startX: startX320,
        startY: startY200,
      ),
    );
  }

  @override
  int finalizeFrame() => 0;

  @override
  void drawHud(WolfEngine engine) {}

  @override
  void drawSpriteStripe(
    int stripeX,
    int drawStartY,
    int drawEndY,
    int spriteHeight,
    Sprite texture,
    int texX,
    double transformY,
  ) {}

  @override
  void drawWallColumn(
    int x,
    int drawStart,
    int drawEnd,
    int columnHeight,
    Sprite texture,
    int texX,
    double perpWallDist,
    int side,
  ) {}

  @override
  void drawWeapon(WolfEngine engine) {}

  @override
  void prepareFrame(WolfEngine engine) {}
}

class _HudDrawCall {
  final int imageIndex;
  final int startX;
  final int startY;

  const _HudDrawCall({
    required this.imageIndex,
    required this.startX,
    required this.startY,
  });
}

WolfEngine _buildEngine({
  GameVersion version = GameVersion.retail,
  AssetRegistry? registry,
}) {
  final wallGrid = _buildGrid();
  final objectGrid = _buildGrid();

  _fillBoundaries(wallGrid, 2);
  objectGrid[2][2] = MapObject.playerEast;

  return WolfEngine(
    data: WolfensteinData(
      version: version,
      dataVersion: DataVersion.unknown,
      registry: 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: List.generate(220, (_) => _vgaStub()),
      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(96, 96),
    input: _StaticInput(),
    onGameWon: () {},
  );
}

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

VgaImage _vgaStub() {
  return VgaImage(
    width: 4,
    height: 1,
    pixels: Uint8List.fromList([0, 0, 0, 0]),
  );
}

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 colorIndex) {
  return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex)));
}
