import 'dart:math' as math;

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

void main() {
  group('Enemy map IDs', () {
    test('maps officers, dogs, and mutants to the correct enemy types', () {
      expect(EnemyType.fromMapId(116), EnemyType.officer);
      expect(EnemyType.fromMapId(134), EnemyType.dog);
      expect(EnemyType.fromMapId(216), EnemyType.mutant);
    });

    test('uses the original mutant 18-ID difficulty offset', () {
      expect(
        EnemyType.mutant.mapData.getNormalizedId(216, Difficulty.easy),
        216,
      );
      expect(
        EnemyType.mutant.mapData.getNormalizedId(234, Difficulty.medium),
        216,
      );
      expect(
        EnemyType.mutant.mapData.getNormalizedId(252, Difficulty.hard),
        216,
      );
    });
  });

  group('Enemy spawn facing', () {
    test('spawns dog directions using the original dog map IDs', () {
      expect(_spawnEnemy(134).angle, CardinalDirection.east.radians);
      expect(_spawnEnemy(135).angle, CardinalDirection.north.radians);
      expect(_spawnEnemy(136).angle, CardinalDirection.west.radians);
      expect(_spawnEnemy(137).angle, CardinalDirection.south.radians);
    });

    test('maps patrol variants to the same facing directions', () {
      expect(_spawnEnemy(138).angle, CardinalDirection.east.radians);
      expect(_spawnEnemy(139).angle, CardinalDirection.north.radians);
      expect(_spawnEnemy(140).angle, CardinalDirection.west.radians);
      expect(_spawnEnemy(141).angle, CardinalDirection.south.radians);
    });

    test('spawns standing variants as idle', () {
      expect(_spawnEnemy(134).state, EntityState.ambushing);
      expect(_spawnEnemy(135).state, EntityState.ambushing);
      expect(_spawnEnemy(136).state, EntityState.ambushing);
      expect(_spawnEnemy(137).state, EntityState.ambushing);
    });

    test('spawns patrol variants as patrolling', () {
      expect(_spawnEnemy(138).state, EntityState.patrolling);
      expect(_spawnEnemy(139).state, EntityState.patrolling);
      expect(_spawnEnemy(140).state, EntityState.patrolling);
      expect(_spawnEnemy(141).state, EntityState.patrolling);
    });

    test('standing guard and dog do not move before alert', () {
      final guard = _spawnEnemy(108); // guard standing east
      final dog = _spawnEnemy(134); // dog standing east

      final guardIntent = guard.update(
        elapsedMs: 1000,
        elapsedDeltaMs: 16,
        playerPosition: const Coordinate2D(20, 20),
        playerAngle: 0,
        isPlayerRunning: false,
        isWalkable: (_, _) => false,
        areaAt: (_, _) => 0,
        isAreaConnectedToPlayer: (_) => true,
        onDamagePlayer: (_) {},
        tryOpenDoor: (_, _) {},
        onPlaySound: (_) {},
      );

      final dogIntent = dog.update(
        elapsedMs: 1000,
        elapsedDeltaMs: 16,
        playerPosition: const Coordinate2D(20, 20),
        playerAngle: 0,
        isPlayerRunning: false,
        isWalkable: (_, _) => false,
        areaAt: (_, _) => 0,
        isAreaConnectedToPlayer: (_) => true,
        onDamagePlayer: (_) {},
        tryOpenDoor: (_, _) {},
        onPlaySound: (_) {},
      );

      expect(guardIntent.movement.x, 0);
      expect(guardIntent.movement.y, 0);
      expect(dogIntent.movement.x, 0);
      expect(dogIntent.movement.y, 0);
    });

    test('patrol guard and dog produce movement intent', () {
      final guard = _spawnEnemy(112); // guard patrol east
      final dog = _spawnEnemy(138); // dog patrol east

      final guardIntent = guard.update(
        elapsedMs: 1000,
        elapsedDeltaMs: 16,
        playerPosition: const Coordinate2D(40, 40),
        playerAngle: 0,
        isPlayerRunning: false,
        isWalkable: (_, _) => true,
        areaAt: (_, _) => 0,
        isAreaConnectedToPlayer: (_) => true,
        onDamagePlayer: (_) {},
        tryOpenDoor: (_, _) {},
        onPlaySound: (_) {},
      );

      final dogIntent = dog.update(
        elapsedMs: 1000,
        elapsedDeltaMs: 16,
        playerPosition: const Coordinate2D(40, 40),
        playerAngle: 0,
        isPlayerRunning: false,
        isWalkable: (_, _) => true,
        areaAt: (_, _) => 0,
        isAreaConnectedToPlayer: (_) => true,
        onDamagePlayer: (_) {},
        tryOpenDoor: (_, _) {},
        onPlaySound: (_) {},
      );

      expect(
        guardIntent.movement.x.abs() + guardIntent.movement.y.abs(),
        greaterThan(0),
      );
      expect(
        dogIntent.movement.x.abs() + dogIntent.movement.y.abs(),
        greaterThan(0),
      );
    });

    test(
      'alerted dog can choose alternate path when direct chase is blocked',
      () {
        final dog = Dog(x: 8.99, y: 8.99, angle: 0, mapId: MapObject.dogStart);
        dog.isAlerted = true;

        final intent = dog.update(
          elapsedMs: 1000,
          elapsedDeltaMs: 16,
          playerPosition: const Coordinate2D(11.99, 8.99),
          playerAngle: 0,
          isPlayerRunning: false,
          isWalkable: (x, y) {
            // Block the direct east chase tile only.
            if (x == 9 && y == 8) return false;
            return true;
          },
          areaAt: (_, _) => 0,
          isAreaConnectedToPlayer: (_) => true,
          onDamagePlayer: (_) {},
          tryOpenDoor: (_, _) {},
          onPlaySound: (_) {},
        );

        expect(
          intent.movement.x.abs() + intent.movement.y.abs(),
          greaterThan(0),
        );
      },
    );

    test('alerted dog chase does not increase player distance', () {
      final dog = Dog(x: 10.5, y: 10.5, angle: 0, mapId: MapObject.dogStart);
      dog
        ..isAlerted = true
        ..state = EntityState.patrolling;

      final playerPosition = const Coordinate2D(8.5, 10.5);
      final double before = dog.position.distanceTo(playerPosition);

      final intent = dog.update(
        elapsedMs: 1000,
        elapsedDeltaMs: 16,
        playerPosition: playerPosition,
        playerAngle: 0,
        isPlayerRunning: false,
        isWalkable: (x, y) {
          // Block direct west and diagonals to force fallback selection.
          if (x == 9 && y == 10) return false;
          if (x == 9 && y == 9) return false;
          if (x == 9 && y == 11) return false;
          return true;
        },
        areaAt: (_, _) => 0,
        isAreaConnectedToPlayer: (_) => true,
        onDamagePlayer: (_) {},
        tryOpenDoor: (_, _) {},
        onPlaySound: (_) {},
      );

      final after = (dog.position + intent.movement).distanceTo(playerPosition);
      expect(after, lessThanOrEqualTo(before + 0.001));
    });

    test('blocked cardinal movement yields exact zero vector', () {
      final dog = Dog(
        x: 10.5,
        y: 10.5,
        angle: -1.57079632679, // north
        mapId: MapObject.dogStart,
      );

      final movement = dog.getValidMovement(
        intendedMovement: const Coordinate2D(1e-7, -0.6),
        playerPosition: const Coordinate2D(30, 30),
        isWalkable: (x, y) {
          // Block tile directly north of dog.
          if (x == 10 && y == 9) return false;
          return true;
        },
        tryOpenDoor: (_, _) {},
      );

      expect(movement.x, 0);
      expect(movement.y, 0);
    });

    test('patrol tile-step keeps north heading in map coordinates', () {
      final guard = Guard(
        x: 20.5,
        y: 20.5,
        angle: -math.pi / 2,
        mapId: MapObject.guardStart,
      );

      final movement = guard.getPatrolPathMovement(
        moveSpeed: 0.1,
        playerPosition: const Coordinate2D(40, 40),
        isWalkable: (_, _) => true,
        tryOpenDoor: (_, _) {},
      );

      expect(movement.y, lessThan(0));
    });
  });
}

Enemy _spawnEnemy(int mapId) {
  final enemy = Enemy.spawn(mapId, 8.5, 8.5, Difficulty.hard);
  expect(enemy, isNotNull);
  return enemy!;
}
