package httpapi

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

func TestRouterHealthz(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: newInMemoryPlayerStatusStore()}).ServeHTTP(w, req)

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

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

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

	if got, want := response.Service, "morz-infoboard-backend"; got != want {
		t.Fatalf("service = %q, want %q", got, want)
	}

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

func TestRouterBaseAPI(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1", nil)
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: newInMemoryPlayerStatusStore()}).ServeHTTP(w, req)

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

	var response struct {
		Name    string   `json:"name"`
		Version string   `json:"version"`
		Tools   []string `json:"tools"`
	}

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

	if got, want := response.Name, "morz-infoboard-backend"; got != want {
		t.Fatalf("name = %q, want %q", got, want)
	}

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

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

	if got, want := response.Tools[0], "message-wall-resolve"; got != want {
		t.Fatalf("tool[0] = %q, want %q", got, want)
	}

	if got, want := response.Tools[1], "screen-status-list"; got != want {
		t.Fatalf("tool[1] = %q, want %q", got, want)
	}

	if got, want := response.Tools[2], "screen-status-detail"; got != want {
		t.Fatalf("tool[2] = %q, want %q", got, want)
	}

	if got, want := response.Tools[3], "player-status-ingest"; got != want {
		t.Fatalf("tool[3] = %q, want %q", got, want)
	}

	if got, want := response.Tools[4], "screen-status-delete"; got != want {
		t.Fatalf("tool[4] = %q, want %q", got, want)
	}
}

func TestRouterMeta(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/v1/meta", nil)
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: newInMemoryPlayerStatusStore()}).ServeHTTP(w, req)

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

	var response struct {
		Service string `json:"service"`
		Version string `json:"version"`
		API     struct {
			BasePath string `json:"base_path"`
			Health   string `json:"health"`
			Tools    []struct {
				Name   string `json:"name"`
				Method string `json:"method"`
				Path   string `json:"path"`
			} `json:"tools"`
			DiagnosticUI struct {
				ScreenList   string `json:"screen_list"`
				ScreenDetail string `json:"screen_detail"`
			} `json:"diagnostic_ui"`
		} `json:"api"`
	}

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

	if got, want := response.API.BasePath, "/api/v1"; got != want {
		t.Fatalf("api.base_path = %q, want %q", got, want)
	}

	if got, want := response.API.Health, "/healthz"; got != want {
		t.Fatalf("api.health = %q, want %q", got, want)
	}

	if got, want := len(response.API.Tools), 5; got != want {
		t.Fatalf("len(api.tools) = %d, want %d", got, want)
	}

	if got, want := response.API.Tools[0].Path, "/api/v1/tools/message-wall/resolve"; got != want {
		t.Fatalf("api.tools[0].path = %q, want %q", got, want)
	}

	if got, want := response.API.Tools[1].Path, "/api/v1/screens/status"; got != want {
		t.Fatalf("api.tools[1].path = %q, want %q", got, want)
	}

	if got, want := response.API.Tools[2].Path, "/api/v1/screens/{screenId}/status"; got != want {
		t.Fatalf("api.tools[2].path = %q, want %q", got, want)
	}

	if got, want := response.API.Tools[3].Path, "/api/v1/player/status"; got != want {
		t.Fatalf("api.tools[3].path = %q, want %q", got, want)
	}

	if got, want := response.API.Tools[4].Path, "/api/v1/screens/{screenId}/status"; got != want {
		t.Fatalf("api.tools[4].path = %q, want %q", got, want)
	}

	if got, want := response.API.DiagnosticUI.ScreenList, "/status"; got != want {
		t.Fatalf("api.diagnostic_ui.screen_list = %q, want %q", got, want)
	}

	if got, want := response.API.DiagnosticUI.ScreenDetail, "/status/{screenId}"; got != want {
		t.Fatalf("api.diagnostic_ui.screen_detail = %q, want %q", got, want)
	}
}

func TestRouterPlayerStatusRoute(t *testing.T) {
	req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewBufferString(`{"screen_id":"demo","ts":"2026-03-22T16:00:00Z","status":"running","heartbeat_every_seconds":30}`))
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: newInMemoryPlayerStatusStore()}).ServeHTTP(w, req)

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

