package handlers

import (
	"context"
	"encoding/json"
	"net/http"
	"testing"

	"github.com/gin-gonic/gin"

	"switchboard-core/config"
	"switchboard-core/database"
	"switchboard-core/middleware"
	"switchboard-core/models"
	"switchboard-core/notifications"
	"switchboard-core/store"
	postgres "switchboard-core/store/postgres"
	sqlite "switchboard-core/store/sqlite"
)

// ── Notification Test Harness ──────────────

type notifHarness struct {
	*testHarness
	stores     store.Stores
	adminToken string
	adminID    string
	userToken  string
	userID     string
}

func setupNotifHarness(t *testing.T) *notifHarness {
	t.Helper()
	database.RequireTestDB(t)
	database.TruncateAll(t)

	cfg := &config.Config{
		JWTSecret: testJWTSecret,
		BasePath:  "",
	}

	var stores store.Stores
	if database.IsSQLite() {
		stores = sqlite.NewStores(database.TestDB)
	} else {
		stores = postgres.NewStores(database.TestDB)
	}
	userCache := middleware.NewUserStatusCache()

	r := gin.New()
	api := r.Group("/api/v1")
	protected := api.Group("")
	protected.Use(middleware.Auth(cfg, stores.Users, userCache))

	notifH := NewNotificationHandler(stores, nil) // no hub in tests
	protected.GET("/notifications", notifH.List)
	protected.GET("/notifications/unread-count", notifH.UnreadCount)
	protected.PATCH("/notifications/:id/read", notifH.MarkRead)
	protected.POST("/notifications/mark-all-read", notifH.MarkAllRead)
	protected.DELETE("/notifications/:id", notifH.Delete)
	protected.GET("/notifications/preferences", notifH.ListPreferences)
	protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
	protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)

	// Admin broadcast route (v0.28.6)
	admin := api.Group("/admin")
	admin.Use(middleware.Auth(cfg, stores.Users, userCache))
	admin.Use(middleware.RequireAdmin(stores))
	admin.POST("/notifications/broadcast", notifH.Broadcast)

	// Set up notification service singleton for Broadcast handler
	notifSvc := notifications.NewService(stores.Notifications, nil)
	notifications.SetDefault(notifSvc)

	// Seed admin
	adminID := database.SeedTestUser(t, "notif-admin", "notif-admin@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), adminID)
	database.SeedAdminsGroupMember(t, adminID)
	adminToken := makeToken(adminID, "notif-admin@test.com", "admin")

	// Seed regular user
	userID := database.SeedTestUser(t, "notif-user", "notif-user@test.com")
	database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), userID)
	userToken := makeToken(userID, "notif-user@test.com", "user")

	return &notifHarness{
		testHarness: &testHarness{router: r, t: t},
		stores:      stores,
		adminToken:  adminToken,
		adminID:     adminID,
		userToken:   userToken,
		userID:      userID,
	}
}

// seedNotification inserts a notification directly via the store.
func (h *notifHarness) seedNotification(userID, notifType, title string) string {
	h.t.Helper()
	n := &models.Notification{
		UserID: userID,
		Type:   notifType,
		Title:  title,
	}
	if err := h.stores.Notifications.Create(context.Background(), n); err != nil {
		h.t.Fatalf("seedNotification: %v", err)
	}
	return n.ID
}

// ── GET /notifications ─────────────────────

