import 'dart:typed_data';

import 'package:test/test.dart';
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.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_input.dart';
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';

void main() {
  group('Fizzle fade sequence', () {
    test('covers every canonical menu pixel exactly once', () {
      final sequence = FizzleFade.canonicalSequence;

      expect(sequence.length, FizzleFade.canonicalPixelCount);
      expect(sequence.toSet().length, FizzleFade.canonicalPixelCount);
      expect(sequence.first, 0);
      expect(FizzleFade.nextState(FizzleFade.seed), 0x12000);
    });
  });

  group('Software renderer transitions', () {
    test(
      'default intro fade-in starts fully black and reveals by hold phase',
      () {
        final engine = _buildEngine(frameBuffer: FrameBuffer(320, 200));
        final renderer = SoftwareRenderer();
        final int black = _rgbToFrameColor(0x000000);

        engine.init();

        final FrameBuffer startFrame = renderer.render(engine);
        expect(startFrame.pixels.every((pixel) => pixel == black), isTrue);

        engine.menuManager.tickTransition(MenuManager.introFadeDurationMs);

        final FrameBuffer revealedFrame = renderer.render(engine);
        expect(revealedFrame.pixels.any((pixel) => pixel != black), isTrue);
      },
    );

    test(
      'fizzle menu transition covers at midpoint and reveals target menu on completion',
      () {
        final engine = _buildEngine(frameBuffer: FrameBuffer(320, 200));
        final baselineEngine = _buildEngine(frameBuffer: FrameBuffer(320, 200));
        final difficultyEngine = _buildEngine(
          frameBuffer: FrameBuffer(320, 200),
        );
        final renderer = SoftwareRenderer();
        final int bgColor = _rgbToFrameColor(
          engine.menuManager.menuBackgroundRgb,
        );

        engine.init();
        baselineEngine.init();
        difficultyEngine.init();

        engine.menuManager.showMainMenu(hasResumableGame: false);
        baselineEngine.menuManager.showMainMenu(hasResumableGame: false);
        difficultyEngine.menuManager.beginDifficultySelection();

        final List<int> mainMenuFrame = List<int>.from(
          renderer.render(baselineEngine).pixels,
        );

        engine.menuManager.startTransition(
          WolfMenuScreen.difficultySelect,
          effect: WolfTransitionEffect.fizzleFade,
        );
        engine.menuManager.tickTransition(
          MenuManager.transitionDurationMs ~/ 4,
        );
        final List<int> partialFrame = List<int>.from(
          renderer.render(engine).pixels,
        );

        expect(
          _countDifferentPixels(partialFrame, mainMenuFrame),
          greaterThan(0),
        );

        engine.menuManager.tickTransition(
          MenuManager.transitionDurationMs ~/ 4,
        );
        final List<int> midpointFrame = List<int>.from(
          renderer.render(engine).pixels,
        );

        expect(midpointFrame.every((pixel) => pixel == bgColor), isTrue);

        engine.menuManager.tickTransition(
          MenuManager.transitionDurationMs ~/ 2,
        );
        final List<int> completedFrame = List<int>.from(
          renderer.render(engine).pixels,
        );
        final List<int> expectedDifficultyFrame = List<int>.from(
          renderer.render(difficultyEngine).pixels,
        );

        expect(completedFrame, orderedEquals(expectedDifficultyFrame));
      },
    );
  });
}

WolfEngine _buildEngine({required FrameBuffer frameBuffer}) {
  return WolfEngine(
    data: _buildTestData(),
    difficulty: null,
    startingEpisode: 0,
    frameBuffer: frameBuffer,
    input: _TestInput(),
    engineAudio: _SilentAudio(),
    onGameWon: () {},
  );
}

WolfensteinData _buildTestData() {
  final wallGrid = _buildGrid();
  final objectGrid = _buildGrid();

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

  return 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,
          ),
        ],
      ),
    ],
  );
}

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 playLevelMusic(Music music) {}

  @override
  void playMenuMusic() {}

  @override
  void playSoundEffect(SoundEffect effect) {}

  @override
  void playSoundEffectId(int sfxId) {}

  @override
  void stopMusic() {}

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

  @override
  void dispose() {}
}

int _countDifferentPixels(List<int> a, List<int> b) {
  int count = 0;
  for (int i = 0; i < a.length; i++) {
    if (a[i] != b[i]) {
      count++;
    }
  }
  return count;
}

int _rgbToFrameColor(int rgb) {
  final int r = (rgb >> 16) & 0xFF;
  final int g = (rgb >> 8) & 0xFF;
  final int b = rgb & 0xFF;
  return 0xFF000000 | (b << 16) | (g << 8) | r;
}

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)));
}
