package engine

import (
	"errors"
	"testing"

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

// boardWith returns a 15x15 board with the given (row, col) cells occupied. The
// concrete letter is irrelevant to direction inference, which reads only
// occupancy, so every filler uses alphabet index 0.
func boardWith(cells ...[2]int) *board.Board {
	b := board.New(15, 15)
	ps := make([]scrabble.Placement, len(cells))
	for i, c := range cells {
		ps[i] = scrabble.Placement{Row: c[0], Col: c[1], Letter: 0}
	}
	scrabble.Apply(b, scrabble.Move{Tiles: ps})
	return b
}

// TestRunLength covers the word-span measurement that drives single-tile
// direction inference, including the board edge.
func TestRunLength(t *testing.T) {
	tests := []struct {
		name     string
		filled   [][2]int
		row, col int
		dir      scrabble.Direction
		want     int
	}{
		{"isolated horizontal", nil, 7, 7, scrabble.Horizontal, 1},
		{"isolated vertical", nil, 7, 7, scrabble.Vertical, 1},
		{"neighbour below", [][2]int{{8, 7}}, 7, 7, scrabble.Vertical, 2},
		{"neighbour above", [][2]int{{6, 7}}, 7, 7, scrabble.Vertical, 2},
		{"run below", [][2]int{{8, 7}, {9, 7}}, 7, 7, scrabble.Vertical, 3},
		{"bridge vertical", [][2]int{{6, 7}, {8, 7}}, 7, 7, scrabble.Vertical, 3},
		{"neighbour left", [][2]int{{7, 6}}, 7, 7, scrabble.Horizontal, 2},
		{"neighbour right", [][2]int{{7, 8}}, 7, 7, scrabble.Horizontal, 2},
		{"bridge horizontal", [][2]int{{7, 6}, {7, 8}}, 7, 7, scrabble.Horizontal, 3},
		{"perpendicular ignored", [][2]int{{7, 6}}, 7, 7, scrabble.Vertical, 1},
		{"top edge", [][2]int{{1, 0}}, 0, 0, scrabble.Vertical, 2},
		{"left edge", [][2]int{{0, 1}}, 0, 0, scrabble.Horizontal, 2},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			b := boardWith(tt.filled...)
			if got := runLength(b, tt.row, tt.col, tt.dir); got != tt.want {
				t.Errorf("runLength(%d,%d,%v) = %d, want %d", tt.row, tt.col, tt.dir, got, tt.want)
			}
		})
	}
}

// TestResolveDirection covers orientation inference for both multi-tile plays
// (fixed by the shared line) and the ambiguous single tile (the axis it abuts,
// longer word winning and horizontal on a tie; disconnected falls back to
// horizontal for the downstream rejection).
func TestResolveDirection(t *testing.T) {
	at := func(cells ...[2]int) []scrabble.Placement {
		ps := make([]scrabble.Placement, len(cells))
		for i, c := range cells {
			ps[i] = scrabble.Placement{Row: c[0], Col: c[1]}
		}
		return ps
	}
	tests := []struct {
		name   string
		filled [][2]int
		play   []scrabble.Placement
		want   scrabble.Direction
	}{
		{"single extends down", [][2]int{{8, 7}, {9, 7}}, at([2]int{7, 7}), scrabble.Vertical},
		{"single extends up", [][2]int{{6, 7}, {5, 7}}, at([2]int{7, 7}), scrabble.Vertical},
		{"single extends left", [][2]int{{7, 6}}, at([2]int{7, 7}), scrabble.Horizontal},
		{"single extends right", [][2]int{{7, 8}}, at([2]int{7, 7}), scrabble.Horizontal},
		{"single both axes vertical longer", [][2]int{{6, 7}, {8, 7}, {7, 6}}, at([2]int{7, 7}), scrabble.Vertical},
		{"single both axes horizontal longer", [][2]int{{7, 6}, {7, 8}, {6, 7}}, at([2]int{7, 7}), scrabble.Horizontal},
		{"single both axes equal prefers horizontal", [][2]int{{6, 7}, {7, 6}}, at([2]int{7, 7}), scrabble.Horizontal},
		{"single disconnected falls back to horizontal", nil, at([2]int{7, 7}), scrabble.Horizontal},
		{"multi shared row is horizontal", nil, at([2]int{7, 7}, [2]int{7, 8}), scrabble.Horizontal},
		{"multi shared column is vertical", nil, at([2]int{7, 7}, [2]int{8, 7}), scrabble.Vertical},
		{"multi non-linear is vertical", nil, at([2]int{7, 7}, [2]int{8, 8}), scrabble.Vertical},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			b := boardWith(tt.filled...)
			if got := resolveDirection(b, tt.play); got != tt.want {
				t.Errorf("resolveDirection = %v, want %v", got, tt.want)
			}
		})
	}
}

// TestResolveDirectionEmpty checks the degenerate empty placement does not panic
// and falls back to horizontal (Evaluate rejects the empty play downstream).
func TestResolveDirectionEmpty(t *testing.T) {
	if got := resolveDirection(boardWith(), nil); got != scrabble.Horizontal {
		t.Errorf("resolveDirection(empty) = %v, want Horizontal", got)
	}
}

// TestSubmitPlaySingleTileVerticalExtension is the regression for the reported
// bug: a single tile placed above an existing vertical word forms a legal play
// the engine must accept by inferring the vertical orientation. Trusting a
// horizontal orientation (the pre-fix client default) wrongly rejects it.
func TestSubmitPlaySingleTileVerticalExtension(t *testing.T) {
	// БАК runs down column 7 (rows 7..9); the mover holds А and plays it at
	// (6,7), prefixing АБАК. This mirrors the contour game that surfaced the bug.
	setup := func(t *testing.T) (*Game, []TileRecord) {
		t.Helper()
		g, err := New(testReg, Options{Variant: VariantErudit, Version: testVersion, Players: 2, Seed: 1})
		if err != nil {
			t.Fatalf("new erudit game: %v", err)
		}
		idx := func(s string) byte {
			i, err := g.rules.Alphabet.Index(s)
			if err != nil {
				t.Fatalf("index %q: %v", s, err)
			}
			return i
		}
		scrabble.Apply(g.board, scrabble.Move{Tiles: []scrabble.Placement{
			{Row: 7, Col: 7, Letter: idx("б")},
			{Row: 8, Col: 7, Letter: idx("а")},
			{Row: 9, Col: 7, Letter: idx("к")},
		}})
		g.hands[0] = []byte{idx("а")}
		return g, []TileRecord{{Row: 6, Col: 7, Letter: "а"}}
	}

	t.Run("inferred direction accepts the play", func(t *testing.T) {
		g, tiles := setup(t)
		rec, err := g.SubmitPlay(tiles)
		if err != nil {
			t.Fatalf("submit play: %v", err)
		}
		if rec.Dir != Vertical {
			t.Errorf("direction = %v, want Vertical", rec.Dir)
		}
		if len(rec.Words) == 0 || rec.Words[0] != "абак" {
			t.Errorf("words = %v, want main word абак", rec.Words)
		}
		if rec.Score <= 0 {
			t.Errorf("score = %d, want positive", rec.Score)
		}
	})

	t.Run("trusting horizontal rejects it", func(t *testing.T) {
		g, tiles := setup(t)
		if _, err := g.SubmitPlayDir(Horizontal, tiles); !errors.Is(err, ErrIllegalPlay) {
			t.Errorf("submit horizontal = %v, want ErrIllegalPlay", err)
		}
	})
}
