// Package permission provides the core permission management logic for AutoSSO.
// This file contains unit tests for the Lifecycle component.
package permission

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

	"AutoSSO/internal/config"
	"AutoSSO/internal/pkg/logger"
	"AutoSSO/internal/state"

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

// lifecycleMockStateStore is a mock implementation of state.Store for lifecycle testing.
type lifecycleMockStateStore struct {
	instances        map[string]*state.ApprovalInstance
	pendingRemovals  map[int64]*state.PendingRemoval
	deletedInstances map[string]bool
	mu               sync.RWMutex
	nextRemovalID    int64
}

func newLifecycleMockStateStore() *lifecycleMockStateStore {
	return &lifecycleMockStateStore{
		instances:        make(map[string]*state.ApprovalInstance),
		pendingRemovals:  make(map[int64]*state.PendingRemoval),
		deletedInstances: make(map[string]bool),
		nextRemovalID:    1,
	}
}

func (m *lifecycleMockStateStore) SaveInstance(ctx context.Context, instance *state.ApprovalInstance) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.instances[instance.ID] = instance
	return nil
}

func (m *lifecycleMockStateStore) GetInstance(ctx context.Context, instanceID string) (*state.ApprovalInstance, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.instances[instanceID], nil
}

func (m *lifecycleMockStateStore) ListInstances(ctx context.Context, filter *state.InstanceFilter) ([]*state.ApprovalInstance, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	var result []*state.ApprovalInstance
	for _, inst := range m.instances {
		if filter == nil || filter.Status == "" || inst.Status == filter.Status {
			result = append(result, inst)
		}
	}
	return result, nil
}

func (m *lifecycleMockStateStore) DeleteInstance(ctx context.Context, instanceID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.instances, instanceID)
	return nil
}

func (m *lifecycleMockStateStore) SavePendingRemoval(ctx context.Context, removal *state.PendingRemoval) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if removal.ID == 0 {
		removal.ID = m.nextRemovalID
		m.nextRemovalID++
	}
	m.pendingRemovals[removal.ID] = removal
	return nil
}

func (m *lifecycleMockStateStore) GetPendingRemovals(ctx context.Context) ([]*state.PendingRemoval, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	var result []*state.PendingRemoval
	for _, r := range m.pendingRemovals {
		result = append(result, r)
	}
	return result, nil
}

func (m *lifecycleMockStateStore) DeletePendingRemoval(ctx context.Context, id int64) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.pendingRemovals, id)
	return nil
}

func (m *lifecycleMockStateStore) SaveDeletedInstance(ctx context.Context, instanceID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.deletedInstances[instanceID] = true
	return nil
}

func (m *lifecycleMockStateStore) IsInstanceDeleted(ctx context.Context, instanceID string) (bool, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.deletedInstances[instanceID], nil
}

func (m *lifecycleMockStateStore) Close() error {
	return nil
}

// createLifecycleTestConfig creates a test configuration for lifecycle tests.
func createLifecycleTestConfig() *config.Config {
	return &config.Config{
		Runtime: config.RuntimeConfig{
			GracePeriodDays:     3,
			ExpireCheckInterval: 1 * time.Hour,
			PollInterval:        5 * time.Second,
		},
		Feishu: config.FeishuConfig{
			ChatID: "test-chat-id",
		},
	}
}

// createLifecycleTestLogger creates a test logger for lifecycle tests.
func createLifecycleTestLogger() *logger.Logger {
	return logger.NewLogger(logger.Config{
		Level:       logger.DebugLevel,
		PrettyPrint: false,
	})
}

func TestNewLifecycle(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	stateStore := newLifecycleMockStateStore()

	lifecycle := NewLifecycle(nil, nil, stateStore, cfg, log)

	assert.NotNil(t, lifecycle)
	assert.NotNil(t, lifecycle.stats)
	assert.Equal(t, stateStore, lifecycle.state)
	assert.Equal(t, cfg, lifecycle.config)
}

func TestLifecycle_Stop_Idempotent(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	assert.NotPanics(t, func() {
		lifecycle.Stop()
		lifecycle.Stop()
	})
}

