import 'package:block_seasons/core/rng.dart';
import 'package:block_seasons/game/engine/piece_generator.dart';
import 'package:block_seasons/game/models/cell.dart';
import 'package:block_seasons/game/models/grid.dart';
import 'package:block_seasons/game/models/piece_library.dart';
import 'package:flutter_test/flutter_test.dart';

/// Checkerboard: no line can ever be completed, big shapes never fit.
GridState _checkerboard() {
  var grid = GridState.empty();
  for (var y = 0; y < GridState.size; y++) {
    for (var x = 0; x < GridState.size; x++) {
      if ((x + y).isEven) {
        grid = grid.withCell(x, y, const Cell(CellType.filled, colorId: 0));
      }
    }
  }
  return grid;
}

/// Rows 1..7 filled except column 0; row 0 filled except (0,0).
/// Only column 0 is free -> tight but recoverable via line clears.
GridState _tightBoard() {
  var grid = GridState.empty();
  for (var y = 0; y < GridState.size; y++) {
    for (var x = 1; x < GridState.size; x++) {
      grid = grid.withCell(x, y, const Cell(CellType.filled, colorId: 0));
    }
  }
  return grid;
}

void main() {
  group('isTrayPlayable', () {
    test('any tray plays out on an empty grid', () {
      final tray = [
        PieceLibrary.byId('square3'),
        PieceLibrary.byId('line5_h'),
        PieceLibrary.byId('rect3x2'),
      ];
      expect(isTrayPlayable(GridState.empty(), tray), isTrue);
    });

    test('recognizes trays that need intermediate line clears', () {
      // Column 0 free (8 cells); tray sums to 9 cells, so it only plays out
      // if placements trigger clears that open space.
      final tray = [
        PieceLibrary.byId('line4_v'),
        PieceLibrary.byId('line4_v'),
        PieceLibrary.byId('mono'),
      ];
      expect(isTrayPlayable(_tightBoard(), tray), isTrue);
    });

    test('returns false when no ordering can play all pieces', () {
      final tray = [
        PieceLibrary.byId('square3'),
        PieceLibrary.byId('square3'),
        PieceLibrary.byId('line5_h'),
      ];
      expect(isTrayPlayable(_checkerboard(), tray), isFalse);
    });
  });

  group('PieceGenerator', () {
    test('same seed produces identical tray sequences', () {
      final a = PieceGenerator(SeededRng(42));
      final b = PieceGenerator(SeededRng(42));
      final grid = GridState.empty();
      for (var i = 0; i < 100; i++) {
        final trayA = a.nextTray(grid).map((p) => p.id).toList();
        final trayB = b.nextTray(grid).map((p) => p.id).toList();
        expect(trayA, trayB);
      }
    });

    test('trays always contain exactly 3 pieces with distinct ids', () {
      final gen = PieceGenerator(SeededRng(7));
      final grids = [GridState.empty(), _checkerboard(), _tightBoard()];
      for (final grid in grids) {
        for (var i = 0; i < 200; i++) {
          final tray = gen.nextTray(grid);
          expect(tray.length, 3);
          expect(tray.map((p) => p.id).toSet().length, 3);
        }
      }
    });

    test('pity bias: tighter boards get smaller pieces on average', () {
      final emptyGen = PieceGenerator(SeededRng(123));
      final tightGen = PieceGenerator(SeededRng(123));
      final empty = GridState.empty();
      final tight = _checkerboard(); // 50% fill, no clears possible

      double meanSize(PieceGenerator gen, GridState grid) {
        var total = 0;
        for (var i = 0; i < 400; i++) {
          for (final p in gen.nextTray(grid)) {
            total += p.size;
          }
        }
        return total / (400 * 3);
      }

      final emptyMean = meanSize(emptyGen, empty);
      final tightMean = meanSize(tightGen, tight);
      expect(tightMean, lessThan(emptyMean));
    });

    test('nudge: tight-but-recoverable boards always get playable trays',
        () {
      final gen = PieceGenerator(SeededRng(2026));
      final grid = _tightBoard();
      for (var i = 0; i < 100; i++) {
        final tray = gen.nextTray(grid);
        expect(isTrayPlayable(grid, tray), isTrue,
            reason: 'deal #$i: ${tray.map((p) => p.id).join(", ")}');
      }
    });

    test('on a dead board the tray still contains the smallest pieces', () {
      // Fully occupied: nothing fits; engine handles game over, but the
      // generator must not loop forever and should fall back to small pieces.
      var grid = GridState.empty();
      for (var y = 0; y < GridState.size; y++) {
        for (var x = 0; x < GridState.size; x++) {
          grid = grid.withCell(x, y, const Cell(CellType.filled, colorId: 0));
        }
      }
      final tray = PieceGenerator(SeededRng(5)).nextTray(grid);
      expect(tray.length, 3);
      expect(tray.every((p) => p.size <= 2), isTrue);
    });
  });
}
