package auth

import (
	"testing"

	"github.com/jfraeys/fetch_ml/internal/auth"
)

func TestGenerateAPIKey(t *testing.T) {
	t.Parallel() // Enable parallel execution
	key1, hashed1, err := auth.GenerateAPIKey()
	if err != nil {
		t.Fatalf("Failed to generate API key: %v", err)
	}

	if len(key1) != 64 { // 32 bytes = 64 hex chars
		t.Errorf("Expected key length 64, got %d", len(key1))
	}

	if hashed1.Algorithm != "argon2id" {
		t.Errorf("Expected algorithm argon2id, got %s", hashed1.Algorithm)
	}

	// Test uniqueness
	key2, _, err := auth.GenerateAPIKey()
	if err != nil {
		t.Fatalf("Failed to generate second API key: %v", err)
	}

	if key1 == key2 {
		t.Error("Generated keys should be unique")
	}
}

func TestUserHasPermission(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name       string
		user       *auth.User
		permission string
		want       bool
	}{
		{
			name: "wildcard grants all",
			user: &auth.User{Permissions: map[string]bool{"*": true}},
			want: true,
		},
		{
			name:       "direct permission match",
			user:       &auth.User{Permissions: map[string]bool{"jobs:create": true}},
			permission: "jobs:create",
			want:       true,
		},
		{
			name:       "hierarchical permission match",
			user:       &auth.User{Permissions: map[string]bool{"jobs": true}},
			permission: "jobs:create",
			want:       true,
		},
		{
			name:       "missing permission",
			user:       &auth.User{Permissions: map[string]bool{"jobs:read": true}},
			permission: "jobs:create",
			want:       false,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if got := tt.user.HasPermission(tt.permission); got != tt.want {
				t.Fatalf("HasPermission(%q) = %v, want %v", tt.permission, got, tt.want)
			}
		})
	}
}

func TestUserHasRole(t *testing.T) {
	t.Parallel()
	user := &auth.User{
		Roles: []string{"admin", "data_scientist"},
	}

	if !user.HasRole("admin") {
		t.Fatal("expected admin role to be present")
	}
	if user.HasRole("operator") {
		t.Fatal("did not expect operator role to be present")
	}
}

// TestHashAPIKey_Legacy tests the legacy SHA256 hashing (kept for backward compatibility)
func TestHashAPIKey_Legacy(t *testing.T) {
	t.Parallel() // Enable parallel execution
	key := "test-key-123"
	hash := auth.HashAPIKey(key)

	if len(hash) != 64 { // SHA256 = 64 hex chars
		t.Errorf("Expected hash length 64, got %d", len(hash))
	}

	// Test consistency
	hash2 := auth.HashAPIKey(key)
	if hash != hash2 {
		t.Error("Hash should be consistent for same key")
	}

	// Test different keys produce different hashes
	hash3 := auth.HashAPIKey("different-key")
	if hash == hash3 {
		t.Error("Different keys should produce different hashes")
	}
}

// TestHashAPIKeyKnownValues_Legacy tests SHA256 with known hash values (backward compatibility)
func TestHashAPIKeyKnownValues_Legacy(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name     string
		key      string
		expected string
	}{
		{
			name:     "password hash",
			key:      "password",
			expected: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
		},
		{
			name:     "test hash",
			key:      "test",
			expected: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if got := auth.HashAPIKey(tt.key); got != tt.expected {
				t.Fatalf("HashAPIKey(%q) = %s, want %s", tt.key, got, tt.expected)
			}
		})
	}
}

// TestHashAPIKeyConsistency_Legacy tests SHA256 hash consistency (backward compatibility)
func TestHashAPIKeyConsistency_Legacy(t *testing.T) {
	t.Parallel()
	key := "consistency-key"
	hash1 := auth.HashAPIKey(key)
	hash2 := auth.HashAPIKey(key)

	if hash1 != hash2 {
		t.Fatalf("HashAPIKey() not deterministic: %s vs %s", hash1, hash2)
	}
	if len(hash1) != 64 {
		t.Fatalf("HashAPIKey() length = %d, want 64", len(hash1))
	}
}

// TestValidateAPIKey_Legacy tests API key validation using SHA256 hashes (backward compatibility)
func TestValidateAPIKey_Legacy(t *testing.T) {
	t.Parallel() // Enable parallel execution
	config := auth.Config{
		Enabled: true,
		APIKeys: map[auth.Username]auth.APIKeyEntry{
			"admin": {
				Hash:  auth.APIKeyHash(auth.HashAPIKey("admin-key")),
				Admin: true,
			},
			"data_scientist": {
				Hash:  auth.APIKeyHash(auth.HashAPIKey("ds-key")),
				Admin: false,
			},
		},
	}

	tests := []struct {
		name      string
		apiKey    string
		wantErr   bool
		wantUser  string
		wantAdmin bool
	}{
		{
			name:      "valid admin key",
			apiKey:    "admin-key",
			wantErr:   false,
			wantUser:  "admin",
			wantAdmin: true,
		},
		{
			name:      "valid user key",
			apiKey:    "ds-key",
			wantErr:   false,
			wantUser:  "data_scientist",
			wantAdmin: false,
		},
		{
			name:    "invalid key",
			apiKey:  "wrong-key",
			wantErr: true,
		},
		{
			name:    "empty key",
			apiKey:  "",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			user, err := config.ValidateAPIKey(tt.apiKey)

			if tt.wantErr {
				if err == nil {
					t.Error("Expected error but got none")
				}
				return
			}

			if err != nil {
				t.Errorf("Unexpected error: %v", err)
				return
			}

			if user.Name != tt.wantUser {
				t.Errorf("Expected user %s, got %s", tt.wantUser, user.Name)
			}

			if user.Admin != tt.wantAdmin {
				t.Errorf("Expected admin %v, got %v", tt.wantAdmin, user.Admin)
			}
		})
	}
}