func TestNotifications_List_Empty(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("GET", "/api/v1/notifications", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body struct {
		Data   []json.RawMessage `json:"data"`
		Total  int               `json:"total"`
		Limit  int               `json:"limit"`
		Offset int               `json:"offset"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)

	if body.Data == nil {
		t.Error("data should be [] not null")
	}
	if len(body.Data) != 0 {
		t.Errorf("expected 0 items, got %d", len(body.Data))
	}
	if body.Total != 0 {
		t.Errorf("total: got %d, want 0", body.Total)
	}
	if body.Limit != 20 {
		t.Errorf("default limit: got %d, want 20", body.Limit)
	}
}

func TestNotifications_List_Pagination(t *testing.T) {
	h := setupNotifHarness(t)

	// Seed 5 notifications
	for i := 0; i < 5; i++ {
		h.seedNotification(h.userID, "test.type", "Test Notification")
	}

	resp := h.request("GET", "/api/v1/notifications?limit=2&offset=0", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d", resp.Code)
	}

	var body struct {
		Data  []json.RawMessage `json:"data"`
		Total int               `json:"total"`
		Limit int               `json:"limit"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)

	if len(body.Data) != 2 {
		t.Errorf("items: got %d, want 2", len(body.Data))
	}
	if body.Total != 5 {
		t.Errorf("total: got %d, want 5", body.Total)
	}
	if body.Limit != 2 {
		t.Errorf("limit: got %d, want 2", body.Limit)
	}
}

func TestNotifications_List_UnreadOnly(t *testing.T) {
	h := setupNotifHarness(t)

	id1 := h.seedNotification(h.userID, "test.type", "Unread One")
	h.seedNotification(h.userID, "test.type", "Unread Two")
	_ = id1

	// Mark one as read
	h.stores.Notifications.MarkRead(context.Background(), id1, h.userID)

	resp := h.request("GET", "/api/v1/notifications?unread_only=true", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d", resp.Code)
	}

	var body struct {
		Data  []json.RawMessage `json:"data"`
		Total int               `json:"total"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)

	if len(body.Data) != 1 {
		t.Errorf("unread items: got %d, want 1", len(body.Data))
	}
	if body.Total != 1 {
		t.Errorf("total unread: got %d, want 1", body.Total)
	}
}

func TestNotifications_List_UserIsolation(t *testing.T) {
	h := setupNotifHarness(t)

	// Seed notifications for admin, not user
	h.seedNotification(h.adminID, "test.type", "Admin Only")

	resp := h.request("GET", "/api/v1/notifications", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d", resp.Code)
	}

	var body struct {
		Data []json.RawMessage `json:"data"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)

	if len(body.Data) != 0 {
		t.Errorf("user should not see admin's notifications, got %d", len(body.Data))
	}
}

// ── GET /notifications/unread-count ────────

func TestNotifications_UnreadCount(t *testing.T) {
	h := setupNotifHarness(t)

	h.seedNotification(h.userID, "test.type", "N1")
	id2 := h.seedNotification(h.userID, "test.type", "N2")
	h.seedNotification(h.userID, "test.type", "N3")

	// Mark one as read
	h.stores.Notifications.MarkRead(context.Background(), id2, h.userID)

	resp := h.request("GET", "/api/v1/notifications/unread-count", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d", resp.Code)
	}

	var body struct {
		Count int `json:"count"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)

	if body.Count != 2 {
		t.Errorf("unread count: got %d, want 2", body.Count)
	}
}

// ── PATCH /notifications/:id/read ─────────

func TestNotifications_MarkRead(t *testing.T) {
	h := setupNotifHarness(t)

	id := h.seedNotification(h.userID, "test.type", "Read Me")

	resp := h.request("PATCH", "/api/v1/notifications/"+id+"/read", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	// Verify via unread-count
	resp = h.request("GET", "/api/v1/notifications/unread-count", h.userToken, nil)
	var body struct {
		Count int `json:"count"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)
	if body.Count != 0 {
		t.Errorf("unread count after mark read: got %d, want 0", body.Count)
	}
}

func TestNotifications_MarkRead_WrongUser(t *testing.T) {
	h := setupNotifHarness(t)

	// Notification belongs to admin
	id := h.seedNotification(h.adminID, "test.type", "Admin's")

	// User tries to mark it read
	resp := h.request("PATCH", "/api/v1/notifications/"+id+"/read", h.userToken, nil)
	if resp.Code != http.StatusNotFound {
		t.Errorf("cross-user mark read: got %d, want 404", resp.Code)
	}
}

func TestNotifications_MarkRead_NotFound(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("PATCH", "/api/v1/notifications/00000000-0000-0000-0000-000000000000/read", h.userToken, nil)
	if resp.Code != http.StatusNotFound {
		t.Errorf("non-existent: got %d, want 404", resp.Code)
	}
}

// ── POST /notifications/mark-all-read ─────

func TestNotifications_MarkAllRead(t *testing.T) {
	h := setupNotifHarness(t)

	h.seedNotification(h.userID, "test.type", "N1")
	h.seedNotification(h.userID, "test.type", "N2")
	h.seedNotification(h.userID, "test.type", "N3")

	resp := h.request("POST", "/api/v1/notifications/mark-all-read", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d", resp.Code)
	}

	// Verify
	resp = h.request("GET", "/api/v1/notifications/unread-count", h.userToken, nil)
	var body struct {
		Count int `json:"count"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)
	if body.Count != 0 {
		t.Errorf("unread count after mark-all-read: got %d, want 0", body.Count)
	}
}

func TestNotifications_MarkAllRead_IsolatesUsers(t *testing.T) {
	h := setupNotifHarness(t)

	// Both users have notifications
	h.seedNotification(h.userID, "test.type", "User's")
	h.seedNotification(h.adminID, "test.type", "Admin's")

	// User marks all read
	h.request("POST", "/api/v1/notifications/mark-all-read", h.userToken, nil)

	// Admin should still have unread
	resp := h.request("GET", "/api/v1/notifications/unread-count", h.adminToken, nil)
	var body struct {
		Count int `json:"count"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)
	if body.Count != 1 {
		t.Errorf("admin unread count: got %d, want 1", body.Count)
	}
}

// ── DELETE /notifications/:id ─────────────

func TestNotifications_Delete(t *testing.T) {
	h := setupNotifHarness(t)

	id := h.seedNotification(h.userID, "test.type", "Delete Me")

	resp := h.request("DELETE", "/api/v1/notifications/"+id, h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d", resp.Code)
	}

	// Verify gone
	resp = h.request("GET", "/api/v1/notifications", h.userToken, nil)
	var body struct {
		Data []json.RawMessage `json:"data"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)
	if len(body.Data) != 0 {
		t.Errorf("notification should be deleted, got %d", len(body.Data))
	}
}

func TestNotifications_Delete_WrongUser(t *testing.T) {
	h := setupNotifHarness(t)

	id := h.seedNotification(h.adminID, "test.type", "Admin's")

	resp := h.request("DELETE", "/api/v1/notifications/"+id, h.userToken, nil)
	if resp.Code != http.StatusNotFound {
		t.Errorf("cross-user delete: got %d, want 404", resp.Code)
	}
}

func TestNotifications_Delete_NotFound(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("DELETE", "/api/v1/notifications/00000000-0000-0000-0000-000000000000", h.userToken, nil)
	if resp.Code != http.StatusNotFound {
		t.Errorf("non-existent: got %d, want 404", resp.Code)
	}
}

// ── GET /notifications/preferences ────────

func TestNotifications_Preferences_Empty(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("GET", "/api/v1/notifications/preferences", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body struct {
		Data []json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(resp.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode: %v, raw: %s", err, resp.Body.String())
	}
	if body.Data == nil {
		t.Error("data should be [] not null")
	}
	if len(body.Data) != 0 {
		t.Errorf("expected empty prefs, got %d", len(body.Data))
	}
}

func TestNotifications_Preferences_Envelope(t *testing.T) {
	h := setupNotifHarness(t)

	// Verify response is wrapped in {"data": []}
	resp := h.request("GET", "/api/v1/notifications/preferences", h.userToken, nil)
	var raw map[string]json.RawMessage
	json.Unmarshal(resp.Body.Bytes(), &raw)

	if _, ok := raw["data"]; !ok {
		t.Errorf("response must have 'data' key, got keys: %v, raw: %s",
			keys(raw), resp.Body.String())
	}
}

// ── PUT /notifications/preferences/:type ──

func TestNotifications_SetPreference(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("PUT", "/api/v1/notifications/preferences/kb.ready", h.userToken, map[string]interface{}{
		"in_app": true,
		"email":  true,
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
	}

	var pref models.NotificationPreference
	json.Unmarshal(resp.Body.Bytes(), &pref)
	if !pref.InApp || !pref.Email {
		t.Errorf("expected in_app=true email=true, got %+v", pref)
	}

	// Verify via list
	resp = h.request("GET", "/api/v1/notifications/preferences", h.userToken, nil)
	var body struct {
		Data []models.NotificationPreference `json:"data"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)
	if len(body.Data) != 1 {
		t.Fatalf("prefs count: got %d, want 1", len(body.Data))
	}
	if body.Data[0].Type != "kb.ready" {
		t.Errorf("type: got %q, want %q", body.Data[0].Type, "kb.ready")
	}
}

func TestNotifications_SetPreference_PartialUpdate(t *testing.T) {
	h := setupNotifHarness(t)

	// Set both
	h.request("PUT", "/api/v1/notifications/preferences/kb.ready", h.userToken, map[string]interface{}{
		"in_app": true,
		"email":  true,
	})

	// Partial update: only change email
	resp := h.request("PUT", "/api/v1/notifications/preferences/kb.ready", h.userToken, map[string]interface{}{
		"email": false,
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("partial update: got %d", resp.Code)
	}

	var pref models.NotificationPreference
	json.Unmarshal(resp.Body.Bytes(), &pref)
	if !pref.InApp {
		t.Error("in_app should remain true after partial update")
	}
	if pref.Email {
		t.Error("email should be false after partial update")
	}
}

func TestNotifications_SetPreference_Wildcard(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("PUT", "/api/v1/notifications/preferences/*", h.userToken, map[string]interface{}{
		"in_app": false,
		"email":  true,
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("wildcard pref: got %d, body: %s", resp.Code, resp.Body.String())
	}

	var pref models.NotificationPreference
	json.Unmarshal(resp.Body.Bytes(), &pref)
	if pref.Type != "*" {
		t.Errorf("type: got %q, want %q", pref.Type, "*")
	}
}

// ── DELETE /notifications/preferences/:type ──

func TestNotifications_DeletePreference(t *testing.T) {
	h := setupNotifHarness(t)

	// Create then delete
	h.request("PUT", "/api/v1/notifications/preferences/kb.ready", h.userToken, map[string]interface{}{
		"email": true,
	})

	resp := h.request("DELETE", "/api/v1/notifications/preferences/kb.ready", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("delete pref: got %d", resp.Code)
	}

	// Verify gone
	resp = h.request("GET", "/api/v1/notifications/preferences", h.userToken, nil)
	var body struct {
		Data []models.NotificationPreference `json:"data"`
	}
	json.Unmarshal(resp.Body.Bytes(), &body)
	if len(body.Data) != 0 {
		t.Errorf("prefs should be empty after delete, got %d", len(body.Data))
	}
}

func TestNotifications_DeletePreference_Idempotent(t *testing.T) {
	h := setupNotifHarness(t)

	// Delete non-existent preference should still return 200 (idempotent)
	resp := h.request("DELETE", "/api/v1/notifications/preferences/nonexistent.type", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Errorf("idempotent delete: got %d, want 200", resp.Code)
	}
}

// ── Auth enforcement ──────────────────────

func TestNotifications_Unauthenticated(t *testing.T) {
	h := setupNotifHarness(t)

	endpoints := []struct {
		method string
		path   string
	}{
		{"GET", "/api/v1/notifications"},
		{"GET", "/api/v1/notifications/unread-count"},
		{"PATCH", "/api/v1/notifications/fake-id/read"},
		{"POST", "/api/v1/notifications/mark-all-read"},
		{"DELETE", "/api/v1/notifications/fake-id"},
		{"GET", "/api/v1/notifications/preferences"},
		{"PUT", "/api/v1/notifications/preferences/test"},
		{"DELETE", "/api/v1/notifications/preferences/test"},
	}

	for _, e := range endpoints {
		resp := h.request(e.method, e.path, "", nil)
		if resp.Code != http.StatusUnauthorized {
			t.Errorf("%s %s without token: got %d, want 401", e.method, e.path, resp.Code)
		}
	}
}

// ── Admin Broadcast (v0.28.6) ────────────

func TestBroadcast_Success(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
		"title":   "Maintenance Tonight",
		"message": "Servers will be down from 2-4 AM.",
		"level":   "warning",
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("broadcast: got %d, body: %s", resp.Code, resp.Body.String())
	}

	var body map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&body)
	if body["message"] != "broadcast sent" {
		t.Errorf("expected 'broadcast sent', got %v", body["message"])
	}
	count := body["count"].(float64)
	if count < 2 {
		t.Errorf("expected at least 2 recipients (admin+user), got %v", count)
	}

	// Verify notification created for regular user
	resp = h.request("GET", "/api/v1/notifications", h.userToken, nil)
	if resp.Code != http.StatusOK {
		t.Fatalf("list notifications: got %d", resp.Code)
	}
	var listBody map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&listBody)
	items := listBody["data"].([]interface{})
	if len(items) == 0 {
		t.Fatal("user should have received the broadcast notification")
	}
	n := items[0].(map[string]interface{})
	if n["type"] != "system.announcement" {
		t.Errorf("notification type: got %v, want system.announcement", n["type"])
	}
	if n["title"] != "Maintenance Tonight" {
		t.Errorf("notification title: got %v", n["title"])
	}
}

