package mail_test

import (
	"context"
	"database/sql"
	"errors"
	"net/url"
	"testing"
	"time"

	"galaxy/backend/internal/mail"
	backendpg "galaxy/backend/internal/postgres"
	pgshared "galaxy/postgres"

	"github.com/google/uuid"
	testcontainers "github.com/testcontainers/testcontainers-go"
	tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"
)

const (
	pgImage    = "postgres:16-alpine"
	pgUser     = "galaxy"
	pgPassword = "galaxy"
	pgDatabase = "galaxy_backend"
	pgSchema   = "backend"
	pgStartup  = 90 * time.Second
	pgOpTO     = 10 * time.Second
)

// startPostgres mirrors the auth_e2e_test scaffolding: spin up
// Postgres, apply migrations, return *sql.DB.
func startPostgres(t *testing.T) *sql.DB {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	t.Cleanup(cancel)

	pgContainer, err := tcpostgres.Run(ctx, pgImage,
		tcpostgres.WithDatabase(pgDatabase),
		tcpostgres.WithUsername(pgUser),
		tcpostgres.WithPassword(pgPassword),
		testcontainers.WithWaitStrategy(
			wait.ForLog("database system is ready to accept connections").
				WithOccurrence(2).
				WithStartupTimeout(pgStartup),
		),
	)
	if err != nil {
		t.Skipf("postgres testcontainer unavailable, skipping: %v", err)
	}
	t.Cleanup(func() {
		if termErr := testcontainers.TerminateContainer(pgContainer); termErr != nil {
			t.Errorf("terminate postgres container: %v", termErr)
		}
	})

	baseDSN, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		t.Fatalf("connection string: %v", err)
	}
	scopedDSN, err := dsnWithSearchPath(baseDSN, pgSchema)
	if err != nil {
		t.Fatalf("scope dsn: %v", err)
	}

	cfg := pgshared.DefaultConfig()
	cfg.PrimaryDSN = scopedDSN
	cfg.OperationTimeout = pgOpTO

	db, err := pgshared.OpenPrimary(ctx, cfg, backendpg.NoObservabilityOptions()...)
	if err != nil {
		t.Fatalf("open primary: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })

	if err := pgshared.Ping(ctx, db, cfg.OperationTimeout); err != nil {
		t.Fatalf("ping: %v", err)
	}
	if err := backendpg.ApplyMigrations(ctx, db); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}
	return db
}

func dsnWithSearchPath(baseDSN, schema string) (string, error) {
	parsed, err := url.Parse(baseDSN)
	if err != nil {
		return "", err
	}
	values := parsed.Query()
	values.Set("search_path", schema)
	if values.Get("sslmode") == "" {
		values.Set("sslmode", "disable")
	}
	parsed.RawQuery = values.Encode()
	return parsed.String(), nil
}

func TestStoreInsertEnqueueRoundTrip(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	store := mail.NewStore(db)
	ctx := context.Background()

	args := mail.EnqueueArgs{
		DeliveryID:     uuid.New(),
		TemplateID:     mail.TemplateLoginCode,
		IdempotencyKey: uuid.NewString(),
		Recipients:     []string{"alice@example.test"},
		ContentType:    "text/plain",
		Subject:        "hello",
		Body:           []byte("hi"),
	}
	inserted, err := store.InsertEnqueue(ctx, args)
	if err != nil {
		t.Fatalf("insert: %v", err)
	}
	if !inserted {
		t.Fatal("first insert must report inserted=true")
	}

	// Same idempotency key must dedupe.
	args2 := args
	args2.DeliveryID = uuid.New()
	inserted2, err := store.InsertEnqueue(ctx, args2)
	if err != nil {
		t.Fatalf("insert retry: %v", err)
	}
	if inserted2 {
		t.Fatal("re-enqueue with same key must report inserted=false")
	}

	d, err := store.GetDelivery(ctx, args.DeliveryID)
	if err != nil {
		t.Fatalf("get delivery: %v", err)
	}
	if d.Status != mail.StatusPending {
		t.Fatalf("status=%q want pending", d.Status)
	}
	if d.NextAttemptAt == nil {
		t.Fatal("next_attempt_at must be set on insert")
	}
}

