package email

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/tim/cairnquire/apps/server/internal/config"
	"github.com/tim/cairnquire/apps/server/internal/database"
)

func newTestDB(t *testing.T) *sql.DB {
	t.Helper()
	cfg := config.DatabaseConfig{Path: t.TempDir() + "/test.sqlite"}
	handle, err := database.Open(context.Background(), cfg)
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	t.Cleanup(func() { _ = handle.Close() })
	db := handle.SQL()
	if err := database.ApplyMigrations(context.Background(), db); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	// Seed a user + notification so the queue's notification_id FK is
	// satisfiable and MarkNotificationEmailed can update a real row.
	_, err = db.Exec(`INSERT INTO users (id, email, display_name, role, created_at, last_seen_at) VALUES ('user:alice@example.com', 'alice@example.com', 'Alice', 'admin', ?, ?)`,
		time.Now().UTC().Format(time.RFC3339), time.Now().UTC().Format(time.RFC3339))
	if err != nil {
		t.Fatalf("seed user: %v", err)
	}
	_, err = db.Exec(`INSERT INTO notifications (id, user_id, type, resource_type, resource_id, message, created_at) VALUES (?, 'user:alice@example.com', 'file_changed', 'document', 'doc:test', 'test', ?)`,
		"notif:test-1", time.Now().UTC().Format(time.RFC3339))
	if err != nil {
		t.Fatalf("seed notification: %v", err)
	}
	return db
}

func TestQueue_EnqueueAndPendingCount(t *testing.T) {
	db := newTestDB(t)
	q := NewQueue(db, nil, testLogger(t))

	id, err := q.Enqueue(context.Background(), "notif:test-1", "alice@example.com", "Subject", "Body")
	if err != nil {
		t.Fatalf("Enqueue: %v", err)
	}
	if id == "" {
		t.Fatal("expected non-empty id")
	}
	count, err := q.PendingCount(context.Background())
	if err != nil {
		t.Fatalf("PendingCount: %v", err)
	}
	if count != 1 {
		t.Errorf("pending = %d, want 1", count)
	}
}

func TestQueue_EnqueueValidation(t *testing.T) {
	db := newTestDB(t)
	q := NewQueue(db, nil, testLogger(t))

	// Empty notification_id is allowed (the column is nullable).
	if _, err := q.Enqueue(context.Background(), "", "alice@example.com", "s", "b"); err != nil {
		t.Fatalf("empty notification_id should be allowed: %v", err)
	}
	if _, err := q.Enqueue(context.Background(), "notif:test-1", "", "s", "b"); err == nil {
		t.Fatal("expected error for empty to")
	}
	if _, err := q.Enqueue(context.Background(), "notif:test-1", "alice@example.com", "", "b"); err == nil {
		t.Fatal("expected error for empty subject")
	}
}

type recordingSender struct {
	sends []recordedSend
	err   error
}

type recordedSend struct {
	to      []string
	subject string
	body    string
}

func (r *recordingSender) Send(ctx context.Context, to []string, subject, body string) error {
	if r.err != nil {
		return r.err
	}
	r.sends = append(r.sends, recordedSend{to: to, subject: subject, body: body})
	return nil
}

