package server

import (
	"context"
	"net/http"
	"testing"
	"time"

	"github.com/google/uuid"

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

func TestStatusForError(t *testing.T) {
	cases := map[string]struct {
		err        error
		wantStatus int
		wantCode   string
	}{
		"not a player":   {game.ErrNotAPlayer, http.StatusForbidden, "not_a_player"},
		"not your turn":  {game.ErrNotYourTurn, http.StatusConflict, "not_your_turn"},
		"nudge own turn": {social.ErrNudgeOnOwnTurn, http.StatusConflict, "nudge_own_turn"},
		"nudge too soon": {social.ErrNudgeTooSoon, http.StatusConflict, "nudge_too_soon"},
		"chat off turn":  {social.ErrChatNotYourTurn, http.StatusConflict, "chat_not_your_turn"},
		"chat one/turn":  {social.ErrChatAlreadySentThisTurn, http.StatusConflict, "chat_already_sent"},
		"illegal play":   {engine.ErrIllegalPlay, http.StatusUnprocessableEntity, "illegal_play"},
		"email taken":    {account.ErrEmailTaken, http.StatusConflict, "email_taken"},
		"code mismatch":  {account.ErrCodeMismatch, http.StatusUnauthorized, "code_invalid"},
		"too many codes": {account.ErrTooManyRequests, http.StatusTooManyRequests, "too_many_requests"},
		"session gone":   {session.ErrNotFound, http.StatusUnauthorized, "session_invalid"},
		"chat forbidden": {social.ErrForbiddenContent, http.StatusUnprocessableEntity, "chat_rejected"},
		"no hint move":   {game.ErrNoHintAvailable, http.StatusConflict, "no_hint_available"},
		"no hints left":  {game.ErrNoHintsLeft, http.StatusConflict, "hint_unavailable"},
		"game limit":     {game.ErrGameLimitReached, http.StatusConflict, "game_limit_reached"},
		"unknown -> 500": {context_deadline, http.StatusInternalServerError, "internal"},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			status, code := statusForError(tc.err)
			if status != tc.wantStatus || code != tc.wantCode {
				t.Fatalf("statusForError(%v) = (%d, %q), want (%d, %q)", tc.err, status, code, tc.wantStatus, tc.wantCode)
			}
		})
	}
}

// context_deadline is an arbitrary unmapped error standing in for "anything
// unrecognised", which must fall through to 500/internal.
var context_deadline = errNew("boom")

type simpleErr string

func (e simpleErr) Error() string { return string(e) }
func errNew(s string) error       { return simpleErr(s) }

func TestGameDTOFromGame(t *testing.T) {
	gid, aid := uuid.New(), uuid.New()
	g := game.Game{
		ID:          gid,
		Variant:     engine.VariantEnglish,
		DictVersion: "v1",
		Status:      game.StatusActive,
		Players:     2,
		ToMove:      1,
		TurnTimeout: 24 * time.Hour,
		MoveCount:   3,
		Seats:       []game.Seat{{Seat: 0, AccountID: aid, Score: 12}},
	}
	dto := gameDTOFromGame(g)
	if dto.ID != gid.String() || dto.Variant != "scrabble_en" || dto.ToMove != 1 || dto.TurnTimeoutSecs != 86400 {
		t.Fatalf("game dto mismatch: %+v", dto)
	}
	if len(dto.Seats) != 1 || dto.Seats[0].AccountID != aid.String() || dto.Seats[0].Score != 12 {
		t.Fatalf("seat dto mismatch: %+v", dto.Seats)
	}
}

func TestProfileResponseForAwayWindow(t *testing.T) {
	acc := account.Account{
		ID:                uuid.New(),
		DisplayName:       "Kaya",
		PreferredLanguage: "ru",
		TimeZone:          "Europe/Moscow",
		AwayStart:         time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC),
		AwayEnd:           time.Date(0, 1, 1, 7, 30, 0, 0, time.UTC),
	}
	dto := profileResponseFor(acc)
	if dto.AwayStart != "00:00" || dto.AwayEnd != "07:30" {
		t.Fatalf("away window = (%q, %q), want (00:00, 07:30)", dto.AwayStart, dto.AwayEnd)
	}
	if dto.PreferredLanguage != "ru" || dto.TimeZone != "Europe/Moscow" {
		t.Fatalf("profile dto mismatch: %+v", dto)
	}
}

func TestMoveRecordDTOFrom(t *testing.T) {
	rec := engine.MoveRecord{
		Player:  1,
		Action:  engine.ActionPlay,
		Dir:     engine.Vertical,
		MainRow: 7,
		MainCol: 7,
		Tiles:   []engine.TileRecord{{Row: 7, Col: 7, Letter: "A", Blank: false}},
		Words:   []string{"AB"},
		Score:   10,
		Total:   10,
	}
	dto := moveRecordDTOFrom(rec)
	if dto.Action != "play" || dto.Dir != "V" || dto.Score != 10 || len(dto.Tiles) != 1 || dto.Tiles[0].Letter != "A" {
		t.Fatalf("move dto mismatch: %+v", dto)
	}
}

// TestGameDTOCarriesSeatSnapshot checks the DTO carries each seat's display-name
// snapshot (so a disguised robot's per-game name reaches the REST views, not just live
// events) rather than leaving it for an account lookup.
func TestGameDTOCarriesSeatSnapshot(t *testing.T) {
	g := game.Game{
		Variant: engine.VariantEnglish, Players: 2, TurnTimeout: time.Hour,
		Seats: []game.Seat{
			{Seat: 0, AccountID: uuid.New(), DisplayName: "Звёздный_Барс2"},
			{Seat: 1, AccountID: uuid.New(), DisplayName: "Тест"},
		},
	}
	dto := gameDTOFromGame(g)
	if dto.Seats[0].DisplayName != "Звёздный_Барс2" || dto.Seats[1].DisplayName != "Тест" {
		t.Fatalf("seat snapshot not carried into DTO: %+v", dto.Seats)
	}
}

// TestFillSeatNamesKeepsSnapshot checks fillSeatNames leaves a seat whose snapshot is
// already set untouched (no account lookup), so the per-game name is not overwritten by
// the robot's seeded account name.
func TestFillSeatNamesKeepsSnapshot(t *testing.T) {
	s := &Server{} // accounts is unused: a present snapshot must skip the lookup
	dto := &gameDTO{Seats: []seatDTO{
		{Seat: 0, AccountID: uuid.New().String(), DisplayName: "Звёздный_Барс2"},
		{Seat: 1, AccountID: uuid.New().String(), DisplayName: "Тест"},
	}}
	s.fillSeatNames(context.Background(), dto, map[string]string{})
	if dto.Seats[0].DisplayName != "Звёздный_Барс2" || dto.Seats[1].DisplayName != "Тест" {
		t.Fatalf("fillSeatNames overwrote a seat snapshot: %+v", dto.Seats)
	}
}
