package internal_test

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

	"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
	"mgit.msbls.de/m/KanzlAI-mGMT/internal/config"
	"mgit.msbls.de/m/KanzlAI-mGMT/internal/db"
	"mgit.msbls.de/m/KanzlAI-mGMT/internal/router"
	"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
)

const testJWTSecret = "test-jwt-secret-for-integration-tests"

// testServer sets up the full router with a real DB connection.
// Requires DATABASE_URL env var.
func testServer(t *testing.T) (http.Handler, func()) {
	t.Helper()

	dbURL := os.Getenv("DATABASE_URL")
	if dbURL == "" {
		t.Skip("DATABASE_URL not set, skipping integration test")
	}
	jwtSecret := os.Getenv("SUPABASE_JWT_SECRET")
	if jwtSecret == "" {
		jwtSecret = testJWTSecret
		os.Setenv("SUPABASE_JWT_SECRET", jwtSecret)
	}

	database, err := db.Connect(dbURL)
	if err != nil {
		t.Fatalf("connecting to database: %v", err)
	}

	cfg := &config.Config{
		Port:              "0",
		DatabaseURL:       dbURL,
		SupabaseJWTSecret: jwtSecret,
	}

	authMW := auth.NewMiddleware(jwtSecret, database)
	notifSvc := services.NewNotificationService(database)
	handler := router.New(database, authMW, cfg, nil, notifSvc)

	return handler, func() {
		notifSvc.Stop()
		database.Close()
	}
}

// createTestJWT creates a JWT for the given user ID, signed with the test secret.
func createTestJWT(t *testing.T, userID uuid.UUID) string {
	t.Helper()
	secret := os.Getenv("SUPABASE_JWT_SECRET")
	if secret == "" {
		secret = testJWTSecret
	}

	claims := jwt.MapClaims{
		"sub": userID.String(),
		"aud": "authenticated",
		"exp": time.Now().Add(1 * time.Hour).Unix(),
		"iat": time.Now().Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	signed, err := token.SignedString([]byte(secret))
	if err != nil {
		t.Fatalf("signing JWT: %v", err)
	}
	return signed
}

// createExpiredJWT creates an expired JWT for testing auth rejection.
func createExpiredJWT(t *testing.T, userID uuid.UUID) string {
	t.Helper()
	secret := os.Getenv("SUPABASE_JWT_SECRET")
	if secret == "" {
		secret = testJWTSecret
	}

	claims := jwt.MapClaims{
		"sub": userID.String(),
		"aud": "authenticated",
		"exp": time.Now().Add(-1 * time.Hour).Unix(),
		"iat": time.Now().Add(-2 * time.Hour).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	signed, err := token.SignedString([]byte(secret))
	if err != nil {
		t.Fatalf("signing expired JWT: %v", err)
	}
	return signed
}

// setupTestTenant creates a temporary tenant and user_tenant for testing.
// Returns tenantID and userID. Cleans up on test completion.
func setupTestTenant(t *testing.T, _ http.Handler) (tenantID, userID uuid.UUID) {
	t.Helper()

	dbURL := os.Getenv("DATABASE_URL")
	database, err := db.Connect(dbURL)
	if err != nil {
		t.Fatalf("connecting to database for setup: %v", err)
	}
	defer database.Close()

	tenantID = uuid.New()
	userID = uuid.New()

	_, err = database.Exec(`INSERT INTO tenants (id, name, slug) VALUES ($1, $2, $3)`,
		tenantID, "Integration Test Kanzlei "+tenantID.String()[:8], "test-"+tenantID.String()[:8])
	if err != nil {
		t.Fatalf("creating test tenant: %v", err)
	}

	// Create a user in auth.users so JWT auth works
	_, err = database.Exec(`INSERT INTO auth.users (id, instance_id, role, aud, email, encrypted_password, email_confirmed_at, created_at, updated_at, confirmation_token, recovery_token)
		VALUES ($1, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', $2, '', NOW(), NOW(), NOW(), '', '')`,
		userID, fmt.Sprintf("test-%s@kanzlai.test", userID.String()[:8]))
	if err != nil {
		t.Fatalf("creating test user: %v", err)
	}

	_, err = database.Exec(`INSERT INTO user_tenants (user_id, tenant_id, role) VALUES ($1, $2, 'owner')`,
		userID, tenantID)
	if err != nil {
		t.Fatalf("creating user_tenant: %v", err)
	}

	t.Cleanup(func() {
		cleanupDB, _ := db.Connect(dbURL)
		if cleanupDB != nil {
			cleanupDB.Exec(`DELETE FROM notification_preferences WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM notifications WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM notes WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM time_entries WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM invoices WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM billing_rates WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM case_assignments WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM audit_log WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM case_events WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM deadlines WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM appointments WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM parties WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM document_templates WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM documents WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM cases WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM user_tenants WHERE tenant_id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM tenants WHERE id = $1`, tenantID)
			cleanupDB.Exec(`DELETE FROM auth.users WHERE id = $1`, userID)
			cleanupDB.Close()
		}
	})

	return tenantID, userID
}

// doRequest is a test helper that makes an HTTP request to the test handler.
func doRequest(t *testing.T, handler http.Handler, method, path, token string, body any) *httptest.ResponseRecorder {
	t.Helper()

	var bodyReader *bytes.Buffer
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("marshaling request body: %v", err)
		}
		bodyReader = bytes.NewBuffer(b)
	} else {
		bodyReader = &bytes.Buffer{}
	}

	req := httptest.NewRequest(method, path, bodyReader)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)
	return w
}

// decodeJSON decodes the response body into the given target.
func decodeJSON(t *testing.T, w *httptest.ResponseRecorder, target any) {
	t.Helper()
	if err := json.Unmarshal(w.Body.Bytes(), target); err != nil {
		t.Fatalf("decoding JSON response: %v (body: %s)", err, w.Body.String())
	}
}

