// Package gcp provides the Google Cloud Platform provider implementation.
// This file contains unit tests for the GCP provider.
//
// Requirements: 3.2, 3.3, 3.4, 3.6
package gcp

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"AutoSSO/internal/config"
	"AutoSSO/internal/provider"

	"google.golang.org/api/cloudresourcemanager/v3"
	"google.golang.org/api/option"
)

// TestNewGCPProvider tests the GCP provider constructor.
func TestNewGCPProvider(t *testing.T) {
	tests := []struct {
		name        string
		config      *config.GCPConfig
		expectError bool
		errorMsg    string
	}{
		{
			name:        "nil config",
			config:      nil,
			expectError: true,
			errorMsg:    "GCP config cannot be nil",
		},
		{
			name: "empty credentials file",
			config: &config.GCPConfig{
				CredentialsFile: "",
			},
			expectError: true,
			errorMsg:    "GCP credentials file is required",
		},
		{
			name: "non-existent credentials file",
			config: &config.GCPConfig{
				CredentialsFile: "/non/existent/path/credentials.json",
			},
			expectError: true,
			// The error will be from the GCP SDK about invalid credentials
		},
	}

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

			if tt.expectError {
				assert.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
				assert.Nil(t, provider)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, provider)
			}
		})
	}
}

func TestNewGCPProvider_UsesInitTimeout(t *testing.T) {
	originalNewCRMService := newCRMService
	defer func() {
		newCRMService = originalNewCRMService
	}()

	var deadline time.Time
	var hasDeadline bool
	newCRMService = func(ctx context.Context, _ ...option.ClientOption) (*cloudresourcemanager.Service, error) {
		deadline, hasDeadline = ctx.Deadline()
		return &cloudresourcemanager.Service{}, nil
	}

	cfg := &config.GCPConfig{
		CredentialsFile: "/tmp/fake-credentials.json",
	}
	p, err := NewGCPProvider(cfg)
	require.NoError(t, err)
	require.NotNil(t, p)
	require.True(t, hasDeadline, "expected context deadline to be set")
	if time.Until(deadline) <= 0 {
		t.Fatalf("expected deadline in the future, got %v", deadline)
	}
}

// TestGCPProviderName tests the Name method.
func TestGCPProviderName(t *testing.T) {
	// Create a minimal provider for testing (without actual GCP connection)
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
	}

	assert.Equal(t, string(provider.ProviderGCP), p.Name())
	assert.Equal(t, "GCP", p.Name())
}

// TestGCPProviderGetUserID tests the GetUserID method.
func TestGCPProviderGetUserID(t *testing.T) {
	// Create a minimal provider for testing
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
	}

	tests := []struct {
		name        string
		email       string
		expectError bool
		errorMsg    string
		expectedID  string
	}{
		{
			name:        "valid email",
			email:       "user@example.com",
			expectError: false,
			expectedID:  "user@example.com",
		},
		{
			name:        "email with whitespace",
			email:       "  user@example.com  ",
			expectError: false,
			expectedID:  "user@example.com",
		},
		{
			name:        "empty email",
			email:       "",
			expectError: true,
			errorMsg:    "email is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			userID, err := p.GetUserID(ctx, tt.email)

			if tt.expectError {
				assert.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expectedID, userID)
			}
		})
	}
}

// TestGCPProviderGetUserIDClosed tests GetUserID when provider is closed.
func TestGCPProviderGetUserIDClosed(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
		closed:           true,
	}

	ctx := context.Background()
	_, err := p.GetUserID(ctx, "user@example.com")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "provider is closed")
}

// TestGCPProviderClose tests the Close method.
func TestGCPProviderClose(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
	}

	// First close should succeed
	err := p.Close()
	assert.NoError(t, err)
	assert.True(t, p.closed)

	// Second close should also succeed (idempotent)
	err = p.Close()
	assert.NoError(t, err)
}

// TestGCPProviderLoadRoleMappings tests the role mapping loading functionality.
func TestGCPProviderLoadRoleMappings(t *testing.T) {
	// Create a temporary INI file for testing
	tempDir := t.TempDir()
	iniPath := filepath.Join(tempDir, "test_role_mapping.ini")

	iniContent := `[DBaaS-Prod-ViewOnly-Role]
roles = roles/viewer|roles/browser

[DBaaS-Prod-Admin-Role]
roles = roles/editor|roles/iam.securityAdmin

[SingleRole]
roles = roles/compute.viewer
`

	err := os.WriteFile(iniPath, []byte(iniContent), 0644)
	require.NoError(t, err)

	p := &GCPProvider{
		config: &config.GCPConfig{
			RoleMappingFile: iniPath,
		},
		roleMappingCache: make(map[string][]string),
	}

	// Load role mappings
	err = p.loadRoleMappings()
	assert.NoError(t, err)

	// Verify the mappings
	cache := p.GetRoleMappingCache()

	assert.Contains(t, cache, "DBaaS-Prod-ViewOnly-Role")
	assert.Equal(t, []string{"roles/viewer", "roles/browser"}, cache["DBaaS-Prod-ViewOnly-Role"])

	assert.Contains(t, cache, "DBaaS-Prod-Admin-Role")
	assert.Equal(t, []string{"roles/editor", "roles/iam.securityAdmin"}, cache["DBaaS-Prod-Admin-Role"])

	assert.Contains(t, cache, "SingleRole")
	assert.Equal(t, []string{"roles/compute.viewer"}, cache["SingleRole"])
}