func TestStoreClaimDueAndMarkSent(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	store := mail.NewStore(db)
	ctx := context.Background()

	deliveryID := uuid.New()
	if _, err := store.InsertEnqueue(ctx, mail.EnqueueArgs{
		DeliveryID:     deliveryID,
		TemplateID:     mail.TemplateLoginCode,
		IdempotencyKey: uuid.NewString(),
		Recipients:     []string{"bob@example.test"},
		ContentType:    "text/plain",
		Subject:        "hello",
		Body:           []byte("hi"),
	}); err != nil {
		t.Fatalf("insert: %v", err)
	}

	tx, err := store.BeginTx(ctx)
	if err != nil {
		t.Fatalf("begin: %v", err)
	}
	t.Cleanup(func() { _ = tx.Rollback() })

	claimed, err := store.ClaimDue(ctx, tx, 5)
	if err != nil {
		t.Fatalf("claim: %v", err)
	}
	if len(claimed) != 1 {
		t.Fatalf("got %d claimed, want 1", len(claimed))
	}
	if claimed[0].Delivery.DeliveryID != deliveryID {
		t.Fatalf("claimed wrong delivery: %s", claimed[0].Delivery.DeliveryID)
	}
	if string(claimed[0].Payload.Body) != "hi" {
		t.Fatalf("payload body lost in round trip: %q", claimed[0].Payload.Body)
	}
	if len(claimed[0].Recipients) != 1 || claimed[0].Recipients[0].Address != "bob@example.test" {
		t.Fatalf("recipient lost: %+v", claimed[0].Recipients)
	}

	now := time.Now().UTC()
	if _, err := store.RecordAttempt(ctx, tx, deliveryID, now, now, mail.OutcomeSuccess, ""); err != nil {
		t.Fatalf("record attempt: %v", err)
	}
	if err := store.MarkSent(ctx, tx, deliveryID, now); err != nil {
		t.Fatalf("mark sent: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatalf("commit: %v", err)
	}

	d, err := store.GetDelivery(ctx, deliveryID)
	if err != nil {
		t.Fatalf("get delivery: %v", err)
	}
	if d.Status != mail.StatusSent {
		t.Fatalf("status=%q want sent", d.Status)
	}
	if d.SentAt == nil {
		t.Fatal("sent_at must be set after MarkSent")
	}
	if d.Attempts != 1 {
		t.Fatalf("attempts=%d want 1", d.Attempts)
	}

	attempts, err := store.ListAttempts(ctx, deliveryID)
	if err != nil {
		t.Fatalf("list attempts: %v", err)
	}
	if len(attempts) != 1 || attempts[0].Outcome != mail.OutcomeSuccess {
		t.Fatalf("attempts=%+v", attempts)
	}
}

func TestStoreScheduleRetryThenDeadLetter(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	store := mail.NewStore(db)
	ctx := context.Background()

	deliveryID := uuid.New()
	if _, err := store.InsertEnqueue(ctx, mail.EnqueueArgs{
		DeliveryID:     deliveryID,
		TemplateID:     "test.template",
		IdempotencyKey: uuid.NewString(),
		Recipients:     []string{"carol@example.test"},
		ContentType:    "text/plain",
		Subject:        "hi",
		Body:           []byte("body"),
	}); err != nil {
		t.Fatalf("insert: %v", err)
	}

	tx, err := store.BeginTx(ctx)
	if err != nil {
		t.Fatalf("begin tx 1: %v", err)
	}
	if _, err := store.ClaimDue(ctx, tx, 1); err != nil {
		t.Fatalf("claim 1: %v", err)
	}
	now := time.Now().UTC()
	if _, err := store.RecordAttempt(ctx, tx, deliveryID, now, now, mail.OutcomeTransientError, "boom"); err != nil {
		t.Fatalf("record attempt: %v", err)
	}
	if err := store.ScheduleRetry(ctx, tx, deliveryID, now, now.Add(2*time.Second), "boom"); err != nil {
		t.Fatalf("schedule retry: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatalf("commit 1: %v", err)
	}

	d, err := store.GetDelivery(ctx, deliveryID)
	if err != nil {
		t.Fatalf("get delivery: %v", err)
	}
	if d.Status != mail.StatusRetrying {
		t.Fatalf("status=%q want retrying", d.Status)
	}
	if d.LastError != "boom" {
		t.Fatalf("last_error=%q want boom", d.LastError)
	}

	tx2, err := store.BeginTx(ctx)
	if err != nil {
		t.Fatalf("begin tx 2: %v", err)
	}
	if err := store.MarkDeadLettered(ctx, tx2, deliveryID, now, "max attempts"); err != nil {
		t.Fatalf("mark dead-lettered: %v", err)
	}
	if err := tx2.Commit(); err != nil {
		t.Fatalf("commit 2: %v", err)
	}

	d, err = store.GetDelivery(ctx, deliveryID)
	if err != nil {
		t.Fatalf("get delivery 2: %v", err)
	}
	if d.Status != mail.StatusDeadLettered {
		t.Fatalf("status=%q want dead_lettered", d.Status)
	}
	if d.DeadLetteredAt == nil {
		t.Fatal("dead_lettered_at must be set")
	}

	_, total, err := store.ListDeadLetters(ctx, 0, 25)
	if err != nil {
		t.Fatalf("list dead letters: %v", err)
	}
	if total != 1 {
		t.Fatalf("dead-letter total=%d want 1", total)
	}
}

func TestStoreResendNonSent(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	store := mail.NewStore(db)
	ctx := context.Background()

	deliveryID := uuid.New()
	if _, err := store.InsertEnqueue(ctx, mail.EnqueueArgs{
		DeliveryID:     deliveryID,
		TemplateID:     "test.template",
		IdempotencyKey: uuid.NewString(),
		Recipients:     []string{"d@example.test"},
		ContentType:    "text/plain",
		Subject:        "hi",
		Body:           []byte("b"),
	}); err != nil {
		t.Fatalf("insert: %v", err)
	}

	// re-arm pending row -> ok.
	if _, err := store.ResendNonSent(ctx, deliveryID, time.Now().UTC()); err != nil {
		t.Fatalf("resend pending: %v", err)
	}

	// flip to sent and verify resend now errors.
	tx, err := store.BeginTx(ctx)
	if err != nil {
		t.Fatalf("begin: %v", err)
	}
	if _, err := store.ClaimDue(ctx, tx, 1); err != nil {
		t.Fatalf("claim: %v", err)
	}
	now := time.Now().UTC()
	if _, err := store.RecordAttempt(ctx, tx, deliveryID, now, now, mail.OutcomeSuccess, ""); err != nil {
		t.Fatalf("record attempt: %v", err)
	}
	if err := store.MarkSent(ctx, tx, deliveryID, now); err != nil {
		t.Fatalf("mark sent: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatalf("commit: %v", err)
	}

	if _, err := store.ResendNonSent(ctx, deliveryID, time.Now().UTC()); !errors.Is(err, mail.ErrResendOnSent) {
		t.Fatalf("resend on sent: want ErrResendOnSent, got %v", err)
	}

	if _, err := store.ResendNonSent(ctx, uuid.New(), time.Now().UTC()); !errors.Is(err, mail.ErrDeliveryNotFound) {
		t.Fatalf("resend on missing: want ErrDeliveryNotFound, got %v", err)
	}
}