// createTestCase is a helper that creates a case and returns its ID.
func createTestCase(t *testing.T, handler http.Handler, token, caseNum, title string) string {
	t.Helper()
	body := map[string]string{
		"case_number": caseNum,
		"title":       title,
	}
	w := doRequest(t, handler, "POST", "/api/cases", token, body)
	if w.Code != http.StatusCreated {
		t.Fatalf("create case: expected 201, got %d: %s", w.Code, w.Body.String())
	}
	var resp map[string]any
	decodeJSON(t, w, &resp)
	id, _ := resp["id"].(string)
	if id == "" {
		t.Fatal("created case missing ID")
	}
	return id
}

// ── Health ──────────────────────────────────────────────────────────────────

func TestIntegration_Health(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	w := doRequest(t, handler, "GET", "/health", "", nil)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	var resp map[string]string
	decodeJSON(t, w, &resp)
	if resp["status"] != "ok" {
		t.Fatalf("expected status ok, got %v", resp)
	}
}

// ── Auth Middleware ──────────────────────────────────────────────────────────

func TestIntegration_Auth(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	t.Run("Unauthenticated", func(t *testing.T) {
		endpoints := []struct {
			method string
			path   string
		}{
			{"GET", "/api/cases"},
			{"POST", "/api/cases"},
			{"GET", "/api/tenants"},
			{"GET", "/api/deadlines"},
			{"GET", "/api/appointments"},
			{"GET", "/api/dashboard"},
			{"GET", "/api/notes"},
			{"GET", "/api/reports/cases"},
			{"GET", "/api/templates"},
			{"GET", "/api/time-entries"},
			{"GET", "/api/invoices"},
			{"GET", "/api/notifications"},
			{"GET", "/api/audit-log"},
		}

		for _, ep := range endpoints {
			t.Run(ep.method+" "+ep.path, func(t *testing.T) {
				w := doRequest(t, handler, ep.method, ep.path, "", nil)
				if w.Code != http.StatusUnauthorized {
					t.Errorf("expected 401, got %d: %s", w.Code, w.Body.String())
				}
			})
		}
	})

	t.Run("ExpiredJWT", func(t *testing.T) {
		token := createExpiredJWT(t, uuid.New())
		w := doRequest(t, handler, "GET", "/api/cases", token, nil)
		if w.Code != http.StatusUnauthorized {
			t.Errorf("expected 401 for expired token, got %d", w.Code)
		}
	})

	t.Run("InvalidJWT", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases", "totally-not-a-jwt", nil)
		if w.Code != http.StatusUnauthorized {
			t.Errorf("expected 401 for invalid token, got %d", w.Code)
		}
	})

	t.Run("WrongSecretJWT", func(t *testing.T) {
		claims := jwt.MapClaims{
			"sub": uuid.New().String(),
			"exp": time.Now().Add(1 * time.Hour).Unix(),
		}
		token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
		signed, _ := token.SignedString([]byte("wrong-secret-key"))

		w := doRequest(t, handler, "GET", "/api/cases", signed, nil)
		if w.Code != http.StatusUnauthorized {
			t.Errorf("expected 401 for wrong-secret token, got %d", w.Code)
		}
	})
}

// ── Current User (GET /api/me) ─────────────────────────────────────────────

func TestIntegration_Me(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	w := doRequest(t, handler, "GET", "/api/me", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	var resp map[string]any
	decodeJSON(t, w, &resp)
	if resp["role"] != "owner" {
		t.Errorf("expected role owner, got %v", resp["role"])
	}
	if _, ok := resp["permissions"]; !ok {
		t.Error("missing permissions in response")
	}
}

// ── Tenant CRUD ─────────────────────────────────────────────────────────────

func TestIntegration_TenantCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	tenantID, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	// List tenants
	t.Run("ListTenants", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/tenants", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var tenants []map[string]any
		decodeJSON(t, w, &tenants)
		if len(tenants) < 1 {
			t.Fatalf("expected at least 1 tenant, got %d", len(tenants))
		}
	})

	// Create a new tenant
	var newTenantID string
	t.Run("CreateTenant", func(t *testing.T) {
		body := map[string]string{"name": "Test Kanzlei Neu", "slug": "test-neu-" + uuid.New().String()[:8]}
		w := doRequest(t, handler, "POST", "/api/tenants", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		newTenantID, _ = resp["id"].(string)
		if newTenantID == "" {
			t.Fatal("created tenant missing ID")
		}

		t.Cleanup(func() {
			dbURL := os.Getenv("DATABASE_URL")
			cleanupDB, _ := db.Connect(dbURL)
			if cleanupDB != nil {
				cleanupDB.Exec(`DELETE FROM user_tenants WHERE tenant_id = $1`, newTenantID)
				cleanupDB.Exec(`DELETE FROM tenants WHERE id = $1`, newTenantID)
				cleanupDB.Close()
			}
		})
	})

	// Get tenant by ID (use the setup tenant we know the user is a member of)
	t.Run("GetTenant", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/tenants/"+tenantID.String(), token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["id"] != tenantID.String() {
			t.Errorf("unexpected tenant ID: %v", resp["id"])
		}
	})

	// Get tenant with invalid ID
	t.Run("GetTenant_InvalidID", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/tenants/not-a-uuid", token, nil)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d", w.Code)
		}
	})

	// Create tenant missing fields
	t.Run("CreateTenant_MissingFields", func(t *testing.T) {
		body := map[string]string{"name": "", "slug": ""}
		w := doRequest(t, handler, "POST", "/api/tenants", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
		}
	})
}

// ── Tenant Auto-Assign ─────────────────────────────────────────────────────