func TestValidateAPIKeyAuthDisabled(t *testing.T) {
	t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "1")
	defer t.Setenv("FETCH_ML_ALLOW_INSECURE_AUTH", "")

	config := auth.Config{
		Enabled: false,
		APIKeys: map[auth.Username]auth.APIKeyEntry{}, // Empty
	}

	user, err := config.ValidateAPIKey("any-key")
	if err != nil {
		t.Errorf("Unexpected error when auth disabled: %v", err)
	}

	if user == nil {
		t.Fatal("Expected user, got nil")
	}

	if user.Name != "default" {
		t.Errorf("Expected default user, got %s", user.Name)
	}

	if !user.Admin {
		t.Error("Default user should be admin")
	}
}

func TestArgon2idHashing(t *testing.T) {
	t.Parallel()

	// Generate a key and hash it with Argon2id
	plaintext, hashed, err := auth.GenerateAPIKey()
	if err != nil {
		t.Fatalf("Failed to generate API key: %v", err)
	}

	// Verify algorithm is argon2id
	if hashed.Algorithm != "argon2id" {
		t.Errorf("Expected algorithm argon2id, got %s", hashed.Algorithm)
	}

	// Verify hash is hex-encoded and correct length (32 bytes = 64 hex chars)
	if len(hashed.Hash) != 64 {
		t.Errorf("Expected hash length 64, got %d", len(hashed.Hash))
	}

	// Verify salt is present and correct length (16 bytes = 32 hex chars)
	if len(hashed.Salt) != 32 {
		t.Errorf("Expected salt length 32, got %d", len(hashed.Salt))
	}

	// Build HashedKey for verification
	stored := &auth.HashedKey{
		Hash:      hashed.Hash,
		Salt:      hashed.Salt,
		Algorithm: hashed.Algorithm,
	}

	// Verify the key matches
	match, err := auth.VerifyAPIKey(plaintext, stored)
	if err != nil {
		t.Fatalf("VerifyAPIKey failed: %v", err)
	}
	if !match {
		t.Error("VerifyAPIKey should return true for matching key")
	}

	// Verify wrong key doesn't match
	match, err = auth.VerifyAPIKey("wrong-key", stored)
	if err != nil {
		t.Fatalf("VerifyAPIKey failed: %v", err)
	}
	if match {
		t.Error("VerifyAPIKey should return false for wrong key")
	}
}

func TestArgon2idDifferentSalts(t *testing.T) {
	t.Parallel()

	// Generate two keys - they should have different salts
	_, hashed1, _ := auth.GenerateAPIKey()
	_, hashed2, _ := auth.GenerateAPIKey()

	// Salts should be different
	if hashed1.Salt == hashed2.Salt {
		t.Error("Different API keys should have different salts")
	}

	// Hashes should be different even if same key (but they won't be same key)
	if hashed1.Hash == hashed2.Hash {
		t.Error("Different API keys should produce different hashes")
	}
}

func TestVerifyAPIKeyWithDifferentAlgorithms(t *testing.T) {
	t.Parallel()

	// Test Argon2id verification
	plaintext := "test-key-for-verification"
	hashedKey, err := auth.HashAPIKeyArgon2id(plaintext)
	if err != nil {
		t.Fatalf("Failed to hash with Argon2id: %v", err)
	}

	// Should verify successfully
	match, err := auth.VerifyAPIKey(plaintext, hashedKey)
	if err != nil {
		t.Fatalf("VerifyAPIKey failed: %v", err)
	}
	if !match {
		t.Error("Argon2id verification should succeed for correct key")
	}

	// Test with wrong algorithm
	sha256Stored := &auth.HashedKey{
		Hash:      auth.HashAPIKey(plaintext),
		Algorithm: "sha256",
	}

	match, err = auth.VerifyAPIKey(plaintext, sha256Stored)
	if err != nil {
		t.Fatalf("VerifyAPIKey with SHA256 failed: %v", err)
	}
	if !match {
		t.Error("SHA256 verification should succeed for correct key")
	}
}

func TestGenerateNewAPIKey(t *testing.T) {
	t.Parallel()

	plaintext, entry, err := auth.GenerateNewAPIKey(
		true,                          // admin
		[]string{"admin", "operator"}, // roles
		map[string]bool{"*": true},    // permissions
	)
	if err != nil {
		t.Fatalf("GenerateNewAPIKey failed: %v", err)
	}

	// Verify plaintext is valid
	if len(plaintext) != 64 {
		t.Errorf("Expected plaintext length 64, got %d", len(plaintext))
	}

	// Verify entry has correct values
	if !entry.Admin {
		t.Error("Expected Admin to be true")
	}

	if len(entry.Roles) != 2 || entry.Roles[0] != "admin" {
		t.Errorf("Expected roles [admin operator], got %v", entry.Roles)
	}

	if entry.Algorithm != "argon2id" {
		t.Errorf("Expected algorithm argon2id, got %s", entry.Algorithm)
	}

	// Verify the key can be validated
	config := auth.Config{
		Enabled: true,
		APIKeys: map[auth.Username]auth.APIKeyEntry{
			"testuser": entry,
		},
	}

	user, err := config.ValidateAPIKey(plaintext)
	if err != nil {
		t.Fatalf("Failed to validate generated key: %v", err)
	}

	if user.Name != "testuser" {
		t.Errorf("Expected user name testuser, got %s", user.Name)
	}

	if !user.Admin {
		t.Error("Expected user to be admin")
	}
}
