package services

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"

	"mgit.msbls.de/m/paliad/internal/db"
)

// TestAuditService_UnionFilterCursor exercises the live SQL union against a
// real Postgres so we catch syntax errors in the WITH/UNION query that Go's
// type checker can't see. Skipped when TEST_DATABASE_URL is unset, mirroring
// TestDeadlineReopen_AdminAndNonAdmin.
//
// What it validates:
//   - all three sources are reachable in one round-trip and the discriminator
//     comes back correct
//   - source filter narrows to a single source
//   - keyset cursor (ts, id) returns strictly older rows so a follow-up page
//     never duplicates the seam row
//   - search ILIKE filter survives a "%" character in the input (escape works)
func TestAuditService_UnionFilterCursor(t *testing.T) {
	url := os.Getenv("TEST_DATABASE_URL")
	if url == "" {
		t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
	}
	if err := db.ApplyMigrations(url); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}
	pool, err := sqlx.Connect("postgres", url)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer pool.Close()

	ctx := context.Background()

	userID := uuid.New()
	projectID := uuid.New()

	cleanup := func() {
		pool.ExecContext(ctx, `DELETE FROM paliad.reminder_log    WHERE user_id = $1`, userID)
		pool.ExecContext(ctx, `DELETE FROM paliad.caldav_sync_log WHERE user_id = $1`, userID)
		pool.ExecContext(ctx, `DELETE FROM paliad.project_events  WHERE project_id = $1`, projectID)
		pool.ExecContext(ctx, `DELETE FROM paliad.projects        WHERE id = $1`, projectID)
		pool.ExecContext(ctx, `DELETE FROM paliad.users           WHERE id = $1`, userID)
		pool.ExecContext(ctx, `DELETE FROM auth.users             WHERE id = $1`, userID)
	}
	cleanup()
	defer cleanup()

	if _, err := pool.ExecContext(ctx,
		`INSERT INTO auth.users (id, email) VALUES ($1, 'audit-test@hlc.com')`,
		userID); err != nil {
		t.Fatalf("seed auth.users: %v", err)
	}
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.users (id, email, display_name, office, role, lang)
		 VALUES ($1, 'audit-test@hlc.com', 'Audit Test', 'munich', 'associate', 'de')`,
		userID); err != nil {
		t.Fatalf("seed paliad.users: %v", err)
	}
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.projects (id, type, path, title, reference, status, created_by)
		 VALUES ($1, 'project', $1::text, 'Audit Test Project', '2026/9999', 'active', $2)`,
		projectID, userID); err != nil {
		t.Fatalf("seed paliad.projects: %v", err)
	}

	now := time.Now().UTC()
	// Three project_events at well-separated timestamps so keyset pagination
	// is unambiguous regardless of insert ordering.
	for i, offset := range []time.Duration{-3 * time.Minute, -2 * time.Minute, -1 * time.Minute} {
		_, err := pool.ExecContext(ctx,
			`INSERT INTO paliad.project_events
			    (id, project_id, event_type, title, description, event_date,
			     created_by, metadata, created_at, updated_at)
			 VALUES ($1, $2, 'project_type_changed', $3, $4, $5, $6, '{}'::jsonb, $5, $5)`,
			uuid.New(), projectID, "Project type changed",
			"case → litigation 100% sure",
			now.Add(offset), userID)
		if err != nil {
			t.Fatalf("seed project_events[%d]: %v", i, err)
		}
	}
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.caldav_sync_log
		   (id, user_id, occurred_at, direction, items_pushed, items_pulled, error, duration_ms)
		 VALUES ($1, $2, $3, 'both', 1, 0, NULL, 50)`,
		uuid.New(), userID, now.Add(-30*time.Second)); err != nil {
		t.Fatalf("seed caldav_sync_log: %v", err)
	}
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.reminder_log
		   (id, user_id, reminder_type, sent_at, slot, slot_date)
		 VALUES ($1, $2, 'morning_digest', $3, 'morning', $3::date)`,
		uuid.New(), userID, now); err != nil {
		t.Fatalf("seed reminder_log: %v", err)
	}

	svc := NewAuditService(pool)

	// All sources, last few minutes — should see at least 5 rows (3 project,
	// 1 caldav, 1 reminder), newest first.
	rows, err := svc.ListEntries(ctx, AuditFilter{
		From:  now.Add(-5 * time.Minute),
		Limit: 10,
	})
	if err != nil {
		t.Fatalf("list all: %v", err)
	}
	if len(rows) < 5 {
		t.Fatalf("want >=5 rows, got %d", len(rows))
	}
	if !rows[0].Timestamp.After(rows[len(rows)-1].Timestamp) &&
		!rows[0].Timestamp.Equal(rows[len(rows)-1].Timestamp) {
		t.Errorf("rows not in DESC order: first=%v last=%v", rows[0].Timestamp, rows[len(rows)-1].Timestamp)
	}

	sources := map[string]int{}
	for _, r := range rows {
		sources[r.Source]++
	}
	if sources[AuditSourceProjectEvents] < 3 {
		t.Errorf("missing project_events rows: %v", sources)
	}
	if sources[AuditSourceCalDAVLog] < 1 {
		t.Errorf("missing caldav_sync_log rows: %v", sources)
	}
	if sources[AuditSourceReminderLog] < 1 {
		t.Errorf("missing reminder_log rows: %v", sources)
	}

	// Source filter — only project_events.
	pe, err := svc.ListEntries(ctx, AuditFilter{
		Source: AuditSourceProjectEvents,
		From:   now.Add(-5 * time.Minute),
		Limit:  10,
	})
	if err != nil {
		t.Fatalf("list project_events: %v", err)
	}
	for _, r := range pe {
		if r.Source != AuditSourceProjectEvents {
			t.Errorf("source filter leaked %q", r.Source)
		}
	}

	// Cursor pagination — first call with limit=2, then continue.
	page1, err := svc.ListEntries(ctx, AuditFilter{
		Source: AuditSourceProjectEvents,
		From:   now.Add(-5 * time.Minute),
		Limit:  2,
	})
	if err != nil {
		t.Fatalf("list page1: %v", err)
	}
	if len(page1) != 2 {
		t.Fatalf("want 2 page1 rows, got %d", len(page1))
	}
	cursorTS := page1[1].Timestamp
	cursorID := page1[1].ID
	page2, err := svc.ListEntries(ctx, AuditFilter{
		Source:   AuditSourceProjectEvents,
		From:     now.Add(-5 * time.Minute),
		BeforeTS: &cursorTS,
		BeforeID: &cursorID,
		Limit:    10,
	})
	if err != nil {
		t.Fatalf("list page2: %v", err)
	}
	for _, r := range page2 {
		for _, p := range page1 {
			if r.ID == p.ID {
				t.Errorf("cursor returned duplicate row %v", r.ID)
			}
		}
	}

	// Search escape — "100%" must match the seeded description literal, not
	// behave as ILIKE wildcard.
	hits, err := svc.ListEntries(ctx, AuditFilter{
		Source: AuditSourceProjectEvents,
		From:   now.Add(-5 * time.Minute),
		Search: "100%",
		Limit:  10,
	})
	if err != nil {
		t.Fatalf("list search: %v", err)
	}
	if len(hits) == 0 {
		t.Error("expected at least one hit for literal '100%'")
	}
}