func TestIntegration_TenantAutoAssign(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	t.Run("AutoAssign_NoMatch", func(t *testing.T) {
		body := map[string]string{"email": "nobody@nonexistent-domain-xyz.test"}
		w := doRequest(t, handler, "POST", "/api/tenants/auto-assign", token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["assigned"] != false {
			t.Errorf("expected assigned=false, got %v", resp["assigned"])
		}
	})
}

// ── Case CRUD ───────────────────────────────────────────────────────────────

func TestIntegration_CaseCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	var caseID string

	t.Run("Create", func(t *testing.T) {
		body := map[string]string{
			"case_number": "TEST/CRUD/001",
			"title":       "CRUD Test — Patentverletzung",
			"case_type":   "patent",
			"court":       "UPC München",
		}
		w := doRequest(t, handler, "POST", "/api/cases", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		caseID, _ = resp["id"].(string)
		if caseID == "" {
			t.Fatal("created case missing ID")
		}
		if resp["status"] != "active" {
			t.Errorf("expected default status active, got %v", resp["status"])
		}
	})

	t.Run("Create_MissingFields", func(t *testing.T) {
		body := map[string]string{"title": ""}
		w := doRequest(t, handler, "POST", "/api/cases", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		total := resp["total"].(float64)
		if total < 1 {
			t.Fatalf("expected at least 1 case, got %.0f", total)
		}
	})

	t.Run("List_SearchFilter", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases?search=CRUD", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d", w.Code)
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		total := resp["total"].(float64)
		if total < 1 {
			t.Error("search filter should find our test case")
		}
	})

	t.Run("List_StatusFilter", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases?status=active", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d", w.Code)
		}
	})

	t.Run("Get", func(t *testing.T) {
		if caseID == "" {
			t.Skip("no case ID")
		}
		w := doRequest(t, handler, "GET", "/api/cases/"+caseID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["title"] != "CRUD Test — Patentverletzung" {
			t.Errorf("unexpected title: %v", resp["title"])
		}
		if _, ok := resp["parties"]; !ok {
			t.Error("missing parties in CaseDetail response")
		}
		if _, ok := resp["recent_events"]; !ok {
			t.Error("missing recent_events in CaseDetail response")
		}
	})

	t.Run("Get_InvalidID", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/not-a-uuid", token, nil)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d", w.Code)
		}
	})

	t.Run("Get_NotFound", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/"+uuid.New().String(), token, nil)
		if w.Code != http.StatusNotFound {
			t.Errorf("expected 404, got %d", w.Code)
		}
	})

	t.Run("Update", func(t *testing.T) {
		if caseID == "" {
			t.Skip("no case ID")
		}
		body := map[string]string{"title": "Updated Title", "court": "UPC Paris"}
		w := doRequest(t, handler, "PUT", "/api/cases/"+caseID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["title"] != "Updated Title" {
			t.Errorf("title not updated: %v", resp["title"])
		}
	})

	t.Run("Delete", func(t *testing.T) {
		if caseID == "" {
			t.Skip("no case ID")
		}
		w := doRequest(t, handler, "DELETE", "/api/cases/"+caseID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]string
		decodeJSON(t, w, &resp)
		if resp["status"] != "archived" {
			t.Errorf("expected archived status, got %s", resp["status"])
		}
	})
}

// ── Party CRUD ──────────────────────────────────────────────────────────────

func TestIntegration_PartyCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/PARTY/001", "Party Test Case")

	var partyID string

	t.Run("Create", func(t *testing.T) {
		body := map[string]any{
			"name":           "Acme Corp",
			"role":           "claimant",
			"representative": "Dr. Schmidt",
		}
		w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/parties", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		partyID, _ = resp["id"].(string)
		if partyID == "" {
			t.Fatal("created party missing ID")
		}
	})

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/parties", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		parties, ok := resp["parties"].([]any)
		if !ok {
			t.Fatal("expected parties array in response")
		}
		if len(parties) < 1 {
			t.Fatal("expected at least 1 party")
		}
	})

	t.Run("Update", func(t *testing.T) {
		if partyID == "" {
			t.Skip("no party ID")
		}
		body := map[string]any{
			"name":           "Acme Corp GmbH",
			"representative": "RA Dr. Müller",
		}
		w := doRequest(t, handler, "PUT", "/api/parties/"+partyID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["name"] != "Acme Corp GmbH" {
			t.Errorf("name not updated: %v", resp["name"])
		}
	})

	t.Run("Delete", func(t *testing.T) {
		if partyID == "" {
			t.Skip("no party ID")
		}
		w := doRequest(t, handler, "DELETE", "/api/parties/"+partyID, token, nil)
		if w.Code != http.StatusNoContent {
			t.Errorf("expected 204, got %d: %s", w.Code, w.Body.String())
		}
	})
}

// ── Deadline CRUD ───────────────────────────────────────────────────────────

