package library

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"os"
	"testing"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
)

// triggerTestPool returns a pool against MINSTREL_TEST_DATABASE_URL or skips.
func triggerTestPool(t *testing.T) *pgxpool.Pool {
	t.Helper()
	url := os.Getenv("MINSTREL_TEST_DATABASE_URL")
	if url == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	pool, err := pgxpool.New(context.Background(), url)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	t.Cleanup(pool.Close)
	// Reset scan_runs so each test starts clean.
	if _, err := pool.Exec(context.Background(), `DELETE FROM scan_runs`); err != nil {
		t.Fatalf("reset: %v", err)
	}
	return pool
}

// quietLogger returns a logger that drops all output.
func quietLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, nil))
}

func TestTryStartScan_NoInFlight_Starts(t *testing.T) {
	pool := triggerTestPool(t)

	started, existing, err := TryStartScan(
		context.Background(), pool,
		nil, nil, // scanner/enricher: nil OK because the goroutine that uses them is detached and we don't await it.
		quietLogger(), RunScanConfig{},
	)
	if err != nil {
		t.Fatalf("err = %v, want nil", err)
	}
	if !started {
		t.Errorf("started = false, want true")
	}
	if existing != nil {
		t.Errorf("existing = %v, want nil", existing)
	}
	// Give the detached goroutine a moment.
	time.Sleep(50 * time.Millisecond)
}

func TestTryStartScan_FreshInFlight_Skips(t *testing.T) {
	pool := triggerTestPool(t)

	// Seed a fresh in-flight row.
	var rowID pgtype.UUID
	if err := pool.QueryRow(context.Background(),
		`INSERT INTO scan_runs (started_at) VALUES (now()) RETURNING id`,
	).Scan(&rowID); err != nil {
		t.Fatalf("seed: %v", err)
	}

	started, existing, err := TryStartScan(
		context.Background(), pool,
		nil, nil,
		quietLogger(), RunScanConfig{},
	)
	if err != nil {
		t.Fatalf("err = %v, want nil", err)
	}
	if started {
		t.Errorf("started = true, want false")
	}
	if existing == nil {
		t.Fatal("existing = nil, want non-nil")
	}
	if existing.ID != rowID {
		t.Errorf("existing.ID = %v, want %v", existing.ID, rowID)
	}
}

func TestTryStartScan_StaleInFlight_Reaps(t *testing.T) {
	pool := triggerTestPool(t)

	// Seed a stale in-flight row (started 2 hours ago).
	var staleID pgtype.UUID
	if err := pool.QueryRow(context.Background(),
		`INSERT INTO scan_runs (started_at) VALUES (now() - interval '2 hours') RETURNING id`,
	).Scan(&staleID); err != nil {
		t.Fatalf("seed: %v", err)
	}

	started, existing, err := TryStartScan(
		context.Background(), pool,
		nil, nil,
		quietLogger(), RunScanConfig{},
	)
	if err != nil {
		t.Fatalf("err = %v, want nil", err)
	}
	if !started {
		t.Errorf("started = false, want true (stale row should reap and proceed)")
	}
	if existing != nil {
		t.Errorf("existing = %v, want nil", existing)
	}

	// Verify the stale row was reaped: finished_at non-NULL, error_message contains "reaped".
	var finishedAt pgtype.Timestamptz
	var errMsg *string
	if err := pool.QueryRow(context.Background(),
		`SELECT finished_at, error_message FROM scan_runs WHERE id = $1`, staleID,
	).Scan(&finishedAt, &errMsg); err != nil {
		t.Fatalf("verify reap: %v", err)
	}
	if !finishedAt.Valid {
		t.Errorf("finished_at = NULL, want non-NULL after reap")
	}
	if errMsg == nil || *errMsg != "reaped (stale)" {
		t.Errorf("error_message = %v, want 'reaped (stale)'", errMsg)
	}

	time.Sleep(50 * time.Millisecond)
}

func TestTryStartScan_DBError_Propagates(t *testing.T) {
	url := os.Getenv("MINSTREL_TEST_DATABASE_URL")
	if url == "" {
		t.Skip("MINSTREL_TEST_DATABASE_URL not set")
	}
	pool, err := pgxpool.New(context.Background(), url)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	pool.Close() // close immediately to force errors on subsequent queries

	started, existing, err := TryStartScan(
		context.Background(), pool,
		nil, nil,
		quietLogger(), RunScanConfig{},
	)
	if err == nil {
		t.Errorf("err = nil, want non-nil for closed pool")
	}
	if errors.Is(err, pgx.ErrNoRows) {
		t.Errorf("err is ErrNoRows; want a different DB error type")
	}
	if started {
		t.Errorf("started = true, want false on DB error")
	}
	if existing != nil {
		t.Errorf("existing = %v, want nil on DB error", existing)
	}
}
