package server

import (
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"

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

// newExportServer is the routing harness with a signing key installed, so the
// pre-service branches of the export endpoints are reachable.
func newExportServer() *Server {
	return New(":0", Deps{
		Sessions:      &session.Service{},
		Accounts:      &account.Store{},
		Games:         &game.Service{},
		ExportSignKey: "test-key",
	})
}

func TestSignExportIsDeterministicAndTamperEvident(t *testing.T) {
	s := newExportServer()
	sig := s.signExport("g1", "png", "123", "ru", "Europe/Moscow", "пас,обмен")
	if sig != s.signExport("g1", "png", "123", "ru", "Europe/Moscow", "пас,обмен") {
		t.Fatal("same inputs produced different signatures")
	}
	for _, tampered := range []string{
		s.signExport("g2", "png", "123", "ru", "Europe/Moscow", "пас,обмен"),
		s.signExport("g1", "gcg", "123", "ru", "Europe/Moscow", "пас,обмен"),
		s.signExport("g1", "png", "124", "ru", "Europe/Moscow", "пас,обмен"),
		s.signExport("g1", "png", "123", "en", "Europe/Moscow", "пас,обмен"),
		s.signExport("g1", "png", "123", "ru", "UTC", "пас,обмен"),
		s.signExport("g1", "png", "123", "ru", "Europe/Moscow", "пас"),
	} {
		if tampered == sig {
			t.Fatal("tampered input produced the same signature")
		}
	}
}

func TestExportURLWithoutKeyIs503(t *testing.T) {
	s := New(":0", Deps{Sessions: &session.Service{}, Accounts: &account.Store{}, Games: &game.Service{}})
	rec := do(t, s, http.MethodGet, "/api/v1/user/games/"+uuid.NewString()+"/export-url?kind=png", "",
		map[string]string{"X-User-ID": uuid.NewString()})
	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("mint without key = %d, want 503", rec.Code)
	}
}

func TestExportURLRejectsBadParams(t *testing.T) {
	s := newExportServer()
	uid := map[string]string{"X-User-ID": uuid.NewString()}
	gid := uuid.NewString()

	if rec := do(t, s, http.MethodGet, "/api/v1/user/games/"+gid+"/export-url?kind=exe", "", uid); rec.Code != http.StatusBadRequest {
		t.Fatalf("bad kind = %d, want 400", rec.Code)
	}
	long := strings.Repeat("x", maxLabelsLen+1)
	if rec := do(t, s, http.MethodGet, "/api/v1/user/games/"+gid+"/export-url?kind=png&t="+long, "", uid); rec.Code != http.StatusBadRequest {
		t.Fatalf("oversized labels = %d, want 400", rec.Code)
	}
}

func TestExportDownloadRejectsBadSignatures(t *testing.T) {
	s := newExportServer()
	gid := uuid.NewString()
	exp := strconv.FormatInt(time.Now().Add(time.Minute).Unix(), 10)

	// A wrong signature never reaches the domain (404 before any service call).
	url := "/api/v1/public/dl/" + gid + "/png?e=" + exp + "&s=forged"
	if rec := do(t, s, http.MethodGet, url, "", nil); rec.Code != http.StatusNotFound {
		t.Fatalf("forged signature = %d, want 404", rec.Code)
	}

	// A valid signature over an EXPIRED timestamp is refused the same way.
	old := strconv.FormatInt(time.Now().Add(-time.Minute).Unix(), 10)
	url = "/api/v1/public/dl/" + gid + "/png?e=" + old + "&s=" + s.signExport(gid, "png", old, "", "", "")
	if rec := do(t, s, http.MethodGet, url, "", nil); rec.Code != http.StatusNotFound {
		t.Fatalf("expired link = %d, want 404", rec.Code)
	}

	// An unknown kind is refused before signature checks.
	url = "/api/v1/public/dl/" + gid + "/exe?e=" + exp + "&s=x"
	if rec := do(t, s, http.MethodGet, url, "", nil); rec.Code != http.StatusNotFound {
		t.Fatalf("bad kind = %d, want 404", rec.Code)
	}
}

func TestRenderPayloadMapsTheExportView(t *testing.T) {
	gid := uuid.New()
	finished := time.Unix(1_782_997_629, 0)
	g := game.Game{
		ID:         gid,
		Variant:    engine.VariantRussianScrabble,
		Status:     game.StatusFinished,
		Players:    2,
		FinishedAt: &finished,
		Seats: []game.Seat{
			{Seat: 0, Score: 42, IsWinner: true, DisplayName: "snapshot"},
			{Seat: 1, Score: 30},
		},
	}
	moves := []game.HistoryMove{
		{
			Seat: 0, Action: "play", Dir: "H", MainRow: 7, MainCol: 4,
			Tiles: []engine.TileRecord{{Row: 7, Col: 4, Letter: "ч", Blank: false}},
			Words: []string{"чика"}, Score: 9, RunningTotal: 9,
		},
		{Seat: 1, Action: "exchange", Exchanged: []string{"а", "б"}, RunningTotal: 0},
	}
	names := []string{"Аня", "Боб"}

	p, err := renderPayload(g, moves, names, "ru", "Europe/Moscow", "пас,обмен,сдался,таймаут", "erudit-game.ru")
	if err != nil {
		t.Fatalf("renderPayload: %v", err)
	}
	if p.Game.LastActivityUnix != finished.Unix() {
		t.Fatalf("lastActivityUnix = %d, want %d", p.Game.LastActivityUnix, finished.Unix())
	}
	if p.Game.Seats[0].DisplayName != "Аня" || !p.Game.Seats[0].IsWinner {
		t.Fatalf("seat 0 = %+v, want the resolved name and the winner flag", p.Game.Seats[0])
	}
	if p.Moves[0].Words[0] != "ЧИКА" || p.Moves[0].Tiles[0].Letter != "Ч" {
		t.Fatalf("letters not upper-cased: %+v", p.Moves[0])
	}
	if p.Moves[1].Count != 2 {
		t.Fatalf("exchange count = %d, want 2", p.Moves[1].Count)
	}
	if p.Labels["pass"] != "пас" || p.Labels["timeout"] != "таймаут" {
		t.Fatalf("labels not mapped positionally: %v", p.Labels)
	}
	if len(p.Alphabet) == 0 || p.Hostname != "erudit-game.ru" || p.TimeZone != "Europe/Moscow" {
		t.Fatalf("alphabet/hostname/zone missing: %d entries, host %q, tz %q", len(p.Alphabet), p.Hostname, p.TimeZone)
	}
	for _, a := range p.Alphabet {
		if a.Letter != strings.ToUpper(a.Letter) {
			t.Fatalf("alphabet letter %q not upper-cased", a.Letter)
		}
	}
}
