package auth

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/orchard9/rdev/internal/adapter/postgres"
	"github.com/orchard9/rdev/internal/service"
	"github.com/orchard9/rdev/internal/testutil"
)

func TestAPIKey_IsExpired(t *testing.T) {
	now := time.Now()
	past := now.Add(-1 * time.Hour)
	future := now.Add(1 * time.Hour)

	tests := []struct {
		name string
		key  *APIKey
		want bool
	}{
		{"nil expiration", &APIKey{ExpiresAt: nil}, false},
		{"expired", &APIKey{ExpiresAt: &past}, true},
		{"not expired", &APIKey{ExpiresAt: &future}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.key.IsExpired(); got != tt.want {
				t.Errorf("IsExpired() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAPIKey_IsRevoked(t *testing.T) {
	now := time.Now()

	tests := []struct {
		name string
		key  *APIKey
		want bool
	}{
		{"not revoked", &APIKey{RevokedAt: nil}, false},
		{"revoked", &APIKey{RevokedAt: &now}, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.key.IsRevoked(); got != tt.want {
				t.Errorf("IsRevoked() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAPIKey_IsActive(t *testing.T) {
	now := time.Now()
	past := now.Add(-1 * time.Hour)
	future := now.Add(1 * time.Hour)

	tests := []struct {
		name string
		key  *APIKey
		want bool
	}{
		{"active", &APIKey{ExpiresAt: &future, RevokedAt: nil}, true},
		{"expired", &APIKey{ExpiresAt: &past, RevokedAt: nil}, false},
		{"revoked", &APIKey{ExpiresAt: &future, RevokedAt: &now}, false},
		{"never expires", &APIKey{ExpiresAt: nil, RevokedAt: nil}, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.key.IsActive(); got != tt.want {
				t.Errorf("IsActive() = %v, want %v", got, tt.want)
			}
		})
	}
}

// newTestService creates an auth.Service backed by the real postgres repo for integration tests.
func newTestService(t *testing.T, adminKey string) *Service {
	t.Helper()
	db := testutil.TestDB(t)
	t.Cleanup(func() { testutil.CleanupTestKeys(t, db) })

	repo := postgres.NewAPIKeyRepository(db)
	apiKeySvc := service.NewAPIKeyService(repo, adminKey)
	return NewService(apiKeySvc, adminKey)
}

func TestService_IsAdminKey(t *testing.T) {
	svc := NewService(nil, "admin-secret")

	tests := []struct {
		name string
		key  string
		want bool
	}{
		{"matches admin key", "admin-secret", true},
		{"wrong key", "wrong-key", false},
		{"empty key", "", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := svc.IsAdminKey(tt.key); got != tt.want {
				t.Errorf("IsAdminKey(%q) = %v, want %v", tt.key, got, tt.want)
			}
		})
	}
}

func TestService_IsAdminKey_NoAdminKey(t *testing.T) {
	svc := NewService(nil, "")

	if svc.IsAdminKey("anything") {
		t.Error("IsAdminKey should return false when no admin key is set")
	}
}

// Integration tests - require database
func TestService_Create(t *testing.T) {
	svc := newTestService(t, "admin-key")

	t.Run("creates key with valid scopes", func(t *testing.T) {
		resp, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      "test-key-1",
			Scopes:    []Scope{ScopeProjectsRead},
			ExpiresIn: 24 * time.Hour,
			CreatedBy: "test",
		})

		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}

		if resp.Secret == "" {
			t.Error("Create() returned empty secret")
		}

		if !ValidateKeyFormat(resp.Secret) {
			t.Errorf("Create() returned invalid key format: %q", resp.Secret)
		}

		if resp.Key.Name != "test-key-1" {
			t.Errorf("Key.Name = %q, want %q", resp.Key.Name, "test-key-1")
		}

		if len(resp.Key.Scopes) != 1 || resp.Key.Scopes[0] != ScopeProjectsRead {
			t.Errorf("Key.Scopes = %v, want [%v]", resp.Key.Scopes, ScopeProjectsRead)
		}

		if resp.Key.ExpiresAt == nil {
			t.Error("Key.ExpiresAt should not be nil")
		}
	})

	t.Run("rejects invalid scopes", func(t *testing.T) {
		_, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      "test-key-invalid",
			Scopes:    []Scope{Scope("invalid:scope")},
			CreatedBy: "test",
		})

		if err == nil {
			t.Error("Create() should reject invalid scopes")
		}
	})

	t.Run("creates key with no expiration", func(t *testing.T) {
		resp, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      "test-key-no-expire",
			Scopes:    []Scope{ScopeAdmin},
			ExpiresIn: 0,
			CreatedBy: "test",
		})

		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}

		if resp.Key.ExpiresAt != nil {
			t.Error("Key.ExpiresAt should be nil for no expiration")
		}
	})

	t.Run("creates key with project restrictions", func(t *testing.T) {
		resp, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:       "test-key-projects",
			Scopes:     []Scope{ScopeProjectsRead},
			ProjectIDs: []string{"proj-a", "proj-b"},
			ExpiresIn:  24 * time.Hour,
			CreatedBy:  "test",
		})

		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}

		if len(resp.Key.ProjectIDs) != 2 {
			t.Errorf("Key.ProjectIDs length = %d, want 2", len(resp.Key.ProjectIDs))
		}
	})
}

