package robot

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strconv"
	"testing"

	"scrabble/backend/internal/engine"
)

// The client-side offline robot (ui/src/lib/robot) reproduces the robot's move choice
// so a local vs_ai game plays the same way as the server. These golden fixtures pin that
// TS port to the real Go strategy. Being in-package, this emitter can exercise the
// unexported mix / playToWin / deviates / selectMove that the port mirrors.

type mixCase struct {
	Seed  string `json:"seed"`
	Salt  string `json:"salt"`
	Nums  []int  `json:"nums"`
	Value string `json:"value"` // the mix() uint64, as a decimal string (exceeds JS safe ints)
}

type decisionCase struct {
	Seed       string `json:"seed"`
	MoveCount  int    `json:"moveCount"`
	MyScore    int    `json:"myScore"`
	OppScore   int    `json:"oppScore"`
	CandScores []int  `json:"candScores"`
	RackLen    int    `json:"rackLen"`
	BagLen     int    `json:"bagLen"`
	PlayToWin  bool   `json:"playToWin"`
	Win        bool   `json:"win"` // the per-turn intent after the deviate flip
	Kind       string `json:"kind"`
	Index      int    `json:"index"` // chosen candidate index for a play, else -1
}

type strategyFixture struct {
	PlayToWinPercent int            `json:"playToWinPercent"`
	Mix              []mixCase      `json:"mix"`
	Decisions        []decisionCase `json:"decisions"`
}

// scenario is one selectMove situation exercised across several seeds.
type scenario struct {
	moveCount, myScore, oppScore, rackLen, bagLen int
	cands                                         []int
}

// TestEmitStrategyFixtures regenerates ui/src/lib/robot/testdata/strategy.json. It is
// gated by EMIT_STRATEGY_FIXTURES so a normal `go test` skips it; the committed output is
// what the TS parity test consumes. Regenerate with:
//
//	EMIT_STRATEGY_FIXTURES=1 go test ./backend/internal/robot -run TestEmitStrategyFixtures
func TestEmitStrategyFixtures(t *testing.T) {
	if os.Getenv("EMIT_STRATEGY_FIXTURES") == "" {
		t.Skip("set EMIT_STRATEGY_FIXTURES=1 to regenerate ui/src/lib/robot/testdata/strategy.json")
	}

	seeds := []int64{1, 42, -7, 1000003, 9999999999, 123456789, -123456789}

	mixCases := []mixCase{}
	addMix := func(seed int64, salt string, nums ...int) {
		mixCases = append(mixCases, mixCase{
			Seed:  strconv.FormatInt(seed, 10),
			Salt:  salt,
			Nums:  append([]int{}, nums...),
			Value: strconv.FormatUint(mix(seed, salt, nums...), 10),
		})
	}
	for _, sd := range seeds {
		addMix(sd, "win")
		addMix(sd, "deviate", 0)
		addMix(sd, "deviate", 7)
	}

	scenarios := []scenario{
		{3, 40, 55, 7, 20, []int{30, 12, 8}},   // behind, mid-game
		{10, 120, 90, 7, 8, []int{25, 18, 5}},  // ahead, late
		{1, 0, 0, 7, 30, []int{60, 30, 15, 7}}, // first move, big spread
		{20, 200, 40, 5, 2, []int{50, 20}},     // big lead, bag near empty (no deviate)
		{5, 30, 30, 7, 14, []int{20, 20, 20}},  // equal margins (tie-break)
		{8, 50, 60, 7, 20, []int{}},            // no play, bag can refill -> exchange
		{8, 50, 60, 3, 2, []int{}},             // no play, bag can't refill -> pass
		{15, 300, 10, 7, 6, []int{80, 40, 10}}, // overshoots the band -> nearest to +30
	}

	decisions := []decisionCase{}
	for _, sd := range seeds {
		for _, sc := range scenarios {
			cands := make([]engine.MoveRecord, len(sc.cands))
			for i, s := range sc.cands {
				cands[i] = engine.MoveRecord{Score: s, Player: i}
			}
			rack := make([]string, sc.rackLen)
			for i := range rack {
				rack[i] = "a"
			}

			ptw := playToWin(sd)
			win := ptw
			if deviates(sd, sc.moveCount, sc.bagLen) {
				win = !win
			}
			d := selectMove(cands, sc.myScore, sc.oppScore, win, defaultBand, rack, sc.bagLen)

			kind, index := "pass", -1
			switch d.kind {
			case decidePlay:
				kind, index = "play", d.move.Player
			case decideExchange:
				kind = "exchange"
			}

			decisions = append(decisions, decisionCase{
				Seed:       strconv.FormatInt(sd, 10),
				MoveCount:  sc.moveCount,
				MyScore:    sc.myScore,
				OppScore:   sc.oppScore,
				CandScores: append([]int{}, sc.cands...),
				RackLen:    sc.rackLen,
				BagLen:     sc.bagLen,
				PlayToWin:  ptw,
				Win:        win,
				Kind:       kind,
				Index:      index,
			})
		}
	}

	fx := strategyFixture{PlayToWinPercent: playToWinPercent, Mix: mixCases, Decisions: decisions}
	dir := filepath.Join("..", "..", "..", "ui", "src", "lib", "robot", "testdata")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", dir, err)
	}
	data, err := json.MarshalIndent(fx, "", "  ")
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	path := filepath.Join(dir, "strategy.json")
	if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
	t.Logf("wrote %s (%d mix, %d decisions)", path, len(mixCases), len(decisions))
}