// TestGCPProviderGetRoleMappings tests the getRoleMappings method.
func TestGCPProviderGetRoleMappings(t *testing.T) {
	// Create a temporary INI file for testing
	tempDir := t.TempDir()
	iniPath := filepath.Join(tempDir, "test_role_mapping.ini")

	iniContent := `[TestRole]
roles = roles/viewer|roles/browser
`

	err := os.WriteFile(iniPath, []byte(iniContent), 0644)
	require.NoError(t, err)

	p := &GCPProvider{
		config: &config.GCPConfig{
			RoleMappingFile: iniPath,
		},
		roleMappingCache: make(map[string][]string),
	}

	// Test getting a mapped role
	roles, err := p.getRoleMappings("TestRole")
	assert.NoError(t, err)
	assert.Equal(t, []string{"roles/viewer", "roles/browser"}, roles)

	// Test getting an unmapped role (should return the role name as-is)
	roles, err = p.getRoleMappings("UnmappedRole")
	assert.NoError(t, err)
	assert.Equal(t, []string{"UnmappedRole"}, roles)
}

// TestGCPProviderReloadRoleMappings tests the ReloadRoleMappings method.
func TestGCPProviderReloadRoleMappings(t *testing.T) {
	// Create a temporary INI file for testing
	tempDir := t.TempDir()
	iniPath := filepath.Join(tempDir, "test_role_mapping.ini")

	// Initial content
	iniContent := `[TestRole]
roles = roles/viewer
`
	err := os.WriteFile(iniPath, []byte(iniContent), 0644)
	require.NoError(t, err)

	p := &GCPProvider{
		config: &config.GCPConfig{
			RoleMappingFile: iniPath,
		},
		roleMappingCache: make(map[string][]string),
	}

	// Load initial mappings
	err = p.loadRoleMappings()
	assert.NoError(t, err)

	cache := p.GetRoleMappingCache()
	assert.Equal(t, []string{"roles/viewer"}, cache["TestRole"])

	// Update the INI file
	updatedContent := `[TestRole]
roles = roles/editor|roles/admin
`
	err = os.WriteFile(iniPath, []byte(updatedContent), 0644)
	require.NoError(t, err)

	// Reload mappings
	err = p.ReloadRoleMappings()
	assert.NoError(t, err)

	cache = p.GetRoleMappingCache()
	assert.Equal(t, []string{"roles/editor", "roles/admin"}, cache["TestRole"])
}

// TestGCPProviderReloadRoleMappingsClosed tests ReloadRoleMappings when provider is closed.
func TestGCPProviderReloadRoleMappingsClosed(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
		closed:           true,
	}

	err := p.ReloadRoleMappings()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "provider is closed")
}

// TestGCPProviderAddPermissionValidation tests AddPermission input validation.
func TestGCPProviderAddPermissionValidation(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
	}

	ctx := context.Background()

	tests := []struct {
		name        string
		req         *provider.PermissionRequest
		expectError bool
		errorMsg    string
	}{
		{
			name:        "nil request",
			req:         nil,
			expectError: true,
			errorMsg:    "permission request cannot be nil",
		},
		{
			name: "empty project ID",
			req: &provider.PermissionRequest{
				AccountID:      "",
				UserEmail:      "user@example.com",
				PermissionSets: []string{"role1"},
			},
			expectError: true,
			errorMsg:    "project ID is required",
		},
		{
			name: "empty user email",
			req: &provider.PermissionRequest{
				AccountID:      "project-id",
				UserEmail:      "",
				PermissionSets: []string{"role1"},
			},
			expectError: true,
			errorMsg:    "user email is required",
		},
		{
			name: "empty permission sets",
			req: &provider.PermissionRequest{
				AccountID:      "project-id",
				UserEmail:      "user@example.com",
				PermissionSets: []string{},
			},
			expectError: true,
			errorMsg:    "at least one permission set is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := p.AddPermission(ctx, tt.req)

			if tt.expectError {
				assert.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
			} else {
				// Note: This test only validates input, actual GCP calls would fail
				// without proper credentials
			}
		})
	}
}

// TestGCPProviderAddPermissionClosed tests AddPermission when provider is closed.
func TestGCPProviderAddPermissionClosed(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
		closed:           true,
	}

	ctx := context.Background()
	req := &provider.PermissionRequest{
		AccountID:      "project-id",
		UserEmail:      "user@example.com",
		PermissionSets: []string{"role1"},
	}

	err := p.AddPermission(ctx, req)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "provider is closed")
}

