package userstore_test

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

	"github.com/jackc/pgx/v5"

	"archivmail/internal/userstore"
)

func newTestStore(t *testing.T) *userstore.Store {
	t.Helper()
	dsn := os.Getenv("TEST_DATABASE_URL")
	if dsn == "" {
		t.Skip("TEST_DATABASE_URL not set — skipping (needs PostgreSQL)")
	}
	// Use a unique schema per test to isolate
	schema := "test_" + strings.ReplaceAll(t.Name(), "/", "_")
	schema = strings.ToLower(schema)
	// Append schema to DSN
	sep := "?"
	if strings.Contains(dsn, "?") {
		sep = "&"
	}
	schemaDSN := dsn + sep + "search_path=" + schema

	// Create schema
	ctx := context.Background()
	conn, err := pgx.Connect(ctx, dsn)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	conn.Exec(ctx, "CREATE SCHEMA IF NOT EXISTS "+schema)
	conn.Close(ctx)

	s, err := userstore.New(schemaDSN)
	if err != nil {
		t.Fatalf("userstore.New: %v", err)
	}
	t.Cleanup(func() {
		s.Close()
		conn2, _ := pgx.Connect(context.Background(), dsn)
		if conn2 != nil {
			conn2.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE")
			conn2.Close(context.Background())
		}
	})
	return s
}

func TestCreateAndGetUser(t *testing.T) {
	s := newTestStore(t)

	u, err := s.Create(userstore.CreateUserRequest{
		Username: "alice",
		Email:    "alice@example.com",
		Password: "secret123",
		Role:     userstore.RoleAdmin,
	})
	if err != nil {
		t.Fatalf("Create: %v", err)
	}
	if u.ID == 0 {
		t.Error("expected non-zero ID")
	}
	if u.Username != "alice" {
		t.Errorf("Username = %q", u.Username)
	}
	if u.Role != userstore.RoleAdmin {
		t.Errorf("Role = %q", u.Role)
	}
	if u.Source != "local" {
		t.Errorf("Source = %q, want local", u.Source)
	}

	got, err := s.GetByID(u.ID)
	if err != nil {
		t.Fatalf("GetByID: %v", err)
	}
	if got.Email != "alice@example.com" {
		t.Errorf("Email = %q", got.Email)
	}
}

func TestVerifyPassword(t *testing.T) {
	s := newTestStore(t)
	_, err := s.Create(userstore.CreateUserRequest{
		Username: "bob", Email: "bob@example.com",
		Password: "correcthorse", Role: userstore.RoleUser,
	})
	if err != nil {
		t.Fatal(err)
	}

	// Correct password
	u, err := s.VerifyPassword("bob", "correcthorse")
	if err != nil {
		t.Errorf("VerifyPassword correct: %v", err)
	}
	if u.Username != "bob" {
		t.Errorf("Username = %q", u.Username)
	}

	// Wrong password
	if _, err := s.VerifyPassword("bob", "wrongpassword"); err == nil {
		t.Error("expected error for wrong password")
	}

	// Non-existent user
	if _, err := s.VerifyPassword("nobody", "x"); err == nil {
		t.Error("expected error for unknown user")
	}
}

// TestVerifyLogin covers the full PROJ-46 login-identifier matrix.
func TestVerifyLogin(t *testing.T) {
	s := newTestStore(t)
	ctx := context.Background()

	tenantID := int64(1)

	// Tenant user: username "patrick" != email
	if _, err := s.Create(userstore.CreateUserRequest{
		Username: "patrick", Email: "patrick@perlbach24.de",
		Password: "pw-tenant", Role: userstore.RoleUser, TenantID: &tenantID,
	}); err != nil {
		t.Fatal(err)
	}
	// Non-tenant user (superadmin/system), tenant_id IS NULL
	if _, err := s.Create(userstore.CreateUserRequest{
		Username: "superadmin", Email: "superadmin@localhost",
		Password: "pw-super", Role: userstore.RoleSuperAdmin, TenantID: nil,
	}); err != nil {
		t.Fatal(err)
	}

	// 1. Tenant user via email → success
	u, err := s.VerifyLogin(ctx, "patrick@perlbach24.de", "pw-tenant")
	if err != nil {
		t.Fatalf("tenant user via email should succeed: %v", err)
	}
	if u.Username != "patrick" {
		t.Errorf("Username = %q, want patrick", u.Username)
	}

	// 2. Tenant user via username (≠ email) → invalid_credentials
	if _, err := s.VerifyLogin(ctx, "patrick", "pw-tenant"); err == nil {
		t.Error("tenant user via username should be rejected")
	}

	// 3. Non-tenant user via username → success
	u, err = s.VerifyLogin(ctx, "superadmin", "pw-super")
	if err != nil {
		t.Fatalf("non-tenant user via username should succeed: %v", err)
	}
	if u.Username != "superadmin" {
		t.Errorf("Username = %q, want superadmin", u.Username)
	}

	// 4. Non-tenant user via email → success
	u, err = s.VerifyLogin(ctx, "superadmin@localhost", "pw-super")
	if err != nil {
		t.Fatalf("non-tenant user via email should succeed: %v", err)
	}
	if u.Username != "superadmin" {
		t.Errorf("Username = %q, want superadmin", u.Username)
	}

	// 5. Unknown identifier → invalid_credentials
	if _, err := s.VerifyLogin(ctx, "ghost@nowhere.tld", "x"); err == nil {
		t.Error("unknown identifier should be rejected")
	}
	if _, err := s.VerifyLogin(ctx, "ghost", "x"); err == nil {
		t.Error("unknown username should be rejected")
	}

	// Wrong password for valid identifier → rejected
	if _, err := s.VerifyLogin(ctx, "patrick@perlbach24.de", "wrong"); err == nil {
		t.Error("wrong password should be rejected")
	}
}