func TestLifecycle_RecordPermissionAdded(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	// Record some permission additions
	lifecycle.RecordPermissionAdded("AWS", "DBaaS-Prod-ViewOnly-Role")
	lifecycle.RecordPermissionAdded("AWS", "DBaaS-Prod-ViewOnly-Role")
	lifecycle.RecordPermissionAdded("GCP", "DBaaS-Prod-DevUser-Role")
	lifecycle.RecordPermissionAdded("Azure", "DBaaS-Prod-Admin-Role")
	lifecycle.RecordPermissionAdded("Alibaba", "DBaaS-Prod-Billing-Role")

	stats := lifecycle.GetStats()

	// Verify AWS stats
	assert.Equal(t, 2, stats.AWS.TotalAdded)
	assert.Equal(t, 2, stats.AWS.AddedByPermission["DBaaS-Prod-ViewOnly-Role"])

	// Verify GCP stats
	assert.Equal(t, 1, stats.GCP.TotalAdded)
	assert.Equal(t, 1, stats.GCP.AddedByPermission["DBaaS-Prod-DevUser-Role"])

	// Verify Azure stats
	assert.Equal(t, 1, stats.Azure.TotalAdded)
	assert.Equal(t, 1, stats.Azure.AddedByPermission["DBaaS-Prod-Admin-Role"])

	// Verify Alibaba stats
	assert.Equal(t, 1, stats.Alibaba.TotalAdded)
	assert.Equal(t, 1, stats.Alibaba.AddedByPermission["DBaaS-Prod-Billing-Role"])
}

func TestLifecycle_RecordPermissionRemoved(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	// Record some permission removals
	lifecycle.RecordPermissionRemoved("AWS", "DBaaS-Prod-ViewOnly-Role", false)
	lifecycle.RecordPermissionRemoved("AWS", "CustomRole", true) // Non-general
	lifecycle.RecordPermissionRemoved("GCP", "DBaaS-Prod-DevUser-Role", false)
	lifecycle.RecordPermissionRemoved("Azure", "DBaaS-Prod-Admin-Role", false)
	lifecycle.RecordPermissionRemoved("Alibaba", "DBaaS-Prod-Billing-Role", false)

	stats := lifecycle.GetStats()

	// Verify AWS stats
	assert.Equal(t, 2, stats.AWS.TotalRemoved)
	assert.Equal(t, 1, stats.AWS.RemovedByPermission["DBaaS-Prod-ViewOnly-Role"])
	assert.Equal(t, 1, stats.AWS.NonGeneralRemoved)

	// Verify GCP stats
	assert.Equal(t, 1, stats.GCP.TotalRemoved)
	assert.Equal(t, 1, stats.GCP.RemovedByPermission["DBaaS-Prod-DevUser-Role"])

	// Verify Azure stats
	assert.Equal(t, 1, stats.Azure.TotalRemoved)
	assert.Equal(t, 1, stats.Azure.RemovedByPermission["DBaaS-Prod-Admin-Role"])

	// Verify Alibaba stats
	assert.Equal(t, 1, stats.Alibaba.TotalRemoved)
	assert.Equal(t, 1, stats.Alibaba.RemovedByPermission["DBaaS-Prod-Billing-Role"])
}

func TestLifecycle_ResetStats(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	// Record some stats
	lifecycle.RecordPermissionAdded("AWS", "DBaaS-Prod-ViewOnly-Role")
	lifecycle.RecordPermissionRemoved("GCP", "DBaaS-Prod-DevUser-Role", false)

	// Verify stats are recorded
	stats := lifecycle.GetStats()
	assert.Equal(t, 1, stats.AWS.TotalAdded)
	assert.Equal(t, 1, stats.GCP.TotalRemoved)

	// Reset stats
	lifecycle.ResetStats()

	// Verify stats are reset
	stats = lifecycle.GetStats()
	assert.Equal(t, 0, stats.AWS.TotalAdded)
	assert.Equal(t, 0, stats.GCP.TotalRemoved)
}

func TestLifecycle_GetStats_ReturnsCopy(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	lifecycle.RecordPermissionAdded("AWS", "DBaaS-Prod-ViewOnly-Role")

	// Get stats
	stats1 := lifecycle.GetStats()
	stats2 := lifecycle.GetStats()

	// Modify stats1
	stats1.AWS.TotalAdded = 999
	stats1.AWS.AddedByPermission["test"] = 123

	// Verify stats2 is not affected
	assert.Equal(t, 1, stats2.AWS.TotalAdded)
	assert.Equal(t, 0, stats2.AWS.AddedByPermission["test"])
}

func TestLifecycle_GetGracePeriodDays(t *testing.T) {
	tests := []struct {
		name     string
		config   *config.Config
		expected int
	}{
		{
			name:     "nil config returns default",
			config:   nil,
			expected: 3,
		},
		{
			name: "configured grace period",
			config: &config.Config{
				Runtime: config.RuntimeConfig{
					GracePeriodDays: 5,
				},
			},
			expected: 5,
		},
		{
			name: "zero grace period",
			config: &config.Config{
				Runtime: config.RuntimeConfig{
					GracePeriodDays: 0,
				},
			},
			expected: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			log := createLifecycleTestLogger()
			lifecycle := NewLifecycle(nil, nil, nil, tt.config, log)
			assert.Equal(t, tt.expected, lifecycle.GetGracePeriodDays())
		})
	}
}

