package admin_test

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

	"galaxy/backend/internal/admin"
	"galaxy/backend/internal/config"
	backendpg "galaxy/backend/internal/postgres"
	pgshared "galaxy/postgres"

	testcontainers "github.com/testcontainers/testcontainers-go"
	tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"
	"go.uber.org/zap"
	"golang.org/x/crypto/bcrypt"
)

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

// startPostgres spins up a Postgres testcontainer with the backend
// migrations applied. The returned *sql.DB is closed and the container
// terminated by t.Cleanup hooks. Tests skip cleanly when Docker is
// unavailable.
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 buildService(t *testing.T, db *sql.DB) (*admin.Service, *admin.Store, *admin.Cache) {
	t.Helper()
	store := admin.NewStore(db)
	cache := admin.NewCache()
	if err := cache.Warm(context.Background(), store); err != nil {
		t.Fatalf("warm admin cache: %v", err)
	}
	svc := admin.NewService(admin.Deps{
		Store:  store,
		Cache:  cache,
		Logger: zap.NewNop(),
	})
	return svc, store, cache
}

func TestBootstrapInsertsThenSkips(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	store := admin.NewStore(db)

	cfg := config.AdminBootstrapConfig{User: "root", Password: "root-secret"}
	logger := zap.NewNop()

	if err := admin.Bootstrap(context.Background(), store, cfg, logger); err != nil {
		t.Fatalf("first bootstrap: %v", err)
	}
	first, hash, err := store.Lookup(context.Background(), "root")
	if err != nil {
		t.Fatalf("lookup after first bootstrap: %v", err)
	}
	if first.Username != "root" {
		t.Fatalf("Username = %q, want root", first.Username)
	}
	if err := bcrypt.CompareHashAndPassword(hash, []byte("root-secret")); err != nil {
		t.Fatalf("CompareHashAndPassword: %v", err)
	}

	// Second call must not modify the row even when the password value
	// supplied via env vars differs.
	cfg.Password = "different"
	if err := admin.Bootstrap(context.Background(), store, cfg, logger); err != nil {
		t.Fatalf("second bootstrap: %v", err)
	}
	_, sameHash, err := store.Lookup(context.Background(), "root")
	if err != nil {
		t.Fatalf("lookup after second bootstrap: %v", err)
	}
	if string(hash) != string(sameHash) {
		t.Fatalf("password_hash mutated by idempotent bootstrap")
	}
}

func TestBootstrapSkipsWhenUserEmpty(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	store := admin.NewStore(db)

	if err := admin.Bootstrap(context.Background(), store, config.AdminBootstrapConfig{}, zap.NewNop()); err != nil {
		t.Fatalf("bootstrap: %v", err)
	}
	admins, _, err := store.ListAll(context.Background())
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(admins) != 0 {
		t.Fatalf("ListAll = %d rows, want 0", len(admins))
	}
}

func TestVerifyHappyPath(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)

	created, err := svc.Create(context.Background(), admin.CreateInput{
		Username: "alice",
		Password: "alice-secret",
	})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if created.Username != "alice" {
		t.Fatalf("Username = %q, want alice", created.Username)
	}

	ok, err := svc.Verify(context.Background(), "alice", "alice-secret")
	if err != nil || !ok {
		t.Fatalf("Verify(correct) = (%v, %v), want (true, nil)", ok, err)
	}
}

func TestVerifyRejectsWrongPassword(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	_, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "good"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	ok, err := svc.Verify(context.Background(), "alice", "bad")
	if err != nil {
		t.Fatalf("Verify returned error: %v", err)
	}
	if ok {
		t.Fatalf("Verify(wrong) = true, want false")
	}
}

func TestVerifyRejectsUnknownUser(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)

	ok, err := svc.Verify(context.Background(), "ghost", "x")
	if err != nil || ok {
		t.Fatalf("Verify(ghost) = (%v, %v), want (false, nil)", ok, err)
	}
}

func TestVerifyRejectsDisabledAccount(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "good"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := svc.Disable(context.Background(), "alice"); err != nil {
		t.Fatalf("disable: %v", err)
	}

	ok, err := svc.Verify(context.Background(), "alice", "good")
	if err != nil || ok {
		t.Fatalf("Verify(disabled) = (%v, %v), want (false, nil)", ok, err)
	}
}

