package engine

import (
	"errors"
	"testing"

	"gitea.iliadenisov.ru/developer/scrabble-solver/board"
	"gitea.iliadenisov.ru/developer/scrabble-solver/scrabble"
)

// newEnglishGame starts a two-player English game with the given seed.
func newEnglishGame(t *testing.T, seed int64) *Game {
	t.Helper()
	g, err := New(testReg, Options{Variant: VariantEnglish, Version: testVersion, Players: 2, Seed: seed})
	if err != nil {
		t.Fatalf("new game: %v", err)
	}
	return g
}

// openingGame returns a two-player English game whose opening rack has at least
// one legal move, searching a deterministic range of seeds.
func openingGame(t *testing.T) *Game {
	t.Helper()
	for seed := int64(1); seed <= 100; seed++ {
		g := newEnglishGame(t, seed)
		if len(g.GenerateMoves()) > 0 {
			return g
		}
	}
	t.Fatal("no opening move found in seeds 1..100")
	return nil
}

// boardsEqual reports whether two boards have identical dimensions and cells.
func boardsEqual(a, b *board.Board) bool {
	if a.Rows() != b.Rows() || a.Cols() != b.Cols() {
		return false
	}
	for r := range a.Rows() {
		for c := range a.Cols() {
			if a.At(r, c) != b.At(r, c) {
				return false
			}
		}
	}
	return true
}

// TestNewDealsRacks checks the initial state of a fresh game.
func TestNewDealsRacks(t *testing.T) {
	g := newEnglishGame(t, 1)
	if g.Players() != 2 {
		t.Errorf("players = %d, want 2", g.Players())
	}
	if g.ToMove() != 0 {
		t.Errorf("to move = %d, want 0", g.ToMove())
	}
	if g.Over() {
		t.Error("a fresh game must not be over")
	}
	if g.Score(0) != 0 || g.Score(1) != 0 {
		t.Errorf("scores = (%d, %d), want (0, 0)", g.Score(0), g.Score(1))
	}
	rackSize := g.rules.RackSize
	if len(g.hands[0]) != rackSize || len(g.hands[1]) != rackSize {
		t.Fatalf("hand sizes = (%d, %d), want %d each", len(g.hands[0]), len(g.hands[1]), rackSize)
	}
	if want := len(allTiles(g.rules)) - 2*rackSize; g.BagLen() != want {
		t.Errorf("bag len = %d, want %d", g.BagLen(), want)
	}
}

// TestNewRejectsBadPlayerCount rejects player counts outside 2..4.
func TestNewRejectsBadPlayerCount(t *testing.T) {
	for _, n := range []int{0, 1, 5} {
		if _, err := New(testReg, Options{Variant: VariantEnglish, Version: testVersion, Players: n, Seed: 1}); err == nil {
			t.Errorf("players=%d: expected an error", n)
		}
	}
}

// TestNewUnknownVariant surfaces the registry's not-found error.
func TestNewUnknownVariant(t *testing.T) {
	if _, err := New(testReg, Options{Variant: Variant(99), Version: testVersion, Players: 2}); !errors.Is(err, ErrUnknownVariant) {
		t.Fatalf("got %v, want ErrUnknownVariant", err)
	}
}

// TestPlayScoresAndAdvances plays the top opening move and checks the score,
// running total, refill and turn advance.
func TestPlayScoresAndAdvances(t *testing.T) {
	g := openingGame(t)
	move := g.GenerateMoves()[0]
	played := len(move.Tiles)
	bagBefore := g.BagLen()

	rec, err := g.Play(move.Dir, move.Tiles)
	if err != nil {
		t.Fatalf("play: %v", err)
	}
	if rec.Action != ActionPlay {
		t.Errorf("action = %v, want play", rec.Action)
	}
	if rec.Score != move.Score || g.Score(0) != move.Score {
		t.Errorf("score: rec=%d game=%d, want %d", rec.Score, g.Score(0), move.Score)
	}
	if rec.Total != move.Score {
		t.Errorf("running total = %d, want %d", rec.Total, move.Score)
	}
	if len(rec.Tiles) != played {
		t.Errorf("recorded tiles = %d, want %d", len(rec.Tiles), played)
	}
	if g.ToMove() != 1 {
		t.Errorf("to move = %d, want 1", g.ToMove())
	}
	if len(g.hands[0]) != g.rules.RackSize {
		t.Errorf("hand refilled to %d, want %d", len(g.hands[0]), g.rules.RackSize)
	}
	if g.BagLen() != bagBefore-played {
		t.Errorf("bag len = %d, want %d", g.BagLen(), bagBefore-played)
	}
}

