package httpapi

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestHandlePlayerStatusAccepted(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "2026-03-22T16:00:00Z",
		"status": "running",
		"server_connectivity": "online",
		"server_url": "http://127.0.0.1:8080",
		"mqtt_broker": "tcp://127.0.0.1:1883",
		"heartbeat_every_seconds": 30,
		"started_at": "2026-03-22T15:59:30Z",
		"last_heartbeat_at": "2026-03-22T16:00:00Z"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(store)(w, req)

	if got, want := w.Code, http.StatusOK; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}

	var response struct {
		Status string `json:"status"`
	}

	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := response.Status, "accepted"; got != want {
		t.Fatalf("response status = %q, want %q", got, want)
	}

	stored, ok := store.Get("info01-dev")
	if !ok {
		t.Fatal("store.Get() ok = false, want true")
	}

	if got, want := stored.ScreenID, "info01-dev"; got != want {
		t.Fatalf("stored.ScreenID = %q, want %q", got, want)
	}

	if got, want := stored.ServerConnectivity, "online"; got != want {
		t.Fatalf("stored.ServerConnectivity = %q, want %q", got, want)
	}

	if stored.ReceivedAt == "" {
		t.Fatal("stored.ReceivedAt = empty, want server receive timestamp")
	}
}

func TestHandlePlayerStatusRejectsInvalidJSON(t *testing.T) {
	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewBufferString("{"))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsMissingScreenID(t *testing.T) {
	body := []byte(`{
		"screen_id": "   ",
		"ts": "2026-03-22T16:00:00Z",
		"status": "running"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusStoresNormalizedScreenID(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	body := []byte(`{
		"screen_id": "  info01-dev  ",
		"ts": "2026-03-22T16:00:00Z",
		"status": "running",
		"heartbeat_every_seconds": 30
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(store)(w, req)

	if got, want := w.Code, http.StatusOK; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}

	if _, ok := store.Get("info01-dev"); !ok {
		t.Fatal("store.Get(normalized) ok = false, want true")
	}
}

func TestHandlePlayerStatusRejectsMissingTimestamp(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"status": "running"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsMissingStatus(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "2026-03-22T16:00:00Z"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsUnknownStatus(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "2026-03-22T16:00:00Z",
		"status": "broken"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsUnknownServerConnectivity(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "2026-03-22T16:00:00Z",
		"status": "running",
		"server_connectivity": "maybe"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsNonPositiveHeartbeatInterval(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "2026-03-22T16:00:00Z",
		"status": "running",
		"heartbeat_every_seconds": 0
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsMalformedTimestamps(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "not-a-time",
		"status": "running"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsMalformedStartedAt(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "2026-03-22T16:00:00Z",
		"status": "running",
		"started_at": "not-a-time"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandlePlayerStatusRejectsMalformedLastHeartbeatAt(t *testing.T) {
	body := []byte(`{
		"screen_id": "info01-dev",
		"ts": "2026-03-22T16:00:00Z",
		"status": "running",
		"last_heartbeat_at": "not-a-time"
	}`)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
	w := httptest.NewRecorder()

	handlePlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandleGetLatestPlayerStatus(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 1, 0, 0, time.UTC)
	}
	store.Save(playerStatusRecord{
		ScreenID:              "info01-dev",
		Timestamp:             "2026-03-22T16:00:00Z",
		Status:                "running",
		ServerConnectivity:    "degraded",
		ReceivedAt:            "2026-03-22T16:00:05Z",
		ServerURL:             "http://127.0.0.1:8080",
		MQTTBroker:            "tcp://127.0.0.1:1883",
		HeartbeatEverySeconds: 30,
		StartedAt:             "2026-03-22T15:59:30Z",
		LastHeartbeatAt:       "2026-03-22T16:00:00Z",
	})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/info01-dev/status", nil)
	req.SetPathValue("screenId", "info01-dev")
	w := httptest.NewRecorder()

	handleGetLatestPlayerStatus(store)(w, req)

	if got, want := w.Code, http.StatusOK; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}

	var response playerStatusRecord
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := response.ScreenID, "info01-dev"; got != want {
		t.Fatalf("response.ScreenID = %q, want %q", got, want)
	}

	if got, want := response.ServerConnectivity, "degraded"; got != want {
		t.Fatalf("response.ServerConnectivity = %q, want %q", got, want)
	}

	if got, want := response.Stale, false; got != want {
		t.Fatalf("response.Stale = %v, want %v", got, want)
	}

	if got, want := response.DerivedState, "degraded"; got != want {
		t.Fatalf("response.DerivedState = %q, want %q", got, want)
	}
}

func TestHandleGetLatestPlayerStatusMarksStaleRecords(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC)
	}
	store.Save(playerStatusRecord{
		ScreenID:              "stale-screen",
		Timestamp:             "2026-03-22T16:00:00Z",
		Status:                "running",
		ServerConnectivity:    "offline",
		ReceivedAt:            "2026-03-22T16:00:00Z",
		HeartbeatEverySeconds: 30,
	})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/stale-screen/status", nil)
	req.SetPathValue("screenId", "stale-screen")
	w := httptest.NewRecorder()

	handleGetLatestPlayerStatus(store)(w, req)

	if got, want := w.Code, http.StatusOK; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}

	var response playerStatusRecord
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := response.Stale, true; got != want {
		t.Fatalf("response.Stale = %v, want %v", got, want)
	}

	if got, want := response.DerivedState, "offline"; got != want {
		t.Fatalf("response.DerivedState = %q, want %q", got, want)
	}
}

func TestHandleGetLatestPlayerStatusUsesHeartbeatIntervalForFreshness(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 3, 30, 0, time.UTC)
	}
	store.Save(playerStatusRecord{
		ScreenID:              "slow-screen",
		Timestamp:             "2026-03-22T16:00:00Z",
		Status:                "running",
		ServerConnectivity:    "online",
		ReceivedAt:            "2026-03-22T16:00:00Z",
		HeartbeatEverySeconds: 120,
	})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/slow-screen/status", nil)
	req.SetPathValue("screenId", "slow-screen")
	w := httptest.NewRecorder()

	handleGetLatestPlayerStatus(store)(w, req)

	var response playerStatusRecord
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := response.Stale, false; got != want {
		t.Fatalf("response.Stale = %v, want %v", got, want)
	}
}

func TestHandleGetLatestPlayerStatusDerivesOnlineState(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 0, 30, 0, time.UTC)
	}
	store.Save(playerStatusRecord{
		ScreenID:              "online-screen",
		Timestamp:             "2026-03-22T16:00:00Z",
		Status:                "running",
		ServerConnectivity:    "online",
		ReceivedAt:            "2026-03-22T16:00:00Z",
		HeartbeatEverySeconds: 30,
	})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/online-screen/status", nil)
	req.SetPathValue("screenId", "online-screen")
	w := httptest.NewRecorder()

	handleGetLatestPlayerStatus(store)(w, req)

	var response playerStatusRecord
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := response.DerivedState, "online"; got != want {
		t.Fatalf("response.DerivedState = %q, want %q", got, want)
	}
}