func TestEnableReversesDisable(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "good"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := svc.Disable(context.Background(), "alice"); err != nil {
		t.Fatalf("disable: %v", err)
	}
	got, err := svc.Enable(context.Background(), "alice")
	if err != nil {
		t.Fatalf("enable: %v", err)
	}
	if got.DisabledAt != nil {
		t.Fatalf("DisabledAt = %v, want nil after enable", got.DisabledAt)
	}

	ok, err := svc.Verify(context.Background(), "alice", "good")
	if err != nil || !ok {
		t.Fatalf("Verify after enable = (%v, %v), want (true, nil)", ok, err)
	}
}

func TestCreateRejectsDuplicateUsername(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "x"}); err != nil {
		t.Fatalf("create #1: %v", err)
	}
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "y"}); !errors.Is(err, admin.ErrUsernameTaken) {
		t.Fatalf("Create #2 err = %v, want ErrUsernameTaken", err)
	}
}

func TestCreateRejectsEmptyFields(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)

	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "", Password: "x"}); !errors.Is(err, admin.ErrInvalidInput) {
		t.Fatalf("Create(empty username) err = %v, want ErrInvalidInput", err)
	}
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: ""}); !errors.Is(err, admin.ErrInvalidInput) {
		t.Fatalf("Create(empty password) err = %v, want ErrInvalidInput", err)
	}
}

func TestResetPasswordReplacesHash(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "old"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := svc.ResetPassword(context.Background(), "alice", "new-secret"); err != nil {
		t.Fatalf("reset: %v", err)
	}

	if ok, _ := svc.Verify(context.Background(), "alice", "old"); ok {
		t.Fatalf("Verify(old) = true after reset")
	}
	if ok, err := svc.Verify(context.Background(), "alice", "new-secret"); err != nil || !ok {
		t.Fatalf("Verify(new) = (%v, %v), want (true, nil)", ok, err)
	}
}

func TestResetPasswordOnUnknownUser(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)

	if _, err := svc.ResetPassword(context.Background(), "ghost", "x"); !errors.Is(err, admin.ErrNotFound) {
		t.Fatalf("ResetPassword(ghost) err = %v, want ErrNotFound", err)
	}
}

func TestListReturnsAllRows(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	for _, u := range []string{"alice", "bob", "carol"} {
		if _, err := svc.Create(context.Background(), admin.CreateInput{Username: u, Password: "x"}); err != nil {
			t.Fatalf("create %s: %v", u, err)
		}
	}
	got, err := svc.List(context.Background())
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 3 {
		t.Fatalf("List = %d rows, want 3", len(got))
	}
	// Order is by username ASC at the SQL level.
	if got[0].Username != "alice" || got[1].Username != "bob" || got[2].Username != "carol" {
		t.Fatalf("List order = %v, want [alice bob carol]", []string{got[0].Username, got[1].Username, got[2].Username})
	}
}

func TestVerifyTouchesLastUsedAt(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, store, _ := buildService(t, db)
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "good"}); err != nil {
		t.Fatalf("create: %v", err)
	}

	if ok, err := svc.Verify(context.Background(), "alice", "good"); err != nil || !ok {
		t.Fatalf("Verify: (%v, %v)", ok, err)
	}

	// last_used_at is updated by a fire-and-forget goroutine. Poll until
	// it lands or the deadline passes.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		got, _, err := store.Lookup(context.Background(), "alice")
		if err != nil {
			t.Fatalf("lookup: %v", err)
		}
		if got.LastUsedAt != nil {
			return
		}
		time.Sleep(20 * time.Millisecond)
	}
	t.Fatalf("LastUsedAt not populated after Verify")
}

func TestDisableIsIdempotent(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	if _, err := svc.Create(context.Background(), admin.CreateInput{Username: "alice", Password: "x"}); err != nil {
		t.Fatalf("create: %v", err)
	}
	first, err := svc.Disable(context.Background(), "alice")
	if err != nil {
		t.Fatalf("disable #1: %v", err)
	}
	if first.DisabledAt == nil {
		t.Fatalf("DisabledAt = nil after disable")
	}
	second, err := svc.Disable(context.Background(), "alice")
	if err != nil {
		t.Fatalf("disable #2: %v", err)
	}
	if second.DisabledAt == nil {
		t.Fatalf("DisabledAt = nil on second disable")
	}
}

func TestDisableUnknownUser(t *testing.T) {
	t.Parallel()
	db := startPostgres(t)
	svc, _, _ := buildService(t, db)
	if _, err := svc.Disable(context.Background(), "ghost"); !errors.Is(err, admin.ErrNotFound) {
		t.Fatalf("Disable(ghost) err = %v, want ErrNotFound", err)
	}
}
