package engine

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

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

// The offline engine (ui/src/lib/localgame) reproduces the end-of-game rack settlement and the
// winner rule so a local game finishes with the same scores as the server. These golden fixtures
// pin the ported pure functions (applyEndAdjustment / winner / rackValue) to the real Go engine.
// Being in-package, this emitter constructs Game values directly and drives the unexported
// end-game math on chosen positions.

type endCaseIn struct {
	Name     string  `json:"name"`
	Variant  string  `json:"variant"`
	Reason   string  `json:"reason"`
	Hands    [][]int `json:"hands"`
	Scores   []int   `json:"scores"`
	Resigned []bool  `json:"resigned"`
	ToMove   int     `json:"toMove"`
}

type endCaseOut struct {
	endCaseIn
	ScoresAfter []int `json:"scoresAfter"`
	Winner      int   `json:"winner"`
}

func rulesetFor(variant string) *rules.Ruleset {
	switch variant {
	case "scrabble_ru":
		return rules.RussianScrabble()
	case "erudit_ru":
		return rules.Erudit()
	default:
		return rules.English()
	}
}

func reasonFor(s string) EndReason {
	switch s {
	case "out_of_tiles":
		return EndOutOfTiles
	case "scoreless":
		return EndScoreless
	case "resign":
		return EndResign
	case "aborted":
		return EndAborted
	}
	return EndNotOver
}

func handsBytes(hands [][]int) [][]byte {
	out := make([][]byte, len(hands))
	for i, h := range hands {
		b := make([]byte, len(h))
		for j, x := range h {
			b[j] = byte(x)
		}
		out[i] = b
	}
	return out
}

// TestEmitEndgameFixtures regenerates ui/src/lib/localgame/testdata/endgame.json. Gated by
// EMIT_ENGINE_FIXTURES. Regenerate with:
//
//	EMIT_ENGINE_FIXTURES=1 go test ./backend/internal/engine -run TestEmitEndgameFixtures
func TestEmitEndgameFixtures(t *testing.T) {
	if os.Getenv("EMIT_ENGINE_FIXTURES") == "" {
		t.Skip("set EMIT_ENGINE_FIXTURES=1 to regenerate ui/src/lib/localgame/testdata/endgame.json")
	}

	cases := []endCaseIn{
		{"out-basic", "scrabble_en", "out_of_tiles", [][]int{{}, {0, 1, 2}}, []int{50, 40}, []bool{false, false}, 0},
		{"out-blank", "scrabble_en", "out_of_tiles", [][]int{{}, {255, 0}}, []int{30, 30}, []bool{false, false}, 0},
		{"out-erudit-yo", "erudit_ru", "out_of_tiles", [][]int{{}, {6, 32}}, []int{10, 10}, []bool{false, false}, 0},
		{"out-tie", "scrabble_en", "out_of_tiles", [][]int{{}, {}}, []int{30, 30}, []bool{false, false}, 0},
		{"out-3p", "scrabble_ru", "out_of_tiles", [][]int{{}, {0, 1}, {2}}, []int{10, 10, 10}, []bool{false, false, false}, 0},
		{"scoreless", "scrabble_en", "scoreless", [][]int{{0, 1}, {2, 3}}, []int{20, 20}, []bool{false, false}, 0},
		{"resign-2p", "scrabble_en", "resign", [][]int{{}, {0}}, []int{100, 10}, []bool{true, false}, 1},
		{"resign-3p", "scrabble_en", "resign", [][]int{{}, {}, {}}, []int{50, 60, 40}, []bool{false, true, false}, 0},
		{"aborted", "scrabble_en", "aborted", [][]int{{0}, {1}}, []int{40, 30}, []bool{false, false}, 0},
	}

	out := make([]endCaseOut, 0, len(cases))
	for _, c := range cases {
		rs := rulesetFor(c.Variant)
		scores := append([]int(nil), c.Scores...)
		g := &Game{
			rules:    rs,
			hands:    handsBytes(c.Hands),
			scores:   scores,
			resigned: c.Resigned,
			toMove:   c.ToMove,
		}
		reason := reasonFor(c.Reason)
		g.over = true
		g.reason = reason
		g.applyEndAdjustment(reason)
		out = append(out, endCaseOut{endCaseIn: c, ScoresAfter: g.scores, Winner: g.winner()})
	}

	dir := filepath.Join("..", "..", "..", "ui", "src", "lib", "localgame", "testdata")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", dir, err)
	}
	data, err := json.MarshalIndent(map[string]any{"cases": out}, "", "  ")
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	path := filepath.Join(dir, "endgame.json")
	if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
	t.Logf("wrote %s (%d cases)", path, len(out))
}