func TestHandleGetLatestPlayerStatusNotFound(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/missing/status", nil)
	req.SetPathValue("screenId", "missing")
	w := httptest.NewRecorder()

	handleGetLatestPlayerStatus(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusNotFound; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatuses(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.Save(playerStatusRecord{ScreenID: "screen-b", Timestamp: "2026-03-22T16:00:01Z", Status: "running", ServerConnectivity: "online"})
	store.Save(playerStatusRecord{ScreenID: "screen-a", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "degraded"})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	if got, want := w.Code, http.StatusOK; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}

	var response struct {
		Summary struct {
			Total    int `json:"total"`
			Online   int `json:"online"`
			Degraded int `json:"degraded"`
			Offline  int `json:"offline"`
			Stale    int `json:"stale"`
		} `json:"summary"`
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := len(response.Screens), 2; got != want {
		t.Fatalf("len(response.Screens) = %d, want %d", got, want)
	}

	if got, want := response.Screens[0].ScreenID, "screen-a"; got != want {
		t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
	}

	if got, want := response.Screens[1].ScreenID, "screen-b"; got != want {
		t.Fatalf("response.Screens[1].ScreenID = %q, want %q", got, want)
	}

	if got, want := response.Summary.Total, 2; got != want {
		t.Fatalf("response.Summary.Total = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesOrdersProblematicScreensFirst(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC)
	}
	store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-degraded", Timestamp: "2026-03-22T16:09:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:09:00Z", HeartbeatEverySeconds: 30})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	var response struct {
		Summary struct {
			Total    int `json:"total"`
			Online   int `json:"online"`
			Degraded int `json:"degraded"`
			Offline  int `json:"offline"`
			Stale    int `json:"stale"`
		} `json:"summary"`
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := response.Screens[0].ScreenID, "screen-offline"; got != want {
		t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
	}

	if got, want := response.Screens[1].ScreenID, "screen-degraded"; got != want {
		t.Fatalf("response.Screens[1].ScreenID = %q, want %q", got, want)
	}

	if got, want := response.Screens[2].ScreenID, "screen-online"; got != want {
		t.Fatalf("response.Screens[2].ScreenID = %q, want %q", got, want)
	}
}

func TestHandleListLatestPlayerStatusesFiltersByConnectivityAndStale(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC)
	}
	store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=offline&stale=true", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	if got, want := w.Code, http.StatusOK; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}

	var response struct {
		Summary struct {
			Total    int `json:"total"`
			Online   int `json:"online"`
			Degraded int `json:"degraded"`
			Offline  int `json:"offline"`
			Stale    int `json:"stale"`
		} `json:"summary"`
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := len(response.Screens), 1; got != want {
		t.Fatalf("len(response.Screens) = %d, want %d", got, want)
	}

	if got, want := response.Screens[0].ScreenID, "screen-offline"; got != want {
		t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
	}

	if got, want := response.Summary.Offline, 1; got != want {
		t.Fatalf("response.Summary.Offline = %d, want %d", got, want)
	}

	if got, want := response.Summary.Online, 1; got != want {
		t.Fatalf("response.Summary.Online = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesAppliesLimit(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC)
	}
	store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-degraded", Timestamp: "2026-03-22T16:09:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:09:00Z", HeartbeatEverySeconds: 30})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?limit=2", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	var response struct {
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := len(response.Screens), 2; got != want {
		t.Fatalf("len(response.Screens) = %d, want %d", got, want)
	}

	if got, want := response.Screens[0].ScreenID, "screen-offline"; got != want {
		t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
	}

	if got, want := response.Screens[1].ScreenID, "screen-degraded"; got != want {
		t.Fatalf("response.Screens[1].ScreenID = %q, want %q", got, want)
	}
}

func TestHandleListLatestPlayerStatusesFiltersByScreenIDSubstring(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.Save(playerStatusRecord{ScreenID: "info01-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"})
	store.Save(playerStatusRecord{ScreenID: "info02-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"})
	store.Save(playerStatusRecord{ScreenID: "lobby-main", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?q=info", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	var response struct {
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := len(response.Screens), 2; got != want {
		t.Fatalf("len(response.Screens) = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesScreenIDFilterIsCaseInsensitive(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.Save(playerStatusRecord{ScreenID: "INFO01-DEV", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"})
	store.Save(playerStatusRecord{ScreenID: "lobby-main", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?q=info01", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	var response struct {
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := len(response.Screens), 1; got != want {
		t.Fatalf("len(response.Screens) = %d, want %d", got, want)
	}

	if got, want := response.Screens[0].ScreenID, "INFO01-DEV"; got != want {
		t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
	}
}

func TestHandleListLatestPlayerStatusesFiltersByDerivedState(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.now = func() time.Time {
		return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC)
	}
	store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-degraded", Timestamp: "2026-03-22T16:09:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:09:00Z", HeartbeatEverySeconds: 30})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?derived_state=online", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	var response struct {
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := len(response.Screens), 1; got != want {
		t.Fatalf("len(response.Screens) = %d, want %d", got, want)
	}

	if got, want := response.Screens[0].ScreenID, "screen-online"; got != want {
		t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
	}
}

func TestHandleListLatestPlayerStatusesRejectsInvalidDerivedState(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?derived_state=unknown", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesRejectsInvalidServerConnectivity(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=garbage", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesRejectsInvalidStale(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?stale=maybe", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesRejectsInvalidUpdatedSince(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?updated_since=not-a-time", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesRejectsInvalidLimit(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?limit=0", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req)

	if got, want := w.Code, http.StatusBadRequest; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}

func TestHandleListLatestPlayerStatusesFiltersByUpdatedSince(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.Save(playerStatusRecord{ScreenID: "screen-old", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30})
	store.Save(playerStatusRecord{ScreenID: "screen-new", Timestamp: "2026-03-22T16:06:00Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:06:00Z", HeartbeatEverySeconds: 30})

	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?updated_since=2026-03-22T16:05:00Z", nil)
	w := httptest.NewRecorder()

	handleListLatestPlayerStatuses(store)(w, req)

	var response struct {
		Screens []playerStatusRecord `json:"screens"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}

	if got, want := len(response.Screens), 1; got != want {
		t.Fatalf("len(response.Screens) = %d, want %d", got, want)
	}

	if got, want := response.Screens[0].ScreenID, "screen-new"; got != want {
		t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want)
	}
}

func TestHandleDeletePlayerStatusOK(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.Save(playerStatusRecord{ScreenID: "info01-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running"})

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/screens/info01-dev/status", nil)
	req.SetPathValue("screenId", "info01-dev")
	w := httptest.NewRecorder()

	handleDeletePlayerStatus(store)(w, req)

	if got, want := w.Code, http.StatusOK; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}

	var response map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("Unmarshal() error = %v", err)
	}
	if got, want := response["status"], "deleted"; got != want {
		t.Fatalf("status = %q, want %q", got, want)
	}

	if _, ok := store.Get("info01-dev"); ok {
		t.Fatal("record still present after delete")
	}
}

func TestHandleDeletePlayerStatusNotFound(t *testing.T) {
	store := newInMemoryPlayerStatusStore()

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/screens/missing/status", nil)
	req.SetPathValue("screenId", "missing")
	w := httptest.NewRecorder()

	handleDeletePlayerStatus(store)(w, req)

	if got, want := w.Code, http.StatusNotFound; got != want {
		t.Fatalf("status = %d, want %d", got, want)
	}
}
