package engine

import (
	"errors"
	"slices"
	"testing"
)

// TestDirectionString covers the H/V rendering used by the journal and GCG.
func TestDirectionString(t *testing.T) {
	if Horizontal.String() != "H" {
		t.Errorf("Horizontal = %q, want H", Horizontal.String())
	}
	if Vertical.String() != "V" {
		t.Errorf("Vertical = %q, want V", Vertical.String())
	}
}

// TestSubmitPlayMatchesHint plays the decoded top-1 move through SubmitPlay and
// checks it scores and advances exactly like the underlying solver move, proving
// the decode→encode round trip.
func TestSubmitPlayMatchesHint(t *testing.T) {
	g := openingGame(t)
	hint, ok := g.HintView()
	if !ok {
		t.Fatal("opening game has no hint")
	}
	rec, err := g.SubmitPlay(hint.Tiles)
	if err != nil {
		t.Fatalf("submit play: %v", err)
	}
	if rec.Score != hint.Score {
		t.Errorf("played score = %d, want hint score %d", rec.Score, hint.Score)
	}
	if rec.Action != ActionPlay {
		t.Errorf("action = %v, want play", rec.Action)
	}
	if g.Score(0) != hint.Score {
		t.Errorf("player 0 score = %d, want %d", g.Score(0), hint.Score)
	}
	if g.ToMove() != 1 {
		t.Errorf("to move = %d, want 1 after a play", g.ToMove())
	}
}

// TestCandidatesRankedAndMatchesHint checks that Candidates decodes every
// generated move, ranks them by descending score, and leads with the same move
// HintView reveals.
func TestCandidatesRankedAndMatchesHint(t *testing.T) {
	g := openingGame(t)
	cands := g.Candidates()
	if len(cands) == 0 {
		t.Fatal("opening game has no candidates")
	}
	if got, want := len(cands), len(g.GenerateMoves()); got != want {
		t.Errorf("candidate count = %d, want %d (one per generated move)", got, want)
	}
	for i := 1; i < len(cands); i++ {
		if cands[i-1].Score < cands[i].Score {
			t.Errorf("candidates not ranked: [%d].Score=%d < [%d].Score=%d", i-1, cands[i-1].Score, i, cands[i].Score)
		}
	}
	hint, ok := g.HintView()
	if !ok {
		t.Fatal("opening game has no hint")
	}
	if cands[0].Score != hint.Score {
		t.Errorf("top candidate score = %d, want hint score %d", cands[0].Score, hint.Score)
	}
	for _, c := range cands {
		if c.Action != ActionPlay {
			t.Errorf("candidate action = %v, want play", c.Action)
		}
	}
}

// TestEvaluatePlayDoesNotCommit checks the preview scores like a real play but
// leaves the board, scores, turn and bag untouched.
func TestEvaluatePlayDoesNotCommit(t *testing.T) {
	g := openingGame(t)
	hint, ok := g.HintView()
	if !ok {
		t.Fatal("opening game has no hint")
	}
	boardBefore := g.BoardClone()
	scoreBefore, toMoveBefore, bagBefore := g.Score(0), g.ToMove(), g.BagLen()

	rec, err := g.EvaluatePlay(hint.Tiles)
	if err != nil {
		t.Fatalf("evaluate play: %v", err)
	}
	if rec.Score != hint.Score {
		t.Errorf("evaluated score = %d, want %d", rec.Score, hint.Score)
	}
	if !boardsEqual(boardBefore, g.BoardClone()) {
		t.Error("evaluate must not mutate the board")
	}
	if g.Score(0) != scoreBefore || g.ToMove() != toMoveBefore || g.BagLen() != bagBefore {
		t.Errorf("evaluate mutated state: score %d->%d, toMove %d->%d, bag %d->%d",
			scoreBefore, g.Score(0), toMoveBefore, g.ToMove(), bagBefore, g.BagLen())
	}
}