func TestLifecycle_CalculateScheduledRemovalTime(t *testing.T) {
	cfg := &config.Config{
		Runtime: config.RuntimeConfig{
			GracePeriodDays: 3,
		},
	}
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	expiredAt := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
	expected := time.Date(2024, 1, 18, 10, 0, 0, 0, time.UTC) // 3 days later

	result := lifecycle.CalculateScheduledRemovalTime(expiredAt)
	assert.Equal(t, expected, result)
}

func TestLifecycle_IsPermissionReadyForRemoval(t *testing.T) {
	cfg := &config.Config{
		Runtime: config.RuntimeConfig{
			GracePeriodDays: 3,
		},
	}
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	tests := []struct {
		name      string
		expiredAt time.Time
		expected  bool
	}{
		{
			name:      "expired 4 days ago - ready",
			expiredAt: time.Now().Add(-4 * 24 * time.Hour),
			expected:  true,
		},
		{
			name:      "expired 3 days ago - ready (exactly at grace period)",
			expiredAt: time.Now().Add(-3*24*time.Hour - time.Hour), // A bit more than 3 days
			expected:  true,
		},
		{
			name:      "expired 2 days ago - not ready",
			expiredAt: time.Now().Add(-2 * 24 * time.Hour),
			expected:  false,
		},
		{
			name:      "expired 1 day ago - not ready",
			expiredAt: time.Now().Add(-1 * 24 * time.Hour),
			expected:  false,
		},
		{
			name:      "not expired yet - not ready",
			expiredAt: time.Now().Add(1 * 24 * time.Hour),
			expected:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := lifecycle.IsPermissionReadyForRemoval(tt.expiredAt)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestLifecycle_isNonGeneralPermissionSet(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	tests := []struct {
		name          string
		cloudType     string
		permissionSet string
		expected      bool
	}{
		{
			name:          "AWS general permission set",
			cloudType:     "AWS",
			permissionSet: "DBaaS-Prod-ViewOnly-Role",
			expected:      false,
		},
		{
			name:          "AWS non-general permission set",
			cloudType:     "AWS",
			permissionSet: "CustomRole",
			expected:      true,
		},
		{
			name:          "GCP permission set - always general",
			cloudType:     "GCP",
			permissionSet: "CustomRole",
			expected:      false,
		},
		{
			name:          "Azure permission set - always general",
			cloudType:     "Azure",
			permissionSet: "CustomRole",
			expected:      false,
		},
		{
			name:          "Alibaba permission set - always general",
			cloudType:     "Alibaba",
			permissionSet: "CustomRole",
			expected:      false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := lifecycle.isNonGeneralPermissionSet(tt.cloudType, tt.permissionSet)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestLifecycle_ProcessExpiredPermissions(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	stateStore := newLifecycleMockStateStore()

	// Create a mock manager
	manager := &Manager{
		config: cfg,
		logger: log.WithModule("test-manager"),
		state:  stateStore,
	}

	lifecycle := NewLifecycle(manager, nil, stateStore, cfg, log)

	// Add an expired instance
	expiredInstance := &state.ApprovalInstance{
		ID:             "expired-instance-1",
		Email:          "user@example.com",
		AccountID:      "123456789",
		PermissionSets: []string{"DBaaS-Prod-ViewOnly-Role"},
		StartTime:      time.Now().Add(-10 * 24 * time.Hour),
		EndTime:        time.Now().Add(-5 * 24 * time.Hour), // Expired 5 days ago
		Status:         state.StatusApproved,
		CloudType:      "AWS",
	}
	err := stateStore.SaveInstance(context.Background(), expiredInstance)
	require.NoError(t, err)

	// Add a non-expired instance
	activeInstance := &state.ApprovalInstance{
		ID:             "active-instance-1",
		Email:          "user2@example.com",
		AccountID:      "987654321",
		PermissionSets: []string{"DBaaS-Prod-Admin-Role"},
		StartTime:      time.Now().Add(-1 * 24 * time.Hour),
		EndTime:        time.Now().Add(5 * 24 * time.Hour), // Expires in 5 days
		Status:         state.StatusApproved,
		CloudType:      "GCP",
	}
	err = stateStore.SaveInstance(context.Background(), activeInstance)
	require.NoError(t, err)

	// Process expired permissions
	err = lifecycle.ProcessExpiredPermissions(context.Background())
	require.NoError(t, err)

	// Verify expired instance status was updated
	updatedInstance, err := stateStore.GetInstance(context.Background(), "expired-instance-1")
	require.NoError(t, err)
	assert.Equal(t, state.StatusExpired, updatedInstance.Status)

	// Verify active instance status was not changed
	activeUpdated, err := stateStore.GetInstance(context.Background(), "active-instance-1")
	require.NoError(t, err)
	assert.Equal(t, state.StatusApproved, activeUpdated.Status)

	// Verify pending removal was created
	removals, err := stateStore.GetPendingRemovals(context.Background())
	require.NoError(t, err)
	assert.Len(t, removals, 1)
	assert.Equal(t, "user@example.com", removals[0].Email)
}

func TestLifecycle_ProcessExpiredPermissions_NoStateStore(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()

	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	err := lifecycle.ProcessExpiredPermissions(context.Background())
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "state store is not configured")
}

func TestLifecycle_ProcessExpiredPermissions_ContextCancelled(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	stateStore := newLifecycleMockStateStore()

	manager := &Manager{
		config: cfg,
		logger: log.WithModule("test-manager"),
		state:  stateStore,
	}

	lifecycle := NewLifecycle(manager, nil, stateStore, cfg, log)

	// Add multiple expired instances
	for i := 0; i < 5; i++ {
		instance := &state.ApprovalInstance{
			ID:             fmt.Sprintf("expired-instance-%d", i),
			Email:          fmt.Sprintf("user%d@example.com", i),
			AccountID:      "123456789",
			PermissionSets: []string{"DBaaS-Prod-ViewOnly-Role"},
			StartTime:      time.Now().Add(-10 * 24 * time.Hour),
			EndTime:        time.Now().Add(-5 * 24 * time.Hour),
			Status:         state.StatusApproved,
			CloudType:      "AWS",
		}
		_ = stateStore.SaveInstance(context.Background(), instance)
	}

	// Create a cancelled context
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	err := lifecycle.ProcessExpiredPermissions(ctx)
	assert.Error(t, err)
	assert.Equal(t, context.Canceled, err)
}

func TestLifecycle_ProcessScheduledRemovals_NoManager(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()

	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	processed, errs := lifecycle.ProcessScheduledRemovals(context.Background())
	assert.Equal(t, 0, processed)
	assert.Len(t, errs, 1)
	assert.Contains(t, errs[0].Error(), "permission manager is not configured")
}

func TestLifecycle_SendDailyReport_NoNotificationService(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()

	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	err := lifecycle.SendDailyReport(context.Background())
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "notification service is not configured")
}

func TestLifecycle_StartDailyReportScheduler_NoNotificationService(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()

	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	err := lifecycle.StartDailyReportScheduler(context.Background())
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "notification service is not configured")
}

func TestLifecycle_Stop(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()

	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	// Stop should not panic
	assert.NotPanics(t, func() {
		lifecycle.Stop()
	})
}

func TestLifecycle_ConcurrentStatistics(t *testing.T) {
	cfg := createLifecycleTestConfig()
	log := createLifecycleTestLogger()
	lifecycle := NewLifecycle(nil, nil, nil, cfg, log)

	// Run concurrent operations
	var wg sync.WaitGroup
	numGoroutines := 10
	numOperations := 100

	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < numOperations; j++ {
				cloudType := []string{"AWS", "GCP", "Azure", "Alibaba"}[j%4]
				if j%2 == 0 {
					lifecycle.RecordPermissionAdded(cloudType, "TestRole")
				} else {
					lifecycle.RecordPermissionRemoved(cloudType, "TestRole", false)
				}
			}
		}(i)
	}

	// Also read stats concurrently
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < numOperations; j++ {
				_ = lifecycle.GetStats()
			}
		}()
	}

	wg.Wait()

	// Verify no data race occurred and stats are consistent
	stats := lifecycle.GetStats()
	totalAdded := stats.AWS.TotalAdded + stats.GCP.TotalAdded + stats.Azure.TotalAdded + stats.Alibaba.TotalAdded
	totalRemoved := stats.AWS.TotalRemoved + stats.GCP.TotalRemoved + stats.Azure.TotalRemoved + stats.Alibaba.TotalRemoved

	// Each goroutine does numOperations, half are adds and half are removes
	expectedTotal := numGoroutines * numOperations
	assert.Equal(t, expectedTotal, totalAdded+totalRemoved)
}