// TestPlayRejectsTilesNotOnRack rejects a play using tiles the player lacks.
func TestPlayRejectsTilesNotOnRack(t *testing.T) {
	g := newEnglishGame(t, 1)
	row, col := centre(g.rules)
	ps := placementsForWord(t, g.rules, row, col, scrabble.Horizontal, "cat")
	// Clear the hand so the player provably lacks the tiles; the holds check
	// must reject the play before any dictionary check.
	g.hands[0] = nil
	if _, err := g.Play(scrabble.Horizontal, ps); !errors.Is(err, ErrTilesNotOnRack) {
		t.Fatalf("got %v, want ErrTilesNotOnRack", err)
	}
}

// TestExchangeSwapsTiles exchanges two tiles and checks the bag and turn state.
func TestExchangeSwapsTiles(t *testing.T) {
	g := newEnglishGame(t, 1)
	bagBefore := g.BagLen()
	swap := append([]byte(nil), g.hands[0][:2]...)

	rec, err := g.Exchange(swap)
	if err != nil {
		t.Fatalf("exchange: %v", err)
	}
	if rec.Action != ActionExchange || rec.Count != 2 {
		t.Errorf("record = %+v, want exchange of 2", rec)
	}
	if len(g.hands[0]) != g.rules.RackSize {
		t.Errorf("hand size = %d, want %d", len(g.hands[0]), g.rules.RackSize)
	}
	if g.BagLen() != bagBefore {
		t.Errorf("bag len = %d, want %d (draw and return cancel)", g.BagLen(), bagBefore)
	}
	if g.ToMove() != 1 {
		t.Errorf("to move = %d, want 1", g.ToMove())
	}
	if g.scorelessRun != 1 {
		t.Errorf("scoreless run = %d, want 1", g.scorelessRun)
	}
}

// TestExchangeNeedsFullBag rejects an exchange once the bag is below a rack.
func TestExchangeNeedsFullBag(t *testing.T) {
	g := newEnglishGame(t, 1)
	g.bag.Draw(g.bag.Len()) // drain the bag
	if _, err := g.Exchange(g.hands[0][:1]); !errors.Is(err, ErrNotEnoughTilesToExchange) {
		t.Fatalf("got %v, want ErrNotEnoughTilesToExchange", err)
	}
}

// TestPassEndsAfterSixScoreless ends the game after the scoreless limit and then
// rejects further transitions.
func TestPassEndsAfterSixScoreless(t *testing.T) {
	g := newEnglishGame(t, 1)
	for i := range scorelessLimit {
		if _, err := g.Pass(); err != nil {
			t.Fatalf("pass %d: %v", i, err)
		}
	}
	if !g.Over() {
		t.Fatal("game must be over after six scoreless turns")
	}
	if g.Reason() != EndScoreless {
		t.Errorf("reason = %v, want scoreless", g.Reason())
	}
	if _, err := g.Pass(); !errors.Is(err, ErrGameOver) {
		t.Errorf("pass after end: got %v, want ErrGameOver", err)
	}
}

// TestGreedyPlaythroughEndsAndReplays drives a full greedy game to its end and
// proves the dictionary-independent replay reproduces the final board.
func TestGreedyPlaythroughEndsAndReplays(t *testing.T) {
	g := newEnglishGame(t, 20250602)
	const maxTurns = 600
	for turn := 0; turn < maxTurns && !g.Over(); turn++ {
		if moves := g.GenerateMoves(); len(moves) > 0 {
			if _, err := g.Play(moves[0].Dir, moves[0].Tiles); err != nil {
				t.Fatalf("turn %d play: %v", turn, err)
			}
			continue
		}
		if _, err := g.Pass(); err != nil {
			t.Fatalf("turn %d pass: %v", turn, err)
		}
	}
	if !g.Over() {
		t.Fatalf("game did not finish within %d turns", maxTurns)
	}

	rs, err := Ruleset(VariantEnglish)
	if err != nil {
		t.Fatalf("ruleset: %v", err)
	}
	replayed, err := ReplayBoard(rs, g.Log())
	if err != nil {
		t.Fatalf("replay: %v", err)
	}
	if !boardsEqual(replayed, g.BoardClone()) {
		t.Fatal("replayed board differs from the final board")
	}
}
