package moves

import (
	"math/rand"
	"os"
	"strings"
	"testing"

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

	"scrabble/loadtest/internal/edge"
)

// TestReplayBoardMatchesParse checks that replaying decoded history reproduces the
// exact board (positions, letters and blank flags) that board.Parse builds from the
// equivalent text grid, and that non-play records are ignored.
func TestReplayBoardMatchesParse(t *testing.T) {
	rs := rules.English()
	history := []edge.Move{
		{Action: "pass"}, // must be ignored
		{Action: "play", Tiles: []edge.Tile{
			{Row: 7, Col: 7, Letter: "c"},
			{Row: 7, Col: 8, Letter: "a"},
			{Row: 7, Col: 9, Letter: "t"},
		}},
		{Action: "play", Tiles: []edge.Tile{
			{Row: 7, Col: 10, Letter: "s", Blank: true}, // a blank standing for s
		}},
	}
	got, err := replayBoard(rs, history)
	if err != nil {
		t.Fatalf("replayBoard: %v", err)
	}

	rows := make([]string, rs.Rows)
	for i := range rows {
		rows[i] = strings.Repeat(".", rs.Cols)
	}
	// row 7: cols 0-6 empty, cat at 7-9, an uppercase S (blank) at 10.
	rows[7] = strings.Repeat(".", 7) + "cat" + "S" + strings.Repeat(".", rs.Cols-11)
	want, err := board.Parse(rows, rs.Alphabet)
	if err != nil {
		t.Fatalf("board.Parse: %v", err)
	}
	for r := 0; r < rs.Rows; r++ {
		for c := 0; c < rs.Cols; c++ {
			if got.At(r, c) != want.At(r, c) {
				t.Fatalf("cell (%d,%d): replay = %#x, parse = %#x", r, c, got.At(r, c), want.At(r, c))
			}
		}
	}
}

// TestBuildRack checks the alphabet-index rack (255 a blank) is reconstructed faithfully.
func TestBuildRack(t *testing.T) {
	rs := rules.English()
	rk := buildRack(rs, []byte{0, 0, 2, blankIndex}) // a a c blank
	if rk.Count(0) != 2 {
		t.Errorf("count(a) = %d, want 2", rk.Count(0))
	}
	if rk.Count(2) != 1 {
		t.Errorf("count(c) = %d, want 1", rk.Count(2))
	}
	if rk.Blanks() != 1 {
		t.Errorf("blanks = %d, want 1", rk.Blanks())
	}
	if rk.Total() != 4 {
		t.Errorf("total = %d, want 4", rk.Total())
	}
}

// TestMidRanked checks the pick always lands in the middle third of a ranked list and
// that tiny lists yield their lowest-scoring move.
func TestMidRanked(t *testing.T) {
	ms := make([]scrabble.Move, 9) // scores 100..92, index i has score 100-i
	for i := range ms {
		ms[i] = scrabble.Move{Score: 100 - i}
	}
	rng := rand.New(rand.NewSource(1))
	for n := 0; n < 100; n++ {
		idx := 100 - midRanked(ms, rng).Score // recover the index from the score
		if idx < 3 || idx >= 6 {
			t.Fatalf("picked index %d outside middle third [3,6)", idx)
		}
	}
	if got := midRanked([]scrabble.Move{{Score: 5}}, rng).Score; got != 5 {
		t.Errorf("n=1 pick score = %d, want 5", got)
	}
	if got := midRanked([]scrabble.Move{{Score: 9}, {Score: 4}}, rng).Score; got != 4 {
		t.Errorf("n=2 pick score = %d, want 4 (lower-scoring)", got)
	}
}

// TestToPlayTiles checks the solver-placement to edge-tile mapping, including blanks.
func TestToPlayTiles(t *testing.T) {
	tiles := toPlayTiles([]scrabble.Placement{
		{Row: 1, Col: 2, Letter: 5},
		{Row: 1, Col: 3, Letter: 255, Blank: true},
	})
	want := []edge.PlayTile{
		{Row: 1, Col: 2, Letter: 5},
		{Row: 1, Col: 3, Letter: 255, Blank: true},
	}
	if len(tiles) != len(want) {
		t.Fatalf("len = %d, want %d", len(tiles), len(want))
	}
	for i := range want {
		if tiles[i] != want[i] {
			t.Errorf("tile %d = %+v, want %+v", i, tiles[i], want[i])
		}
	}
}

// TestPickUnknownVariant rejects a variant the registry does not hold.
func TestPickUnknownVariant(t *testing.T) {
	reg := &Registry{engines: map[string]*engine{}}
	if _, err := reg.Pick("nope", nil, nil, 0, rand.New(rand.NewSource(1))); err == nil {
		t.Fatal("want error for an unknown variant")
	}
}

// TestPickWithDawg drives the full path against the committed DAWGs when they are
// available (BACKEND_DICT_DIR, as the engine tests use); it generates a first-move
// play from a productive rack.
func TestPickWithDawg(t *testing.T) {
	dir := os.Getenv("BACKEND_DICT_DIR")
	if dir == "" {
		t.Skip("BACKEND_DICT_DIR not set; skipping DAWG-backed test")
	}
	reg, err := Open(dir)
	if err != nil {
		t.Fatalf("Open(%s): %v", dir, err)
	}
	defer reg.Close()

	rng := rand.New(rand.NewSource(1))
	rack := []byte{2, 0, 19, 18, 4, 17, 13} // c a t s e r n — a productive English rack
	act, err := reg.Pick("scrabble_en", nil, rack, 90, rng)
	if err != nil {
		t.Fatalf("Pick: %v", err)
	}
	switch act.Kind {
	case "play":
		if len(act.Tiles) == 0 {
			t.Error("play action has no tiles")
		}
	case "exchange", "pass":
		// acceptable when the rack has no legal first move
	default:
		t.Errorf("unexpected action kind %q", act.Kind)
	}
}