func TestBroadcast_NonAdmin403(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.userToken, map[string]interface{}{
		"title":   "Unauthorized",
		"message": "Should fail.",
	})
	if resp.Code != http.StatusForbidden {
		t.Fatalf("non-admin broadcast: want 403, got %d", resp.Code)
	}
}

func TestBroadcast_MissingTitle(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
		"message": "No title.",
	})
	if resp.Code != http.StatusBadRequest {
		t.Fatalf("missing title: want 400, got %d", resp.Code)
	}
}

func TestBroadcast_MissingMessage(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
		"title": "No message.",
	})
	if resp.Code != http.StatusBadRequest {
		t.Fatalf("missing message: want 400, got %d", resp.Code)
	}
}

func TestBroadcast_InvalidLevel(t *testing.T) {
	h := setupNotifHarness(t)

	resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
		"title":   "Bad level",
		"message": "Test",
		"level":   "extreme",
	})
	if resp.Code != http.StatusBadRequest {
		t.Fatalf("invalid level: want 400, got %d", resp.Code)
	}
}

func TestBroadcast_DefaultLevel(t *testing.T) {
	h := setupNotifHarness(t)

	// Omit level — should default to info and succeed
	resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
		"title":   "Info broadcast",
		"message": "Default level test.",
	})
	if resp.Code != http.StatusOK {
		t.Fatalf("default level: want 200, got %d, body: %s", resp.Code, resp.Body.String())
	}
}

// ── helpers ────────────────────────────────

func keys(m map[string]json.RawMessage) []string {
	var ks []string
	for k := range m {
		ks = append(ks, k)
	}
	return ks
}