func TestQueue_DeliverSuccess(t *testing.T) {
	db := newTestDB(t)
	sender := &recordingSender{}
	q := NewQueue(db, sender, testLogger(t))
	q.SetPollInterval(20 * time.Millisecond)

	if _, err := q.Enqueue(context.Background(), "notif:test-1", "alice@example.com", "Hello", "World"); err != nil {
		t.Fatalf("Enqueue: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	go q.Run(ctx)

	if err := waitFor(ctx, func() bool {
		n, _ := q.PendingCount(ctx)
		return n == 0
	}); err != nil {
		t.Fatalf("email never sent: %v", err)
	}

	if len(sender.sends) != 1 {
		t.Fatalf("sends = %d, want 1", len(sender.sends))
	}
	if sender.sends[0].subject != "Hello" {
		t.Errorf("subject = %q", sender.sends[0].subject)
	}
	if sender.sends[0].body != "World" {
		t.Errorf("body = %q", sender.sends[0].body)
	}

	// The linked notification should now have emailed_at stamped.
	var emailedAt string
	if err := db.QueryRow(`SELECT COALESCE(emailed_at, '') FROM notifications WHERE id = ?`, "notif:test-1").Scan(&emailedAt); err != nil {
		t.Fatalf("query emailed_at: %v", err)
	}
	if emailedAt == "" {
		t.Error("expected notifications.emailed_at to be set after send")
	}
}

func TestQueue_RetryThenSucceed(t *testing.T) {
	db := newTestDB(t)
	// Fail twice, then succeed.
	sender := &flakySender{failTimes: 2}
	q := NewQueue(db, sender, testLogger(t))
	q.SetPollInterval(15 * time.Millisecond)
	q.SetMaxBackoff(50 * time.Millisecond)

	if _, err := q.Enqueue(context.Background(), "notif:test-1", "alice@example.com", "Retry", "Body"); err != nil {
		t.Fatalf("Enqueue: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	go q.Run(ctx)

	if err := waitFor(ctx, func() bool {
		return sender.successCount.Load() >= 1
	}); err != nil {
		t.Fatalf("email never succeeded: successCount=%d attempts=%d: %v", sender.successCount.Load(), sender.attempts.Load(), err)
	}

	// Verify the row is marked sent.
	var status string
	if err := db.QueryRow(`SELECT status FROM email_queue WHERE to_address = ?`, "alice@example.com").Scan(&status); err != nil {
		t.Fatalf("query status: %v", err)
	}
	if status != "sent" {
		t.Errorf("status = %q, want sent", status)
	}
}

func TestQueue_PermanentFailure(t *testing.T) {
	db := newTestDB(t)
	sender := &recordingSender{err: errors.New("permanent Postmark rejection")}
	q := NewQueue(db, sender, testLogger(t))
	q.SetPollInterval(15 * time.Millisecond)
	q.SetMaxBackoff(20 * time.Millisecond)

	if _, err := q.Enqueue(context.Background(), "notif:test-1", "alice@example.com", "Fail", "Body"); err != nil {
		t.Fatalf("Enqueue: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	go q.Run(ctx)

	if err := waitFor(ctx, func() bool {
		failed, _ := q.ListFailed(ctx, 10)
		return len(failed) == 1
	}); err != nil {
		failed, _ := q.ListFailed(ctx, 10)
		t.Fatalf("expected 1 failed row, got %d: %v", len(failed), err)
	}

	failed, _ := q.ListFailed(ctx, 10)
	if !strings.Contains(failed[0].LastError, "permanent") {
		t.Errorf("last_error = %q", failed[0].LastError)
	}
}

func TestQueue_ProviderRejectionFailsImmediately(t *testing.T) {
	db := newTestDB(t)
	sender := &recordingSender{err: &PermanentError{Cause: errors.New("sender domain not verified")}}
	q := NewQueue(db, sender, testLogger(t))

	id, err := q.Enqueue(context.Background(), "notif:test-1", "alice@example.com", "Fail", "Body")
	if err != nil {
		t.Fatalf("Enqueue: %v", err)
	}
	rows, err := q.claimBatch(context.Background(), 1)
	if err != nil || len(rows) != 1 {
		t.Fatalf("claimBatch = %#v, %v", rows, err)
	}
	if err := q.deliver(context.Background(), rows[0]); err != nil {
		t.Fatalf("deliver: %v", err)
	}
	var status string
	var attempts int
	if err := db.QueryRow(`SELECT status, attempts FROM email_queue WHERE id = ?`, id).Scan(&status, &attempts); err != nil {
		t.Fatalf("query row: %v", err)
	}
	if status != "failed" || attempts != 1 {
		t.Fatalf("status=%q attempts=%d, want failed after one attempt", status, attempts)
	}
}

func TestQueue_NilSenderLeavesPending(t *testing.T) {
	db := newTestDB(t)
	q := NewQueue(db, nil, testLogger(t))
	q.SetPollInterval(15 * time.Millisecond)

	if _, err := q.Enqueue(context.Background(), "notif:test-1", "alice@example.com", "Pending", "Body"); err != nil {
		t.Fatalf("Enqueue: %v", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
	defer cancel()
	go q.Run(ctx)

	<-ctx.Done()
	count, _ := q.PendingCount(context.Background())
	if count != 1 {
		t.Errorf("expected row to stay pending with nil sender, got %d", count)
	}
}

// flakySender fails the first N attempts then succeeds.
type flakySender struct {
	failTimes    int32
	attempts     atomic.Int32
	successCount atomic.Int32
}

func (f *flakySender) Send(ctx context.Context, to []string, subject, body string) error {
	attempt := f.attempts.Add(1)
	if attempt <= f.failTimes {
		return errors.New("transient failure")
	}
	f.successCount.Add(1)
	return nil
}

func waitFor(ctx context.Context, cond func() bool) error {
	ticker := time.NewTicker(10 * time.Millisecond)
	defer ticker.Stop()
	for {
		if cond() {
			return nil
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-ticker.C:
		}
	}
}