func TestIntegration_DeadlineCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/DL/001", "Deadline Test Case")

	var deadlineID string
	dueDate := time.Now().AddDate(0, 0, 14).Format("2006-01-02")
	warnDate := time.Now().AddDate(0, 0, 10).Format("2006-01-02")

	t.Run("Create", func(t *testing.T) {
		body := map[string]string{
			"title":        "Klageerwiderung einreichen",
			"due_date":     dueDate,
			"warning_date": warnDate,
			"source":       "manual",
		}
		w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		deadlineID, _ = resp["id"].(string)
		if deadlineID == "" {
			t.Fatal("created deadline missing ID")
		}
		if resp["status"] != "pending" {
			t.Errorf("expected status pending, got %v", resp["status"])
		}
	})

	t.Run("Create_MissingFields", func(t *testing.T) {
		body := map[string]string{"title": ""}
		w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("ListAll", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/deadlines", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("ListForCase", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/deadlines", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var deadlines []any
		decodeJSON(t, w, &deadlines)
		if len(deadlines) < 1 {
			t.Fatal("expected at least 1 deadline")
		}
	})

	t.Run("Get", func(t *testing.T) {
		if deadlineID == "" {
			t.Skip("no deadline ID")
		}
		w := doRequest(t, handler, "GET", "/api/deadlines/"+deadlineID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["title"] != "Klageerwiderung einreichen" {
			t.Errorf("unexpected title: %v", resp["title"])
		}
	})

	t.Run("Update", func(t *testing.T) {
		if deadlineID == "" {
			t.Skip("no deadline ID")
		}
		newTitle := "Aktualisierte Frist"
		body := map[string]*string{"title": &newTitle}
		w := doRequest(t, handler, "PUT", "/api/deadlines/"+deadlineID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["title"] != newTitle {
			t.Errorf("expected title %q, got %v", newTitle, resp["title"])
		}
	})

	t.Run("Complete", func(t *testing.T) {
		if deadlineID == "" {
			t.Skip("no deadline ID")
		}
		w := doRequest(t, handler, "PATCH", "/api/deadlines/"+deadlineID+"/complete", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["status"] != "completed" {
			t.Errorf("expected status completed, got %v", resp["status"])
		}
		if resp["completed_at"] == nil {
			t.Error("expected completed_at to be set")
		}
	})

	t.Run("Delete", func(t *testing.T) {
		if deadlineID == "" {
			t.Skip("no deadline ID")
		}
		w := doRequest(t, handler, "DELETE", "/api/deadlines/"+deadlineID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("Delete_NotFound", func(t *testing.T) {
		w := doRequest(t, handler, "DELETE", "/api/deadlines/"+uuid.New().String(), token, nil)
		if w.Code != http.StatusNotFound {
			t.Errorf("expected 404, got %d", w.Code)
		}
	})
}

// ── Appointment CRUD ────────────────────────────────────────────────────────

func TestIntegration_AppointmentCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	startAt := time.Now().Add(48 * time.Hour).UTC().Truncate(time.Second)
	endAt := startAt.Add(2 * time.Hour)

	var appointmentID string

	t.Run("Create", func(t *testing.T) {
		body := map[string]any{
			"title":            "Mündliche Verhandlung",
			"start_at":         startAt.Format(time.RFC3339),
			"end_at":           endAt.Format(time.RFC3339),
			"location":         "UPC München, Saal 3",
			"appointment_type": "hearing",
		}
		w := doRequest(t, handler, "POST", "/api/appointments", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		appointmentID, _ = resp["id"].(string)
		if appointmentID == "" {
			t.Fatal("created appointment missing ID")
		}
	})

	t.Run("Create_MissingTitle", func(t *testing.T) {
		body := map[string]any{"start_at": startAt.Format(time.RFC3339)}
		w := doRequest(t, handler, "POST", "/api/appointments", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d", w.Code)
		}
	})

	t.Run("Create_MissingStartAt", func(t *testing.T) {
		body := map[string]any{"title": "Test"}
		w := doRequest(t, handler, "POST", "/api/appointments", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d", w.Code)
		}
	})

	t.Run("Get", func(t *testing.T) {
		if appointmentID == "" {
			t.Skip("no appointment ID")
		}
		w := doRequest(t, handler, "GET", "/api/appointments/"+appointmentID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["title"] != "Mündliche Verhandlung" {
			t.Errorf("unexpected title: %v", resp["title"])
		}
	})

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/appointments", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var appointments []any
		decodeJSON(t, w, &appointments)
		if len(appointments) < 1 {
			t.Fatal("expected at least 1 appointment")
		}
	})

	t.Run("Update", func(t *testing.T) {
		if appointmentID == "" {
			t.Skip("no appointment ID")
		}
		newStart := startAt.Add(24 * time.Hour)
		body := map[string]any{
			"title":    "Verlegter Termin",
			"start_at": newStart.Format(time.RFC3339),
			"location": "UPC Paris, Saal 1",
		}
		w := doRequest(t, handler, "PUT", "/api/appointments/"+appointmentID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["title"] != "Verlegter Termin" {
			t.Errorf("title not updated: %v", resp["title"])
		}
	})

	t.Run("Delete", func(t *testing.T) {
		if appointmentID == "" {
			t.Skip("no appointment ID")
		}
		w := doRequest(t, handler, "DELETE", "/api/appointments/"+appointmentID, token, nil)
		if w.Code != http.StatusNoContent {
			t.Errorf("expected 204, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("Delete_NotFound", func(t *testing.T) {
		w := doRequest(t, handler, "DELETE", "/api/appointments/"+uuid.New().String(), token, nil)
		if w.Code != http.StatusNotFound {
			t.Errorf("expected 404, got %d", w.Code)
		}
	})
}

// ── Note CRUD ───────────────────────────────────────────────────────────────

func TestIntegration_NoteCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/NOTE/001", "Note Test Case")

	var noteID string

	t.Run("Create", func(t *testing.T) {
		body := map[string]any{
			"case_id": caseID,
			"content": "Mandant hat neue Unterlagen übermittelt.",
		}
		w := doRequest(t, handler, "POST", "/api/notes", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		noteID, _ = resp["id"].(string)
		if noteID == "" {
			t.Fatal("created note missing ID")
		}
		if resp["content"] != "Mandant hat neue Unterlagen übermittelt." {
			t.Errorf("unexpected content: %v", resp["content"])
		}
	})

	t.Run("Create_MissingContent", func(t *testing.T) {
		body := map[string]any{"case_id": caseID, "content": ""}
		w := doRequest(t, handler, "POST", "/api/notes", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/notes?case_id="+caseID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var notes []any
		decodeJSON(t, w, &notes)
		if len(notes) < 1 {
			t.Fatal("expected at least 1 note")
		}
	})

	t.Run("Update", func(t *testing.T) {
		if noteID == "" {
			t.Skip("no note ID")
		}
		body := map[string]any{"content": "Aktualisierte Notiz mit neuen Details."}
		w := doRequest(t, handler, "PUT", "/api/notes/"+noteID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["content"] != "Aktualisierte Notiz mit neuen Details." {
			t.Errorf("content not updated: %v", resp["content"])
		}
	})

	t.Run("Delete", func(t *testing.T) {
		if noteID == "" {
			t.Skip("no note ID")
		}
		w := doRequest(t, handler, "DELETE", "/api/notes/"+noteID, token, nil)
		if w.Code != http.StatusNoContent {
			t.Errorf("expected 204, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("Delete_NotFound", func(t *testing.T) {
		w := doRequest(t, handler, "DELETE", "/api/notes/"+uuid.New().String(), token, nil)
		if w.Code != http.StatusNotFound {
			t.Errorf("expected 404, got %d", w.Code)
		}
	})
}

// ── Dashboard ───────────────────────────────────────────────────────────────

func TestIntegration_Dashboard(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/DASH/001", "Dashboard Test Case")

	// Add a deadline so dashboard has data
	dueDate := time.Now().AddDate(0, 0, 3).Format("2006-01-02")
	dlBody := map[string]string{"title": "Dashboard Test Deadline", "due_date": dueDate}
	w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines", token, dlBody)
	if w.Code != http.StatusCreated {
		t.Fatalf("create deadline: %d: %s", w.Code, w.Body.String())
	}

	t.Run("Get", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/dashboard", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}

		var dashboard map[string]any
		decodeJSON(t, w, &dashboard)

		requiredKeys := []string{"deadline_summary", "case_summary", "upcoming_deadlines", "upcoming_appointments", "recent_activity"}
		for _, key := range requiredKeys {
			if _, exists := dashboard[key]; !exists {
				t.Errorf("missing key %q in dashboard", key)
			}
		}

		if cs, ok := dashboard["case_summary"].(map[string]any); ok {
			if active, ok := cs["active_count"].(float64); ok && active < 1 {
				t.Errorf("expected at least 1 active case, got %.0f", active)
			}
		}

		if ds, ok := dashboard["deadline_summary"].(map[string]any); ok {
			for _, key := range []string{"overdue_count", "due_this_week", "due_next_week", "ok_count"} {
				if _, exists := ds[key]; !exists {
					t.Errorf("missing deadline_summary.%s", key)
				}
			}
		}
	})
}

// ── Proceeding Types & Deadline Rules ───────────────────────────────────────

func TestIntegration_ProceedingTypesAndRules(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	t.Run("ListProceedingTypes", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/proceeding-types", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var types []any
		decodeJSON(t, w, &types)
		if len(types) == 0 {
			t.Error("expected at least 1 proceeding type")
		}
	})

	t.Run("ListDeadlineRules", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/deadline-rules", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("GetRuleTree_INF", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/deadline-rules/INF", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var tree []any
		decodeJSON(t, w, &tree)
		if len(tree) == 0 {
			t.Error("expected non-empty rule tree for INF")
		}
	})
}

// ── Deadline Calculator ─────────────────────────────────────────────────────

func TestIntegration_DeadlineCalculator(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	t.Run("Calculate_ValidRequest", func(t *testing.T) {
		body := map[string]any{
			"proceeding_type":    "INF",
			"trigger_event_date": "2026-06-01",
		}
		w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["proceeding_type"] != "INF" {
			t.Errorf("expected INF, got %v", resp["proceeding_type"])
		}
		deadlines, ok := resp["deadlines"].([]any)
		if !ok {
			t.Fatal("deadlines not an array")
		}
		if len(deadlines) == 0 {
			t.Error("expected at least 1 calculated deadline")
		}
		for i, dl := range deadlines {
			m, ok := dl.(map[string]any)
			if !ok {
				continue
			}
			for _, key := range []string{"rule_code", "title", "due_date"} {
				if _, exists := m[key]; !exists {
					t.Errorf("deadline[%d] missing field %q", i, key)
				}
			}
		}
	})

	t.Run("Calculate_MissingFields", func(t *testing.T) {
		body := map[string]string{}
		w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d", w.Code)
		}
	})

	t.Run("Calculate_InvalidDateFormat", func(t *testing.T) {
		body := map[string]string{
			"proceeding_type":    "INF",
			"trigger_event_date": "01/06/2026",
		}
		w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400 for bad date format, got %d", w.Code)
		}
	})

	t.Run("Calculate_UnknownProceedingType", func(t *testing.T) {
		body := map[string]string{
			"proceeding_type":    "NONEXISTENT",
			"trigger_event_date": "2026-06-01",
		}
		w := doRequest(t, handler, "POST", "/api/deadlines/calculate", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400 for unknown type, got %d", w.Code)
		}
	})
}

// ── Deadline Determination ──────────────────────────────────────────────────

func TestIntegration_Determine(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	t.Run("GetTimeline_INF", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/proceeding-types/INF/timeline", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["proceeding_type"]; !ok {
			t.Error("missing proceeding_type in timeline response")
		}
		timeline, ok := resp["timeline"].([]any)
		if !ok {
			t.Fatal("timeline not an array")
		}
		if len(timeline) == 0 {
			t.Error("expected non-empty timeline for INF")
		}
	})

	t.Run("GetTimeline_Unknown", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/proceeding-types/NONEXISTENT/timeline", token, nil)
		if w.Code != http.StatusNotFound && w.Code != http.StatusBadRequest {
			t.Errorf("expected 404 or 400 for unknown type, got %d", w.Code)
		}
	})

	t.Run("Determine_ValidRequest", func(t *testing.T) {
		body := map[string]any{
			"proceeding_type":    "INF",
			"trigger_event_date": "2026-06-01",
			"conditions":         map[string]bool{},
		}
		w := doRequest(t, handler, "POST", "/api/deadlines/determine", token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["proceeding_type"] != "INF" {
			t.Errorf("expected INF, got %v", resp["proceeding_type"])
		}
		if _, ok := resp["timeline"]; !ok {
			t.Error("missing timeline in determine response")
		}
		if _, ok := resp["total_deadlines"]; !ok {
			t.Error("missing total_deadlines in determine response")
		}
	})

	t.Run("Determine_MissingFields", func(t *testing.T) {
		body := map[string]any{}
		w := doRequest(t, handler, "POST", "/api/deadlines/determine", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d", w.Code)
		}
	})

	t.Run("BatchCreate", func(t *testing.T) {
		caseID := createTestCase(t, handler, token, "TEST/BATCH/001", "Batch Deadline Test")
		dueDate := time.Now().AddDate(0, 1, 0).Format("2006-01-02")
		body := map[string]any{
			"deadlines": []map[string]any{
				{"title": "Batch Frist 1", "due_date": dueDate},
				{"title": "Batch Frist 2", "due_date": dueDate},
			},
		}
		w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines/batch", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		created, ok := resp["created"].(float64)
		if !ok || created < 2 {
			t.Errorf("expected at least 2 created, got %v", resp["created"])
		}
	})
}

// ── Reports ─────────────────────────────────────────────────────────────────

func TestIntegration_Reports(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	t.Run("CaseReport", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/reports/cases", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp any
		decodeJSON(t, w, &resp)
	})

	t.Run("DeadlineReport", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/reports/deadlines", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp any
		decodeJSON(t, w, &resp)
	})

	t.Run("WorkloadReport", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/reports/workload", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp any
		decodeJSON(t, w, &resp)
	})

	t.Run("BillingReport", func(t *testing.T) {
		// Owner role has PermManageBilling
		w := doRequest(t, handler, "GET", "/api/reports/billing", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp any
		decodeJSON(t, w, &resp)
	})

	t.Run("CaseReport_WithDateRange", func(t *testing.T) {
		from := time.Now().AddDate(-1, 0, 0).Format("2006-01-02")
		to := time.Now().Format("2006-01-02")
		w := doRequest(t, handler, "GET", fmt.Sprintf("/api/reports/cases?from=%s&to=%s", from, to), token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})
}

// ── Templates ───────────────────────────────────────────────────────────────

func TestIntegration_TemplateCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	var templateID string

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/templates", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["data"]; !ok {
			t.Error("missing data in templates response")
		}
		if _, ok := resp["total"]; !ok {
			t.Error("missing total in templates response")
		}
	})

	t.Run("Create", func(t *testing.T) {
		body := map[string]any{
			"name":     "Klageerwiderung Vorlage",
			"category": "schriftsatz",
			"content":  "Sehr geehrte Damen und Herren,\n\nim Namen und im Auftrag des {{party_name}}...",
		}
		w := doRequest(t, handler, "POST", "/api/templates", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		templateID, _ = resp["id"].(string)
		if templateID == "" {
			t.Fatal("created template missing ID")
		}
		if resp["name"] != "Klageerwiderung Vorlage" {
			t.Errorf("unexpected name: %v", resp["name"])
		}
	})

	t.Run("Get", func(t *testing.T) {
		if templateID == "" {
			t.Skip("no template ID")
		}
		w := doRequest(t, handler, "GET", "/api/templates/"+templateID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["name"] != "Klageerwiderung Vorlage" {
			t.Errorf("unexpected name: %v", resp["name"])
		}
	})

	t.Run("Update", func(t *testing.T) {
		if templateID == "" {
			t.Skip("no template ID")
		}
		body := map[string]any{
			"name":     "Aktualisierte Vorlage",
			"category": "intern",
		}
		w := doRequest(t, handler, "PUT", "/api/templates/"+templateID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["name"] != "Aktualisierte Vorlage" {
			t.Errorf("name not updated: %v", resp["name"])
		}
	})

	t.Run("Render", func(t *testing.T) {
		if templateID == "" {
			t.Skip("no template ID")
		}
		w := doRequest(t, handler, "POST", "/api/templates/"+templateID+"/render", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["content"]; !ok {
			t.Error("missing content in render response")
		}
	})

	t.Run("Delete", func(t *testing.T) {
		if templateID == "" {
			t.Skip("no template ID")
		}
		w := doRequest(t, handler, "DELETE", "/api/templates/"+templateID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})
}

// ── Time Entries ────────────────────────────────────────────────────────────

func TestIntegration_TimeEntryCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/TIME/001", "Time Entry Test Case")

	var entryID string

	t.Run("Create", func(t *testing.T) {
		body := map[string]any{
			"date":             time.Now().Format("2006-01-02"),
			"duration_minutes": 90,
			"description":      "Schriftsatz vorbereitet",
			"activity":         "drafting",
			"billable":         true,
		}
		w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/time-entries", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		entryID, _ = resp["id"].(string)
		if entryID == "" {
			t.Fatal("created time entry missing ID")
		}
	})

	t.Run("Create_MissingDescription", func(t *testing.T) {
		body := map[string]any{
			"date":             time.Now().Format("2006-01-02"),
			"duration_minutes": 60,
		}
		w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/time-entries", token, body)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("ListAll", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/time-entries", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["time_entries"]; !ok {
			t.Error("missing time_entries in response")
		}
	})

	t.Run("ListForCase", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/time-entries", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		entries, ok := resp["time_entries"].([]any)
		if !ok {
			t.Fatal("expected time_entries array")
		}
		if len(entries) < 1 {
			t.Fatal("expected at least 1 time entry")
		}
	})

	t.Run("Update", func(t *testing.T) {
		if entryID == "" {
			t.Skip("no entry ID")
		}
		body := map[string]any{
			"description":      "Schriftsatz überarbeitet",
			"duration_minutes": 120,
		}
		w := doRequest(t, handler, "PUT", "/api/time-entries/"+entryID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("Summary", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/time-entries/summary", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["summary"]; !ok {
			t.Error("missing summary in response")
		}
	})

	t.Run("Delete", func(t *testing.T) {
		if entryID == "" {
			t.Skip("no entry ID")
		}
		w := doRequest(t, handler, "DELETE", "/api/time-entries/"+entryID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})
}

// ── Invoices ────────────────────────────────────────────────────────────────

func TestIntegration_InvoiceCRUD(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/INV/001", "Invoice Test Case")

	var invoiceID string

	t.Run("List_Empty", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/invoices", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["invoices"]; !ok {
			t.Error("missing invoices in response")
		}
	})

	t.Run("Create", func(t *testing.T) {
		body := map[string]any{
			"case_id":     caseID,
			"client_name": "Acme Corp GmbH",
			"items": []map[string]any{
				{
					"description": "Beratung Patentrecht",
					"amount":      500.0,
				},
			},
			"tax_rate": 0.19,
		}
		w := doRequest(t, handler, "POST", "/api/invoices", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		invoiceID, _ = resp["id"].(string)
		if invoiceID == "" {
			t.Fatal("created invoice missing ID")
		}
		if resp["client_name"] != "Acme Corp GmbH" {
			t.Errorf("unexpected client_name: %v", resp["client_name"])
		}
	})

	t.Run("Get", func(t *testing.T) {
		if invoiceID == "" {
			t.Skip("no invoice ID")
		}
		w := doRequest(t, handler, "GET", "/api/invoices/"+invoiceID, token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("Update", func(t *testing.T) {
		if invoiceID == "" {
			t.Skip("no invoice ID")
		}
		body := map[string]any{
			"client_name": "Acme Corp GmbH (updated)",
			"notes":       "Zahlung bis 30.04.2026",
		}
		w := doRequest(t, handler, "PUT", "/api/invoices/"+invoiceID, token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("UpdateStatus", func(t *testing.T) {
		if invoiceID == "" {
			t.Skip("no invoice ID")
		}
		body := map[string]any{"status": "sent"}
		w := doRequest(t, handler, "PATCH", "/api/invoices/"+invoiceID+"/status", token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if resp["status"] != "sent" {
			t.Errorf("expected status sent, got %v", resp["status"])
		}
	})
}

// ── Billing Rates ───────────────────────────────────────────────────────────

func TestIntegration_BillingRates(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/billing-rates", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["billing_rates"]; !ok {
			t.Error("missing billing_rates in response")
		}
	})

	t.Run("Upsert", func(t *testing.T) {
		body := map[string]any{
			"rate":       350.0,
			"currency":   "EUR",
			"valid_from": "2026-01-01",
		}
		w := doRequest(t, handler, "PUT", "/api/billing-rates", token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if rate, ok := resp["rate"].(float64); !ok || rate != 350.0 {
			t.Errorf("expected rate 350, got %v", resp["rate"])
		}
	})
}

// ── Notifications ───────────────────────────────────────────────────────────

func TestIntegration_Notifications(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/notifications", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["data"]; !ok {
			t.Error("missing data in notifications response")
		}
		if _, ok := resp["total"]; !ok {
			t.Error("missing total in notifications response")
		}
	})

	t.Run("UnreadCount", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/notifications/unread-count", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["unread_count"]; !ok {
			t.Error("missing unread_count in response")
		}
	})

	t.Run("MarkAllRead", func(t *testing.T) {
		w := doRequest(t, handler, "PATCH", "/api/notifications/read-all", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("GetPreferences", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/notification-preferences", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		// Should have preference fields
		for _, key := range []string{"email_enabled", "daily_digest"} {
			if _, ok := resp[key]; !ok {
				t.Errorf("missing %s in notification preferences", key)
			}
		}
	})

	t.Run("UpdatePreferences", func(t *testing.T) {
		body := map[string]any{
			"deadline_reminder_days": []int{1, 3, 7},
			"email_enabled":         true,
			"daily_digest":          false,
		}
		w := doRequest(t, handler, "PUT", "/api/notification-preferences", token, body)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})
}

// ── Audit Log ───────────────────────────────────────────────────────────────

func TestIntegration_AuditLog(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	// Create a case first to generate audit entries
	createTestCase(t, handler, token, "TEST/AUDIT/001", "Audit Log Test Case")

	t.Run("List", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/audit-log", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["entries"]; !ok {
			t.Error("missing entries in audit log response")
		}
		if _, ok := resp["total"]; !ok {
			t.Error("missing total in audit log response")
		}
	})

	t.Run("List_WithFilters", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/audit-log?entity_type=case&limit=10", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		if _, ok := resp["entries"]; !ok {
			t.Error("missing entries in filtered audit log response")
		}
		// Note: audit entries may not exist in test environment due to httptest's
		// RemoteAddr (192.0.2.1:1234) failing inet column validation
	})
}

// ── Case Assignments ────────────────────────────────────────────────────────

func TestIntegration_CaseAssignments(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/ASSIGN/001", "Assignment Test Case")

	t.Run("List_Empty", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/assignments", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("Assign", func(t *testing.T) {
		body := map[string]any{
			"user_id": userID.String(),
			"role":    "lead",
		}
		w := doRequest(t, handler, "POST", "/api/cases/"+caseID+"/assignments", token, body)
		if w.Code != http.StatusCreated {
			t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("List_AfterAssign", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/assignments", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})

	t.Run("Unassign", func(t *testing.T) {
		w := doRequest(t, handler, "DELETE", "/api/cases/"+caseID+"/assignments/"+userID.String(), token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
	})
}

// ── Documents ───────────────────────────────────────────────────────────────

func TestIntegration_Documents(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)
	caseID := createTestCase(t, handler, token, "TEST/DOC/001", "Document Test Case")

	t.Run("ListDocuments_Empty", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/cases/"+caseID+"/documents", token, nil)
		if w.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
		}
		var resp map[string]any
		decodeJSON(t, w, &resp)
		total := resp["total"].(float64)
		if total != 0 {
			t.Errorf("expected 0 documents, got %.0f", total)
		}
	})

	t.Run("GetMeta_NotFound", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/documents/"+uuid.New().String()+"/meta", token, nil)
		if w.Code != http.StatusNotFound {
			t.Errorf("expected 404, got %d", w.Code)
		}
	})

	t.Run("GetMeta_InvalidID", func(t *testing.T) {
		w := doRequest(t, handler, "GET", "/api/documents/not-a-uuid/meta", token, nil)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400, got %d", w.Code)
		}
	})
}

// ── AI Endpoints ────────────────────────────────────────────────────────────

func TestIntegration_AI_NoAPIKey(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	body := map[string]string{"text": "Die Frist beträgt 3 Monate ab Zustellung."}
	w := doRequest(t, handler, "POST", "/api/ai/extract-deadlines", token, body)

	// Should be 404 (route not registered) since we don't set ANTHROPIC_API_KEY
	// or 200 if the key happens to be set in the env
	if w.Code == http.StatusNotFound {
		t.Log("AI endpoint correctly not available without API key")
	} else if w.Code == http.StatusOK {
		t.Log("AI endpoint available (API key configured in env)")
	} else {
		t.Logf("AI endpoint returned %d (may need API key)", w.Code)
	}
}

// ── Critical Path E2E ───────────────────────────────────────────────────────

func TestIntegration_CriticalPath(t *testing.T) {
	handler, cleanup := testServer(t)
	defer cleanup()

	_, userID := setupTestTenant(t, handler)
	token := createTestJWT(t, userID)

	// Step 1: Create a case
	caseBody := `{"case_number":"TEST/001","title":"Integration Test — Patentverletzung","case_type":"patent","court":"UPC München"}`
	req := httptest.NewRequest("POST", "/api/cases", bytes.NewBufferString(caseBody))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)

	if w.Code != http.StatusCreated {
		t.Fatalf("create case: expected 201, got %d: %s", w.Code, w.Body.String())
	}

	var createdCase map[string]any
	json.Unmarshal(w.Body.Bytes(), &createdCase)
	caseID, ok := createdCase["id"].(string)
	if !ok || caseID == "" {
		t.Fatalf("create case: missing ID in response: %v", createdCase)
	}

	// Step 2: List cases — verify our case is there
	w = doRequest(t, handler, "GET", "/api/cases", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list cases: expected 200, got %d: %s", w.Code, w.Body.String())
	}

	var caseList map[string]any
	decodeJSON(t, w, &caseList)
	total := caseList["total"].(float64)
	if total < 1 {
		t.Fatalf("list cases: expected at least 1 case, got %.0f", total)
	}

	// Step 3: Add a deadline to the case
	dueDate := time.Now().AddDate(0, 0, 14).Format("2006-01-02")
	warnDate := time.Now().AddDate(0, 0, 10).Format("2006-01-02")
	dlBody := map[string]any{
		"title":        "Klageerwiderung einreichen",
		"due_date":     dueDate,
		"warning_date": warnDate,
		"source":       "manual",
	}
	w = doRequest(t, handler, "POST", "/api/cases/"+caseID+"/deadlines", token, dlBody)
	if w.Code != http.StatusCreated {
		t.Fatalf("create deadline: expected 201, got %d: %s", w.Code, w.Body.String())
	}

	var createdDeadline map[string]any
	decodeJSON(t, w, &createdDeadline)
	deadlineID, ok := createdDeadline["id"].(string)
	if !ok || deadlineID == "" {
		t.Fatalf("create deadline: missing ID in response: %v", createdDeadline)
	}

	// Step 4: Create an appointment linked to the case
	startAt := time.Now().Add(72 * time.Hour).UTC().Truncate(time.Second)
	apptBody := map[string]any{
		"title":            "Mündliche Verhandlung",
		"start_at":         startAt.Format(time.RFC3339),
		"location":         "UPC München",
		"appointment_type": "hearing",
		"case_id":          caseID,
	}
	w = doRequest(t, handler, "POST", "/api/appointments", token, apptBody)
	if w.Code != http.StatusCreated {
		t.Fatalf("create appointment: expected 201, got %d: %s", w.Code, w.Body.String())
	}

	// Step 5: Add a note to the case
	noteBody := map[string]any{
		"case_id": caseID,
		"content": "Mandant hat Klage erhalten, Fristberechnung durchgeführt.",
	}
	w = doRequest(t, handler, "POST", "/api/notes", token, noteBody)
	if w.Code != http.StatusCreated {
		t.Fatalf("create note: expected 201, got %d: %s", w.Code, w.Body.String())
	}

	// Step 6: Add a time entry
	timeBody := map[string]any{
		"date":             time.Now().Format("2006-01-02"),
		"duration_minutes": 45,
		"description":      "Erstberatung zum Fall",
		"billable":         true,
	}
	w = doRequest(t, handler, "POST", "/api/cases/"+caseID+"/time-entries", token, timeBody)
	if w.Code != http.StatusCreated {
		t.Fatalf("create time entry: expected 201, got %d: %s", w.Code, w.Body.String())
	}

	// Step 7: Verify deadline appears in case deadlines
	w = doRequest(t, handler, "GET", "/api/cases/"+caseID+"/deadlines", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("list deadlines: expected 200, got %d: %s", w.Code, w.Body.String())
	}

	var deadlines []any
	decodeJSON(t, w, &deadlines)
	if len(deadlines) < 1 {
		t.Fatalf("list deadlines: expected at least 1, got %d", len(deadlines))
	}

	// Step 8: Fetch dashboard
	w = doRequest(t, handler, "GET", "/api/dashboard", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("dashboard: expected 200, got %d: %s", w.Code, w.Body.String())
	}

	var dashboard map[string]any
	decodeJSON(t, w, &dashboard)
	for _, key := range []string{"deadline_summary", "case_summary", "upcoming_deadlines"} {
		if _, exists := dashboard[key]; !exists {
			t.Errorf("dashboard: missing key %q in response", key)
		}
	}

	// Step 9: Complete the deadline
	w = doRequest(t, handler, "PATCH", "/api/deadlines/"+deadlineID+"/complete", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("complete deadline: expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var completedDL map[string]any
	decodeJSON(t, w, &completedDL)
	if completedDL["status"] != "completed" {
		t.Errorf("expected completed, got %v", completedDL["status"])
	}

	// Step 10: Verify audit log endpoint is accessible
	w = doRequest(t, handler, "GET", "/api/audit-log", token, nil)
	if w.Code != http.StatusOK {
		t.Fatalf("audit log: expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var auditResp map[string]any
	decodeJSON(t, w, &auditResp)
	if _, ok := auditResp["entries"]; !ok {
		t.Error("missing entries in audit log response")
	}
}