func TestService_Validate(t *testing.T) {
	svc := newTestService(t, "admin-key-test")

	t.Run("validates admin key", func(t *testing.T) {
		key, err := svc.Validate(context.Background(), "admin-key-test")
		if err != nil {
			t.Fatalf("Validate() error = %v", err)
		}

		if string(key.ID) != "admin" {
			t.Errorf("Key.ID = %q, want %q", key.ID, "admin")
		}

		if !HasScope(key.Scopes, ScopeAdmin) {
			t.Error("Admin key should have admin scope")
		}
	})

	t.Run("validates created key", func(t *testing.T) {
		resp, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      "test-validate-key",
			Scopes:    []Scope{ScopeProjectsRead, ScopeKeysRead},
			ExpiresIn: 24 * time.Hour,
			CreatedBy: "test",
		})
		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}

		key, err := svc.Validate(context.Background(), resp.Secret)
		if err != nil {
			t.Fatalf("Validate() error = %v", err)
		}

		if key.Name != "test-validate-key" {
			t.Errorf("Key.Name = %q, want %q", key.Name, "test-validate-key")
		}

		if len(key.Scopes) != 2 {
			t.Errorf("Key.Scopes length = %d, want 2", len(key.Scopes))
		}
	})

	t.Run("rejects unknown key", func(t *testing.T) {
		_, err := svc.Validate(context.Background(), "rdev_sk_abc12345_0123456789abcdef0123456789abcdef")
		if !errors.Is(err, ErrKeyNotFound) {
			t.Errorf("Validate() error = %v, want %v", err, ErrKeyNotFound)
		}
	})
}

func TestService_List(t *testing.T) {
	svc := newTestService(t, "admin-key")

	for i := 0; i < 3; i++ {
		_, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      fmt.Sprintf("test-list-key-%d", i),
			Scopes:    []Scope{ScopeProjectsRead},
			ExpiresIn: 24 * time.Hour,
			CreatedBy: "test",
		})
		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}
	}

	keys, err := svc.List(context.Background())
	if err != nil {
		t.Fatalf("List() error = %v", err)
	}

	testKeyCount := 0
	for _, k := range keys {
		if len(k.Name) >= 10 && k.Name[:10] == "test-list-" {
			testKeyCount++
		}
	}

	if testKeyCount != 3 {
		t.Errorf("List() returned %d test keys, want 3", testKeyCount)
	}
}

func TestService_Get(t *testing.T) {
	svc := newTestService(t, "admin-key")

	t.Run("gets existing key", func(t *testing.T) {
		resp, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      "test-get-key",
			Scopes:    []Scope{ScopeProjectsRead},
			ExpiresIn: 24 * time.Hour,
			CreatedBy: "test",
		})
		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}

		key, err := svc.Get(context.Background(), string(resp.Key.ID))
		if err != nil {
			t.Fatalf("Get() error = %v", err)
		}

		if key.Name != "test-get-key" {
			t.Errorf("Key.Name = %q, want %q", key.Name, "test-get-key")
		}
	})

	t.Run("returns error for unknown key", func(t *testing.T) {
		_, err := svc.Get(context.Background(), "00000000-0000-0000-0000-000000000000")
		if !errors.Is(err, ErrKeyNotFound) {
			t.Errorf("Get() error = %v, want %v", err, ErrKeyNotFound)
		}
	})
}

func TestService_Revoke(t *testing.T) {
	svc := newTestService(t, "admin-key")

	t.Run("revokes existing key", func(t *testing.T) {
		resp, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      "test-revoke-key",
			Scopes:    []Scope{ScopeProjectsRead},
			ExpiresIn: 24 * time.Hour,
			CreatedBy: "test",
		})
		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}

		err = svc.Revoke(context.Background(), string(resp.Key.ID))
		if err != nil {
			t.Fatalf("Revoke() error = %v", err)
		}

		_, err = svc.Validate(context.Background(), resp.Secret)
		if !errors.Is(err, ErrKeyRevoked) {
			t.Errorf("Validate() after revoke error = %v, want %v", err, ErrKeyRevoked)
		}
	})

	t.Run("returns error for unknown key", func(t *testing.T) {
		err := svc.Revoke(context.Background(), "00000000-0000-0000-0000-000000000000")
		if !errors.Is(err, ErrKeyNotFound) {
			t.Errorf("Revoke() error = %v, want %v", err, ErrKeyNotFound)
		}
	})

	t.Run("idempotent for already revoked", func(t *testing.T) {
		resp, err := svc.Create(context.Background(), CreateKeyRequest{
			Name:      "test-revoke-twice",
			Scopes:    []Scope{ScopeProjectsRead},
			ExpiresIn: 24 * time.Hour,
			CreatedBy: "test",
		})
		if err != nil {
			t.Fatalf("Create() error = %v", err)
		}

		if err := svc.Revoke(context.Background(), string(resp.Key.ID)); err != nil {
			t.Fatalf("First Revoke() error = %v", err)
		}

		err = svc.Revoke(context.Background(), string(resp.Key.ID))
		if !errors.Is(err, ErrKeyNotFound) {
			t.Errorf("Second Revoke() error = %v, want %v", err, ErrKeyNotFound)
		}
	})
}