func TestUpdateUser(t *testing.T) {
	s := newTestStore(t)
	u, _ := s.Create(userstore.CreateUserRequest{
		Username: "carol", Email: "carol@old.com",
		Password: "pw", Role: userstore.RoleUser,
	})

	newEmail := "carol@new.com"
	newRole := userstore.RoleAuditor
	updated, err := s.Update(u.ID, userstore.UpdateUserRequest{
		Email: &newEmail,
		Role:  &newRole,
	})
	if err != nil {
		t.Fatalf("Update: %v", err)
	}
	if updated.Email != "carol@new.com" {
		t.Errorf("Email after update = %q", updated.Email)
	}
	if updated.Role != userstore.RoleAuditor {
		t.Errorf("Role after update = %q", updated.Role)
	}
}

func TestDisableUser(t *testing.T) {
	s := newTestStore(t)
	u, _ := s.Create(userstore.CreateUserRequest{
		Username: "dave", Email: "dave@x.com",
		Password: "pw", Role: userstore.RoleUser,
	})

	active := false
	s.Update(u.ID, userstore.UpdateUserRequest{Active: &active})

	if _, err := s.VerifyPassword("dave", "pw"); err == nil {
		t.Error("disabled user should not be able to login")
	}
}

func TestDeleteUser(t *testing.T) {
	s := newTestStore(t)
	u, _ := s.Create(userstore.CreateUserRequest{
		Username: "eve", Email: "eve@x.com",
		Password: "pw", Role: userstore.RoleUser,
	})

	if err := s.Delete(u.ID); err != nil {
		t.Fatalf("Delete: %v", err)
	}
	if _, err := s.GetByID(u.ID); err == nil {
		t.Error("GetByID should error after delete")
	}
	// Delete non-existent should error
	if err := s.Delete(u.ID); err == nil {
		t.Error("second delete should return error")
	}
}

func TestListUsers(t *testing.T) {
	s := newTestStore(t)
	users := []userstore.CreateUserRequest{
		{Username: "u1", Email: "u1@x.com", Password: "pw", Role: userstore.RoleUser},
		{Username: "u2", Email: "u2@x.com", Password: "pw", Role: userstore.RoleAdmin},
		{Username: "u3", Email: "u3@x.com", Password: "pw", Role: userstore.RoleAuditor},
		{Username: "u4", Email: "u4@x.com", Password: "pw", Role: userstore.RoleUser},
	}
	for _, req := range users {
		s.Create(req)
	}

	all, err := s.List("")
	if err != nil {
		t.Fatalf("List all: %v", err)
	}
	if len(all) != 4 {
		t.Errorf("List all: got %d, want 4", len(all))
	}

	admins, _ := s.List(userstore.RoleAdmin)
	if len(admins) != 1 {
		t.Errorf("List admin: got %d, want 1", len(admins))
	}

	regular, _ := s.List(userstore.RoleUser)
	if len(regular) != 2 {
		t.Errorf("List user: got %d, want 2", len(regular))
	}
}

func TestTokenBlacklist(t *testing.T) {
	s := newTestStore(t)

	jti := "test-jti-12345"
	expires := time.Now().Add(1 * time.Hour)

	if err := s.BlacklistToken(jti, expires); err != nil {
		t.Fatalf("BlacklistToken: %v", err)
	}

	blacklisted, err := s.IsBlacklisted(jti)
	if err != nil {
		t.Fatalf("IsBlacklisted: %v", err)
	}
	if !blacklisted {
		t.Error("token should be blacklisted")
	}

	// Non-blacklisted token
	bl2, _ := s.IsBlacklisted("other-jti")
	if bl2 {
		t.Error("unknown token should not be blacklisted")
	}
}

func TestCleanExpiredTokens(t *testing.T) {
	s := newTestStore(t)

	// Add an already-expired token
	s.BlacklistToken("expired-jti", time.Now().Add(-1*time.Hour))
	// Add a valid token
	s.BlacklistToken("valid-jti", time.Now().Add(1*time.Hour))

	if err := s.CleanExpiredTokens(); err != nil {
		t.Fatalf("CleanExpiredTokens: %v", err)
	}

	bl, _ := s.IsBlacklisted("expired-jti")
	if bl {
		t.Error("expired token should be cleaned up")
	}
	bl2, _ := s.IsBlacklisted("valid-jti")
	if !bl2 {
		t.Error("valid token should still be blacklisted")
	}
}

func TestUpsertLDAPUser(t *testing.T) {
	s := newTestStore(t)

	u, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor)
	if err != nil {
		t.Fatalf("UpsertLDAPUser: %v", err)
	}
	if u.Source != "ldap" {
		t.Errorf("Source = %q, want ldap", u.Source)
	}

	// Second upsert should update, not duplicate
	u2, err := s.UpsertLDAPUser("ldapuser", "ldap@corp.com", userstore.RoleAuditor)
	if err != nil {
		t.Fatalf("UpsertLDAPUser second: %v", err)
	}
	if u2.ID != u.ID {
		t.Error("second upsert should not create a new record")
	}

	all, _ := s.List("")
	if len(all) != 1 {
		t.Errorf("expected 1 user after double upsert, got %d", len(all))
	}
}