// TestEvaluatePlayRejectsIllegal reports ErrIllegalPlay for a play the solver
// rejects (a single off-centre opening tile) without committing.
func TestEvaluatePlayRejectsIllegal(t *testing.T) {
	g := newEnglishGame(t, 1)
	letter := g.Hand(0)[0]
	_, err := g.EvaluatePlay([]TileRecord{{Row: 0, Col: 0, Letter: letter}})
	if !errors.Is(err, ErrIllegalPlay) {
		t.Errorf("evaluate off-centre opening = %v, want ErrIllegalPlay", err)
	}
}

// TestSubmitExchangeWithBlank exchanges a full rack that includes a blank,
// exercising the "?" encoding path, and checks the turn advances.
func TestSubmitExchangeWithBlank(t *testing.T) {
	g := gameWithBlankInHand(t)
	hand := g.Hand(0)
	if !slices.Contains(hand, blankLetter) {
		t.Fatalf("hand %v has no blank", hand)
	}
	rec, err := g.SubmitExchange(hand)
	if err != nil {
		t.Fatalf("submit exchange: %v", err)
	}
	if rec.Action != ActionExchange || rec.Count != len(hand) {
		t.Errorf("exchange record = %+v, want action exchange count %d", rec, len(hand))
	}
	if g.ToMove() != 1 {
		t.Errorf("to move = %d, want 1 after an exchange", g.ToMove())
	}
}

// TestHandDecodesBlank checks Hand returns concrete letters and "?" for a blank,
// agreeing with the internal hand.
func TestHandDecodesBlank(t *testing.T) {
	g := gameWithBlankInHand(t)
	hand := g.Hand(0)
	if len(hand) != g.rules.RackSize {
		t.Fatalf("hand size = %d, want %d", len(hand), g.rules.RackSize)
	}
	var blanks int
	for _, s := range hand {
		if s == "" {
			t.Errorf("hand %v has an empty letter", hand)
		}
		if s == blankLetter {
			blanks++
		}
	}
	var want int
	for _, t := range g.hands[0] {
		if t == blankTile {
			want++
		}
	}
	if blanks != want {
		t.Errorf("decoded blanks = %d, want %d", blanks, want)
	}
}

// TestRegistryLookup covers word-check membership and its error taxonomy.
func TestRegistryLookup(t *testing.T) {
	cases := []struct {
		name    string
		variant Variant
		word    string
		want    bool
	}{
		{"scrabble_en hit", VariantEnglish, "cat", true},
		{"scrabble_en miss", VariantEnglish, "zzzz", false},
		{"scrabble_ru hit", VariantRussianScrabble, "кот", true},
		{"erudit_ru hit", VariantErudit, "кот", true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := testReg.Lookup(tc.variant, testVersion, tc.word)
			if err != nil {
				t.Fatalf("lookup: %v", err)
			}
			if got != tc.want {
				t.Errorf("lookup %q = %v, want %v", tc.word, got, tc.want)
			}
		})
	}

	if _, err := testReg.Lookup(VariantEnglish, "missing", "cat"); !errors.Is(err, ErrUnknownVersion) {
		t.Errorf("unknown version = %v, want ErrUnknownVersion", err)
	}
	if _, err := NewRegistry().Lookup(VariantEnglish, testVersion, "cat"); !errors.Is(err, ErrUnknownVariant) {
		t.Errorf("empty registry = %v, want ErrUnknownVariant", err)
	}
	if _, err := testReg.Lookup(VariantEnglish, testVersion, "кот"); err == nil {
		t.Error("out-of-alphabet lookup must error")
	}
}

// gameWithBlankInHand returns a two-player English game whose player 0 holds at
// least one blank, searching a deterministic range of seeds.
func gameWithBlankInHand(t *testing.T) *Game {
	t.Helper()
	for seed := int64(1); seed <= 200; seed++ {
		g := newEnglishGame(t, seed)
		if slices.Contains(g.Hand(0), blankLetter) {
			return g
		}
	}
	t.Fatal("no opening rack with a blank found in seeds 1..200")
	return nil
}
