//go:build integration

package inttest

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/google/uuid"

	"scrabble/backend/internal/engine"
	"scrabble/backend/internal/game"
	"scrabble/backend/internal/social"
)

// The open-game suite covers an auto-match game that a player enters immediately and
// waits inside (status 'open', the opponent seat empty) until a human or a robot joins.

// evenOpeningSeed returns a seed whose fresh two-player English opening rack (seat 0) has a
// legal move, for tests that drive or attempt an opening play. It scans even seeds; seat
// order no longer depends on the seed — the first-move draw decides it (docs/ARCHITECTURE.md §6).
func evenOpeningSeed(t *testing.T) int64 {
	t.Helper()
	for seed := int64(2); seed <= 400; seed += 2 {
		g, err := engine.New(testRegistry, engine.Options{Variant: engine.VariantEnglish, Version: testDictVersion, Players: 2, Seed: seed})
		if err != nil {
			t.Fatalf("engine new: %v", err)
		}
		if _, ok := g.HintView(); ok {
			return seed
		}
	}
	t.Fatal("no even opening seed found")
	return 0
}

// openParams are the casual auto-match settings with a pinned seed.
func openParams(seed int64) game.CreateParams {
	return game.CreateParams{
		Variant:              engine.VariantEnglish,
		TurnTimeout:          24 * time.Hour,
		HintsAllowed:         true,
		HintsPerPlayer:       1,
		MultipleWordsPerTurn: true,
		Seed:                 seed,
	}
}

func openGame(t *testing.T, svc *game.Service, starter uuid.UUID, seed int64) game.Game {
	t.Helper()
	g, joined, err := svc.OpenOrJoin(context.Background(), starter, openParams(seed), time.Now().Add(time.Minute), nil)
	if err != nil {
		t.Fatalf("open game: %v", err)
	}
	if joined || g.Status != game.StatusOpen {
		t.Fatalf("opened game = (joined %v, status %q), want (false, open)", joined, g.Status)
	}
	return g
}

// TestOpenGameStarterMovesThenWaits checks the starter may move on their turn while the game
// is open (the first-move draw, run when the game opened, put them at seat 0), after which it
// is the empty opponent seat's turn and the starter just waits.
func TestOpenGameStarterMovesThenWaits(t *testing.T) {
	ctx := context.Background()
	clearOpenGames(t)
	svc := newGameService()
	seed := evenOpeningSeed(t)
	g := openGame(t, svc, provisionAccount(t), seed)
	starter := g.Seats[0].AccountID

	hint, ok := newMirror(t, seed, 2).HintView()
	if !ok || len(hint.Tiles) == 0 {
		t.Fatal("no opening move for the seed")
	}
	res, err := svc.SubmitPlay(ctx, g.ID, starter, hint.Tiles)
	if err != nil {
		t.Fatalf("starter play while open: %v", err)
	}
	if res.Game.Status != game.StatusOpen {
		t.Errorf("after the starter's move the game must stay open, got %q", res.Game.Status)
	}
	if res.Game.ToMove != 1 {
		t.Errorf("after the starter's move it must be the empty seat's turn (to_move 1), got %d", res.Game.ToMove)
	}
	if _, err := svc.Pass(ctx, g.ID, starter); !errors.Is(err, game.ErrNotYourTurn) {
		t.Fatalf("starter acting on the opponent's turn = %v, want ErrNotYourTurn", err)
	}
}

// TestOpenGameStarterWaitsWhenOpponentMovesFirst checks that when the first-move draw seats
// the starter at seat 1 (the synthetic opponent won the open draw), the still-empty seat 0 is
// to move, so the starter cannot act until an opponent fills it.
func TestOpenGameStarterWaitsWhenOpponentMovesFirst(t *testing.T) {
	ctx := context.Background()
	clearOpenGames(t)
	svc := newGameService()
	svc.SetFirstMoveEntropy(seatOneFirstMove) // the synthetic opponent wins → the caller sits at seat 1
	starter := provisionAccount(t)
	g, _, err := svc.OpenOrJoin(ctx, starter, openParams(1), time.Now().Add(time.Minute), nil)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	if g.ToMove != 0 || g.Seats[0].AccountID != uuid.Nil {
		t.Fatalf("opponent-won open draw: to_move %d seat0 %s, want the empty seat 0 to move", g.ToMove, g.Seats[0].AccountID)
	}
	if _, err := svc.Pass(ctx, g.ID, starter); !errors.Is(err, game.ErrNotYourTurn) {
		t.Fatalf("starter acting on the empty seat's turn = %v, want ErrNotYourTurn", err)
	}
}

