package httpapi

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"commilitia.net/cdrop/internal/apns"
	"commilitia.net/cdrop/internal/db"
	"commilitia.net/cdrop/internal/hub"
	"commilitia.net/cdrop/internal/jwtauth"
	"commilitia.net/cdrop/internal/push"
)

// apnsTestKeyPEM generates an ephemeral P-256 key and returns its PEM bytes so
// tests can build an enabled apns.Sender without real Apple credentials.
func apnsTestKeyPEM(t *testing.T) []byte {
	t.Helper()
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	der, err := x509.MarshalPKCS8PrivateKey(priv)
	if err != nil {
		t.Fatalf("marshal pkcs8: %v", err)
	}
	return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
}

// newAPNsTestServer builds a minimal Server with a real in-memory SQLite DB
// and an enabled apns.Sender (using an ephemeral test key). The apns.Sender
// base is left at the default; the endpoint test only exercises DB writes, so
// the Sender never fires a real APNs request.
func newAPNsTestServer(t *testing.T) *Server {
	t.Helper()
	conn, err := db.Open(filepath.Join(t.TempDir(), "push_test.db"))
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })
	if err := db.Bootstrap(context.Background(), conn); err != nil {
		t.Fatalf("bootstrap: %v", err)
	}
	q := db.New(conn)
	apnsSender := apns.NewSender(q, apnsTestKeyPEM(t), "TESTKID", "TESTTEAM", "com.example.cdrop", "sandbox")
	if !apnsSender.Enabled() {
		t.Fatal("apns sender must be enabled for this test")
	}
	return &Server{
		queries: q,
		hub:     hub.New(q),
		apns:    apnsSender,
	}
}

// TestHandleAPNsRegister_DisabledReturns503 verifies that the register endpoint
// returns 503 when APNs is not configured (inert Sender).
func TestHandleAPNsRegister_DisabledReturns503(t *testing.T) {
	conn, err := db.Open(filepath.Join(t.TempDir(), "push_test2.db"))
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	t.Cleanup(func() { _ = conn.Close() })
	if err := db.Bootstrap(context.Background(), conn); err != nil {
		t.Fatalf("bootstrap: %v", err)
	}
	q := db.New(conn)
	s := &Server{
		queries: q,
		hub:     hub.New(q),
		apns:    apns.NewSender(nil, nil, "", "", "", ""), // inert
	}

	body := `{"device_token":"aabbccdd1234"}`
	r := httptest.NewRequest(http.MethodPost, "/api/push/apns/register", strings.NewReader(body))
	r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "u", Scope: "full"}))
	r = r.WithContext(jwtauth.ContextWithDeviceName(r.Context(), "iPhone"))
	w := httptest.NewRecorder()
	s.handleAPNsRegister(w, r)
	if w.Code != http.StatusServiceUnavailable {
		t.Errorf("disabled: got %d, want 503", w.Code)
	}
}