func TestRouterScreenStatusRoute(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	store.Save(playerStatusRecord{ScreenID: "demo", Timestamp: "2026-03-22T16:00:00Z", Status: "running"})
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/demo/status", nil)
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: store}).ServeHTTP(w, req)

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

func TestRouterScreenStatusListRoute(t *testing.T) {
	store := newInMemoryPlayerStatusStore()
	req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil)
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: store}).ServeHTTP(w, req)

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

func TestRouterScreenDetailPageRoute(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:              "info01-dev",
		Timestamp:             "2026-03-22T16:09:30Z",
		Status:                "running",
		ServerConnectivity:    "online",
		ReceivedAt:            "2026-03-22T16:09:30Z",
		HeartbeatEverySeconds: 30,
		ServerURL:             "http://127.0.0.1:8080",
		MQTTBroker:            "tcp://127.0.0.1:1883",
	})

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

	NewRouter(RouterDeps{StatusStore: store}).ServeHTTP(w, req)

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

	if got := w.Header().Get("Content-Type"); !strings.Contains(got, "text/html") {
		t.Fatalf("Content-Type = %q, want text/html", got)
	}

	body := w.Body.String()
	for _, want := range []string{
		"info01-dev",
		"online",
		"running",
		"fresh",
		"http://127.0.0.1:8080",
		"tcp://127.0.0.1:1883",
		"2026-03-22T16:09:30Z",
		"/api/v1/screens/info01-dev/status",
		"← Alle Bildschirme",
		"Zeitstempel",
		"Verbindungen",
		"<meta http-equiv=\"refresh\" content=\"15\">",
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("body missing %q", want)
		}
	}
}

func TestRouterScreenDetailPageNotFound(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/status/missing-screen", nil)
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: newInMemoryPlayerStatusStore()}).ServeHTTP(w, req)

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

	if got := w.Header().Get("Content-Type"); !strings.Contains(got, "text/html") {
		t.Fatalf("Content-Type = %q, want text/html", got)
	}

	if !strings.Contains(w.Body.String(), "← Zurück zum Bildschirmstatus") {
		t.Fatal("body missing back link")
	}
}

func TestRouterStatusPageRejectsInvalidQueryParams(t *testing.T) {
	cases := []struct {
		name  string
		query string
	}{
		{"invalid server_connectivity", "?server_connectivity=garbage"},
		{"invalid stale", "?stale=maybe"},
		{"invalid updated_since", "?updated_since=not-a-time"},
		{"invalid limit zero", "?limit=0"},
		{"invalid limit negative", "?limit=-1"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/status"+tc.query, nil)
			w := httptest.NewRecorder()

			NewRouter(RouterDeps{StatusStore: newInMemoryPlayerStatusStore()}).ServeHTTP(w, req)

			if got, want := w.Code, http.StatusBadRequest; got != want {
				t.Fatalf("status = %d, want %d", got, want)
			}
			if got := w.Header().Get("Content-Type"); !strings.Contains(got, "text/html") {
				t.Fatalf("Content-Type = %q, want text/html (error page must be HTML, not JSON)", got)
			}
		})
	}
}

func TestRouterStatusPageRoute(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, "/status?server_connectivity=offline&stale=true&updated_since=2026-03-22T15:55:00Z&limit=10", nil)
	w := httptest.NewRecorder()

	NewRouter(RouterDeps{StatusStore: store}).ServeHTTP(w, req)

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

	if got := w.Header().Get("Content-Type"); !strings.Contains(got, "text/html") {
		t.Fatalf("Content-Type = %q, want text/html", got)
	}

	body := w.Body.String()
	for _, want := range []string{
		"Bildschirmstatus",
		"2 Bildschirme",
		"<meta http-equiv=\"refresh\" content=\"15\">",
		"Konnektivität: Offline",
		"Konnektivität: Eingeschränkt",
		"Veraltete Meldungen",
		"Aktuelle Meldungen",
		"updated_since=2026-03-22T15%3A55%3A00Z",
		"screen-offline",
		"offline",
		"/api/v1/screens/screen-offline/status",
		"name=\"server_connectivity\"",
		"name=\"stale\"",
		"name=\"limit\"",
		"server_connectivity=offline",
		"stale=true",
		"value=\"10\"",
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("body missing %q", want)
		}
	}
}