// TestGCPProviderRemovePermissionValidation tests RemovePermission input validation.
func TestGCPProviderRemovePermissionValidation(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
	}

	ctx := context.Background()

	tests := []struct {
		name        string
		req         *provider.PermissionRequest
		expectError bool
		errorMsg    string
	}{
		{
			name:        "nil request",
			req:         nil,
			expectError: true,
			errorMsg:    "permission request cannot be nil",
		},
		{
			name: "empty project ID",
			req: &provider.PermissionRequest{
				AccountID:      "",
				UserEmail:      "user@example.com",
				PermissionSets: []string{"role1"},
			},
			expectError: true,
			errorMsg:    "project ID is required",
		},
		{
			name: "empty user email",
			req: &provider.PermissionRequest{
				AccountID:      "project-id",
				UserEmail:      "",
				PermissionSets: []string{"role1"},
			},
			expectError: true,
			errorMsg:    "user email is required",
		},
		{
			name: "empty permission sets",
			req: &provider.PermissionRequest{
				AccountID:      "project-id",
				UserEmail:      "user@example.com",
				PermissionSets: []string{},
			},
			expectError: true,
			errorMsg:    "at least one permission set is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := p.RemovePermission(ctx, tt.req)

			if tt.expectError {
				assert.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
			} else {
				// Note: This test only validates input, actual GCP calls would fail
				// without proper credentials
			}
		})
	}
}

// TestGCPProviderRemovePermissionClosed tests RemovePermission when provider is closed.
func TestGCPProviderRemovePermissionClosed(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
		closed:           true,
	}

	ctx := context.Background()
	req := &provider.PermissionRequest{
		AccountID:      "project-id",
		UserEmail:      "user@example.com",
		PermissionSets: []string{"role1"},
	}

	err := p.RemovePermission(ctx, req)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "provider is closed")
}

// TestGCPProviderValidateCredentialsClosed tests ValidateCredentials when provider is closed.
func TestGCPProviderValidateCredentialsClosed(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
		closed:           true,
	}

	ctx := context.Background()
	err := p.ValidateCredentials(ctx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "provider is closed")
}

// TestGCPProviderValidateCredentialsNoProjects tests ValidateCredentials with no projects configured.
func TestGCPProviderValidateCredentialsNoProjects(t *testing.T) {
	p := &GCPProvider{
		config: &config.GCPConfig{
			Projects: []config.CloudAccount{},
		},
		roleMappingCache: make(map[string][]string),
	}

	ctx := context.Background()
	err := p.ValidateCredentials(ctx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "no GCP projects configured for validation")
}

// TestGCPProviderAddRoleBindingDirectClosed tests AddRoleBindingDirect when provider is closed.
func TestGCPProviderAddRoleBindingDirectClosed(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
		closed:           true,
	}

	ctx := context.Background()
	err := p.AddRoleBindingDirect(ctx, "project-id", "user@example.com", "roles/viewer")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "provider is closed")
}

// TestGCPProviderRemoveRoleBindingDirectClosed tests RemoveRoleBindingDirect when provider is closed.
func TestGCPProviderRemoveRoleBindingDirectClosed(t *testing.T) {
	p := &GCPProvider{
		config:           &config.GCPConfig{},
		roleMappingCache: make(map[string][]string),
		closed:           true,
	}

	ctx := context.Background()
	err := p.RemoveRoleBindingDirect(ctx, "project-id", "user@example.com", "roles/viewer")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "provider is closed")
}

// TestGCPProviderImplementsInterface verifies that GCPProvider implements the Provider interface.
func TestGCPProviderImplementsInterface(t *testing.T) {
	// This is a compile-time check
	var _ provider.Provider = (*GCPProvider)(nil)
}

// TestGCPProviderLoadRoleMappingsEmptyFile tests loading from an empty role mapping file.
func TestGCPProviderLoadRoleMappingsEmptyFile(t *testing.T) {
	p := &GCPProvider{
		config: &config.GCPConfig{
			RoleMappingFile: "",
		},
		roleMappingCache: make(map[string][]string),
	}

	// Should not error with empty file path
	err := p.loadRoleMappings()
	assert.NoError(t, err)
}

// TestGCPProviderLoadRoleMappingsNonExistentFile tests loading from a non-existent file.
func TestGCPProviderLoadRoleMappingsNonExistentFile(t *testing.T) {
	p := &GCPProvider{
		config: &config.GCPConfig{
			RoleMappingFile: "/non/existent/path/roles.ini",
		},
		roleMappingCache: make(map[string][]string),
	}

	err := p.loadRoleMappings()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to load role mapping file")
}

// TestGCPProviderGetRoleMappingsCacheHit tests that cached mappings are returned.
func TestGCPProviderGetRoleMappingsCacheHit(t *testing.T) {
	p := &GCPProvider{
		config: &config.GCPConfig{
			RoleMappingFile: "",
		},
		roleMappingCache: map[string][]string{
			"CachedRole": {"roles/cached1", "roles/cached2"},
		},
	}

	roles, err := p.getRoleMappings("CachedRole")
	assert.NoError(t, err)
	assert.Equal(t, []string{"roles/cached1", "roles/cached2"}, roles)
}