// TestHandleAPNsRegister_StoresRowIdempotently verifies that POST /api/push/apns/register
// stores the subscription row and that a second call with the same token upserts
// in place (idempotent: still exactly one row).
func TestHandleAPNsRegister_StoresRowIdempotently(t *testing.T) {
	s := newAPNsTestServer(t)

	const deviceToken = "aabbccdd1234deadbeef"
	const locale = "en-US"

	callRegister := func() int {
		body := `{"device_token":"` + deviceToken + `","locale":"` + locale + `"}`
		r := httptest.NewRequest(http.MethodPost, "/api/push/apns/register", strings.NewReader(body))
		r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "u", Scope: "full"}))
		r = r.WithContext(jwtauth.ContextWithDeviceName(r.Context(), "iPhone"))
		w := httptest.NewRecorder()
		s.handleAPNsRegister(w, r)
		return w.Code
	}

	// First registration.
	if code := callRegister(); code != http.StatusNoContent {
		t.Fatalf("first register: got %d, want 204", code)
	}

	// Verify the row exists.
	subs, err := s.queries.ListAPNsSubscriptionsByUserDevice(context.Background(), db.ListAPNsSubscriptionsByUserDeviceParams{
		UserID:     "u",
		DeviceName: "iPhone",
	})
	if err != nil {
		t.Fatalf("list subs: %v", err)
	}
	if len(subs) != 1 {
		t.Fatalf("after first register: got %d rows, want 1", len(subs))
	}
	sub := subs[0]
	if sub.Endpoint != deviceToken {
		t.Errorf("endpoint = %q, want %q", sub.Endpoint, deviceToken)
	}
	if sub.Locale != locale {
		t.Errorf("locale = %q, want %q", sub.Locale, locale)
	}
	if sub.Platform != "apns" {
		t.Errorf("platform = %q, want apns", sub.Platform)
	}
	if sub.UserID != "u" {
		t.Errorf("user_id = %q, want u", sub.UserID)
	}
	expectedID := push.SubscriptionID(deviceToken)
	if sub.ID != expectedID {
		t.Errorf("id = %q, want %q (SHA-256 of token)", sub.ID, expectedID)
	}

	// Second registration with same token: must upsert in place.
	if code := callRegister(); code != http.StatusNoContent {
		t.Fatalf("second register: got %d, want 204", code)
	}
	subs2, err := s.queries.ListAPNsSubscriptionsByUserDevice(context.Background(), db.ListAPNsSubscriptionsByUserDeviceParams{
		UserID:     "u",
		DeviceName: "iPhone",
	})
	if err != nil {
		t.Fatalf("list subs after second register: %v", err)
	}
	if len(subs2) != 1 {
		t.Errorf("after second register: got %d rows, want 1 (should upsert not duplicate)", len(subs2))
	}
}

// TestHandleAPNsRegister_MissingToken verifies that a missing device_token
// returns 400.
func TestHandleAPNsRegister_MissingToken(t *testing.T) {
	s := newAPNsTestServer(t)

	r := httptest.NewRequest(http.MethodPost, "/api/push/apns/register", strings.NewReader(`{}`))
	r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "u", Scope: "full"}))
	r = r.WithContext(jwtauth.ContextWithDeviceName(r.Context(), "iPhone"))
	w := httptest.NewRecorder()
	s.handleAPNsRegister(w, r)
	if w.Code != http.StatusBadRequest {
		t.Errorf("missing token: got %d, want 400", w.Code)
	}
	var resp map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err == nil {
		if resp["error"] != "missing device_token" {
			t.Errorf("error msg = %q", resp["error"])
		}
	}
}

// TestHandleAPNsRegister_UnknownLocaleFallsBack verifies that an unrecognized
// locale falls back to the server default (zh-CN).
func TestHandleAPNsRegister_UnknownLocaleFallsBack(t *testing.T) {
	s := newAPNsTestServer(t)

	body := `{"device_token":"tok123456","locale":"fr-FR"}`
	r := httptest.NewRequest(http.MethodPost, "/api/push/apns/register", strings.NewReader(body))
	r = r.WithContext(jwtauth.ContextWithClaims(r.Context(), &jwtauth.Claims{UserID: "u", Scope: "full"}))
	r = r.WithContext(jwtauth.ContextWithDeviceName(r.Context(), "iPhone"))
	w := httptest.NewRecorder()
	s.handleAPNsRegister(w, r)
	if w.Code != http.StatusNoContent {
		t.Fatalf("register: got %d, want 204", w.Code)
	}

	subs, err := s.queries.ListAPNsSubscriptionsByUserDevice(context.Background(), db.ListAPNsSubscriptionsByUserDeviceParams{
		UserID:     "u",
		DeviceName: "iPhone",
	})
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(subs) != 1 {
		t.Fatalf("want 1 row, got %d", len(subs))
	}
	if subs[0].Locale != "zh-CN" {
		t.Errorf("locale = %q, want zh-CN (fallback)", subs[0].Locale)
	}
}