// TestOpenGameJoinAfterStarterMoved checks a second human joins an open game even after the
// starter has already made their first move: the seats were fixed by the draw at open, so the
// joiner takes the empty seat without disturbing the journal, and can act on their turn.
func TestOpenGameJoinAfterStarterMoved(t *testing.T) {
	ctx := context.Background()
	clearOpenGames(t)
	svc := newGameService()
	seed := evenOpeningSeed(t)
	g := openGame(t, svc, provisionAccount(t), seed)
	starter := g.Seats[0].AccountID

	hint, ok := newMirror(t, seed, 2).HintView()
	if !ok {
		t.Fatal("no opening move")
	}
	if _, err := svc.SubmitPlay(ctx, g.ID, starter, hint.Tiles); err != nil {
		t.Fatalf("starter play: %v", err)
	}

	joiner := provisionAccount(t)
	g2, joined, err := svc.OpenOrJoin(ctx, joiner, openParams(0), time.Now().Add(time.Minute), nil)
	if err != nil {
		t.Fatalf("join: %v", err)
	}
	if !joined || g2.ID != g.ID {
		t.Fatalf("join = (joined %v, game %s), want it to join %s", joined, g2.ID, g.ID)
	}
	if g2.Status != game.StatusActive || g2.MoveCount != 1 || g2.ToMove != 1 {
		t.Fatalf("joined game = (status %q, moves %d, to_move %d), want (active, 1, 1)", g2.Status, g2.MoveCount, g2.ToMove)
	}
	// It is the joiner's turn (seat 1); they can act.
	if _, err := svc.Pass(ctx, g.ID, joiner); err != nil {
		t.Fatalf("joiner pass on their turn: %v", err)
	}
}

// TestOpenGameResignRejectedUntilOpponent checks resign is refused while the game is
// open and allowed once an opponent (a robot here) has joined.
func TestOpenGameResignRejectedUntilOpponent(t *testing.T) {
	ctx := context.Background()
	clearOpenGames(t)
	svc := newGameService()
	robots := newRobotService(t, svc)
	if err := robots.EnsurePool(ctx); err != nil {
		t.Fatalf("ensure pool: %v", err)
	}
	g := openGame(t, svc, provisionAccount(t), evenOpeningSeed(t))
	starter := g.Seats[0].AccountID

	if _, err := svc.Resign(ctx, g.ID, starter); !errors.Is(err, game.ErrNoOpponentYet) {
		t.Fatalf("resign while open = %v, want ErrNoOpponentYet", err)
	}

	robotID, name, err := robots.PickNamed(engine.VariantEnglish)
	if err != nil {
		t.Fatalf("pick: %v", err)
	}
	if _, attached, err := svc.AttachRobot(ctx, g.ID, robotID, name); err != nil || !attached {
		t.Fatalf("attach robot = (attached %v, err %v), want attached", attached, err)
	}
	res, err := svc.Resign(ctx, g.ID, starter)
	if err != nil {
		t.Fatalf("resign after the opponent joined: %v", err)
	}
	if res.Game.Status != game.StatusFinished {
		t.Errorf("resign must finish the game, got %q", res.Game.Status)
	}
}

// TestOpenGameChatAndNudgeRejected checks chat and nudge are refused while the game is
// open (no opponent to converse with).
func TestOpenGameChatAndNudgeRejected(t *testing.T) {
	ctx := context.Background()
	clearOpenGames(t)
	svc := newGameService()
	soc := newSocialService()
	g := openGame(t, svc, provisionAccount(t), evenOpeningSeed(t))
	starter := g.Seats[0].AccountID

	if _, err := soc.PostMessage(ctx, g.ID, starter, "hello", ""); !errors.Is(err, social.ErrGameNotActive) {
		t.Errorf("chat while open = %v, want ErrGameNotActive", err)
	}
	if _, err := soc.Nudge(ctx, g.ID, starter); !errors.Is(err, social.ErrGameNotActive) {
		t.Errorf("nudge while open = %v, want ErrGameNotActive", err)
	}
}

// TestOpenGameSweeperSkips checks the turn-timeout sweeper never finishes an open game,
// even with a long-stale turn clock (its seat is the empty opponent's).
func TestOpenGameSweeperSkips(t *testing.T) {
	ctx := context.Background()
	clearOpenGames(t)
	svc := newGameService()
	g := openGame(t, svc, provisionAccount(t), evenOpeningSeed(t))

	setTurnStarted(t, g.ID, time.Now().Add(-1000*time.Hour))
	if _, err := svc.SweepTimeouts(ctx, time.Now()); err != nil {
		t.Fatalf("sweep: %v", err)
	}
	g2, err := svc.GameByID(ctx, g.ID)
	if err != nil {
		t.Fatalf("get game: %v", err)
	}
	if g2.Status != game.StatusOpen {
		t.Errorf("open game must survive the sweeper, got %q", g2.Status)
	}
}

// TestOpenGameLobbyShowsEmptySeat checks an open game appears in the starter's lobby
// with two seats, one of them the still-empty opponent (uuid.Nil).
func TestOpenGameLobbyShowsEmptySeat(t *testing.T) {
	ctx := context.Background()
	clearOpenGames(t)
	svc := newGameService()
	starter := provisionAccount(t)
	g := openGame(t, svc, starter, evenOpeningSeed(t))

	games, err := svc.ListForAccount(ctx, starter)
	if err != nil {
		t.Fatalf("list for account: %v", err)
	}
	var found *game.Game
	for i := range games {
		if games[i].ID == g.ID {
			found = &games[i]
			break
		}
	}
	if found == nil {
		t.Fatal("open game must appear in the starter's lobby")
	}
	var hasStarter, hasEmpty bool
	for _, s := range found.Seats {
		switch s.AccountID {
		case starter:
			hasStarter = true
		case uuid.Nil:
			hasEmpty = true
		}
	}
	if len(found.Seats) != 2 || !hasStarter || !hasEmpty {
		t.Errorf("open game seats = %+v, want the starter and one empty (nil) seat", found.Seats)
	}
}
