// ABOUTME: Comprehensive tests for EdgeConnect resource manager with deployment scenarios
// ABOUTME: Tests deployment execution, rollback functionality, and error handling with mock clients
package v2

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

	"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
	v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

// MockResourceClient extends MockEdgeConnectClient with resource management methods
type MockResourceClient struct {
	MockEdgeConnectClient
}

func (m *MockResourceClient) CreateApp(ctx context.Context, input *v2.NewAppInput) error {
	args := m.Called(ctx, input)
	return args.Error(0)
}

func (m *MockResourceClient) CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error {
	args := m.Called(ctx, input)
	return args.Error(0)
}

func (m *MockResourceClient) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error {
	args := m.Called(ctx, appKey, region)
	return args.Error(0)
}

func (m *MockResourceClient) UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error {
	args := m.Called(ctx, input)
	return args.Error(0)
}

func (m *MockResourceClient) UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error {
	args := m.Called(ctx, input)
	return args.Error(0)
}

func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error {
	args := m.Called(ctx, instanceKey, region)
	return args.Error(0)
}

// TestLogger implements Logger interface for testing
type TestLogger struct {
	messages []string
}

func (l *TestLogger) Printf(format string, v ...interface{}) {
	l.messages = append(l.messages, fmt.Sprintf(format, v...))
}

func TestNewResourceManager(t *testing.T) {
	mockClient := &MockResourceClient{}
	manager := NewResourceManager(mockClient)

	assert.NotNil(t, manager)
	assert.IsType(t, &EdgeConnectResourceManager{}, manager)
}

func TestDefaultResourceManagerOptions(t *testing.T) {
	opts := DefaultResourceManagerOptions()

	assert.Equal(t, 5, opts.ParallelLimit)
	assert.True(t, opts.RollbackOnFail)
	assert.Equal(t, 2*time.Minute, opts.OperationTimeout)
}

func TestWithOptions(t *testing.T) {
	mockClient := &MockResourceClient{}
	logger := &TestLogger{}

	manager := NewResourceManager(mockClient,
		WithParallelLimit(10),
		WithRollbackOnFail(false),
		WithLogger(logger),
	)

	// Cast to implementation to check options were applied
	impl := manager.(*EdgeConnectResourceManager)
	assert.Equal(t, 10, impl.parallelLimit)
	assert.False(t, impl.rollbackOnFail)
	assert.Equal(t, logger, impl.logger)
}

func createTestDeploymentPlan() *DeploymentPlan {
	return &DeploymentPlan{
		ConfigName: "test-deployment",
		AppAction: AppAction{
			Type: ActionCreate,
			Desired: &AppState{
				Name:         "test-app",
				Version:      "1.0.0",
				Organization: "testorg",
				Region:       "US",
			},
		},
		InstanceActions: []InstanceAction{
			{
				Type: ActionCreate,
				Target: config.InfraTemplate{
					Region:       "US",
					CloudletOrg:  "cloudletorg",
					CloudletName: "cloudlet1",
					FlavorName:   "small",
				},
				Desired: &InstanceState{
					Name:    "test-app-1.0.0-instance",
					AppName: "test-app",
				},
				InstanceName: "test-app-1.0.0-instance",
			},
		},
	}
}

func createTestManagerConfig(t *testing.T) *config.EdgeConnectConfig {
	// Create temporary manifest file
	tempDir := t.TempDir()
	manifestFile := filepath.Join(tempDir, "test-manifest.yaml")
	manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n  name: test\n"
	err := os.WriteFile(manifestFile, []byte(manifestContent), 0644)
	require.NoError(t, err)

	return &config.EdgeConnectConfig{
		Kind: "edgeconnect-deployment",
		Metadata: config.Metadata{
			Name:         "test-app",
			AppVersion:   "1.0.0",
			Organization: "testorg",
		},
		Spec: config.Spec{
			K8sApp: &config.K8sApp{
				ManifestFile: manifestFile,
			},
			InfraTemplate: []config.InfraTemplate{
				{
					Region:       "US",
					CloudletOrg:  "cloudletorg",
					CloudletName: "cloudlet1",
					FlavorName:   "small",
				},
			},
			Network: &config.NetworkConfig{
				OutboundConnections: []config.OutboundConnection{
					{
						Protocol:     "tcp",
						PortRangeMin: 80,
						PortRangeMax: 80,
						RemoteCIDR:   "0.0.0.0/0",
					},
				},
			},
		},
	}
}

// createTestStrategyConfig returns a fast configuration for tests
func createTestStrategyConfig() StrategyConfig {
	return StrategyConfig{
		MaxRetries:         0, // No retries for fast tests
		HealthCheckTimeout: 1 * time.Millisecond,
		ParallelOperations: false, // Sequential for predictable tests
		RetryDelay:         0,     // No delay
	}
}

func TestApplyDeploymentSuccess(t *testing.T) {
	mockClient := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))

	plan := createTestDeploymentPlan()
	config := createTestManagerConfig(t)

	// Mock successful operations
	mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
		Return(nil)
	mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
		Return(nil)

	ctx := context.Background()
	result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")

	require.NoError(t, err)
	require.NotNil(t, result)
	assert.True(t, result.Success)
	assert.Len(t, result.CompletedActions, 2) // 1 app + 1 instance
	assert.Len(t, result.FailedActions, 0)
	assert.False(t, result.RollbackPerformed)
	assert.Greater(t, result.Duration, time.Duration(0))

	// Check that operations were logged
	assert.Greater(t, len(logger.messages), 0)

	mockClient.AssertExpectations(t)
}

func TestApplyDeploymentAppFailure(t *testing.T) {
	mockClient := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))

	plan := createTestDeploymentPlan()
	config := createTestManagerConfig(t)

	// Mock app creation failure - deployment should stop here
	mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
		Return(&v2.APIError{StatusCode: 500, Messages: []string{"Server error"}})

	ctx := context.Background()
	result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")

	require.Error(t, err)
	require.NotNil(t, result)
	assert.False(t, result.Success)
	assert.Len(t, result.CompletedActions, 0)
	assert.Len(t, result.FailedActions, 1)
	assert.Contains(t, err.Error(), "Server error")

	mockClient.AssertExpectations(t)
}

func TestApplyDeploymentInstanceFailureWithRollback(t *testing.T) {
	mockClient := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockClient, WithLogger(logger), WithRollbackOnFail(true), WithStrategyConfig(createTestStrategyConfig()))

	plan := createTestDeploymentPlan()
	config := createTestManagerConfig(t)

	// Mock successful app creation but failed instance creation
	mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
		Return(nil)
	mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
		Return(&v2.APIError{StatusCode: 500, Messages: []string{"Instance creation failed"}})

	// Mock rollback operations
	mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(nil)

	ctx := context.Background()
	result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")

	require.Error(t, err)
	require.NotNil(t, result)
	assert.False(t, result.Success)
	assert.Len(t, result.CompletedActions, 1) // App was created
	assert.Len(t, result.FailedActions, 1)    // Instance failed
	assert.True(t, result.RollbackPerformed)
	assert.True(t, result.RollbackSuccess)
	assert.Contains(t, err.Error(), "failed to create instance")

	mockClient.AssertExpectations(t)
}

func TestApplyDeploymentNoActions(t *testing.T) {
	mockClient := &MockResourceClient{}
	manager := NewResourceManager(mockClient)

	// Create empty plan
	plan := &DeploymentPlan{
		ConfigName: "empty-plan",
		AppAction:  AppAction{Type: ActionNone},
	}
	config := createTestManagerConfig(t)

	ctx := context.Background()
	result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")

	require.Error(t, err)
	require.NotNil(t, result)
	assert.Contains(t, err.Error(), "deployment plan is empty")

	mockClient.AssertNotCalled(t, "CreateApp")
	mockClient.AssertNotCalled(t, "CreateAppInstance")
}

func TestApplyDeploymentMultipleInstances(t *testing.T) {
	mockClient := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockClient, WithLogger(logger), WithParallelLimit(2), WithStrategyConfig(createTestStrategyConfig()))

	// Create plan with multiple instances
	plan := &DeploymentPlan{
		ConfigName: "multi-instance",
		AppAction: AppAction{
			Type: ActionCreate,
			Desired: &AppState{
				Name:         "test-app",
				Version:      "1.0.0",
				Organization: "testorg",
				Region:       "US",
			},
		},
		InstanceActions: []InstanceAction{
			{
				Type: ActionCreate,
				Target: config.InfraTemplate{
					Region:       "US",
					CloudletOrg:  "cloudletorg1",
					CloudletName: "cloudlet1",
					FlavorName:   "small",
				},
				Desired:      &InstanceState{Name: "instance1"},
				InstanceName: "instance1",
			},
			{
				Type: ActionCreate,
				Target: config.InfraTemplate{
					Region:       "EU",
					CloudletOrg:  "cloudletorg2",
					CloudletName: "cloudlet2",
					FlavorName:   "medium",
				},
				Desired:      &InstanceState{Name: "instance2"},
				InstanceName: "instance2",
			},
		},
	}

	config := createTestManagerConfig(t)

	// Mock successful operations
	mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
		Return(nil)
	mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
		Return(nil)

	ctx := context.Background()
	result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")

	require.NoError(t, err)
	require.NotNil(t, result)
	assert.True(t, result.Success)
	assert.Len(t, result.CompletedActions, 3) // 1 app + 2 instances
	assert.Len(t, result.FailedActions, 0)

	mockClient.AssertExpectations(t)
}

func TestValidatePrerequisites(t *testing.T) {
	mockClient := &MockResourceClient{}
	manager := NewResourceManager(mockClient)

	tests := []struct {
		name    string
		plan    *DeploymentPlan
		wantErr bool
		errMsg  string
	}{
		{
			name: "valid plan",
			plan: &DeploymentPlan{
				ConfigName: "test",
				AppAction:  AppAction{Type: ActionCreate, Desired: &AppState{}},
			},
			wantErr: false,
		},
		{
			name: "empty plan",
			plan: &DeploymentPlan{
				ConfigName: "test",
				AppAction:  AppAction{Type: ActionNone},
			},
			wantErr: true,
			errMsg:  "deployment plan is empty",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			err := manager.ValidatePrerequisites(ctx, tt.plan)

			if tt.wantErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestRollbackDeployment(t *testing.T) {
	mockClient := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))

	// Create result with completed actions
	plan := createTestDeploymentPlan()
	result := &ExecutionResult{
		Plan: plan,
		CompletedActions: []ActionResult{
			{
				Type:    ActionCreate,
				Target:  "test-app",
				Success: true,
			},
			{
				Type:    ActionCreate,
				Target:  "test-app-1.0.0-instance",
				Success: true,
			},
		},
		FailedActions: []ActionResult{},
	}

	// Mock rollback operations
	mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
		Return(nil)
	mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(nil)

	ctx := context.Background()
	err := manager.RollbackDeployment(ctx, result)

	require.NoError(t, err)
	mockClient.AssertExpectations(t)

	// Check rollback was logged
	assert.Greater(t, len(logger.messages), 0)
}

func TestRollbackDeploymentFailure(t *testing.T) {
	mockClient := &MockResourceClient{}
	manager := NewResourceManager(mockClient)

	plan := createTestDeploymentPlan()
	result := &ExecutionResult{
		Plan: plan,
		CompletedActions: []ActionResult{
			{
				Type:    ActionCreate,
				Target:  "test-app",
				Success: true,
			},
		},
	}

	// Mock rollback failure
	mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(&v2.APIError{StatusCode: 500, Messages: []string{"Delete failed"}})

	ctx := context.Background()
	err := manager.RollbackDeployment(ctx, result)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "rollback encountered")
	mockClient.AssertExpectations(t)
}

func TestRollbackDeploymentWithRestore(t *testing.T) {
	mockClient := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))

	plan := createTestDeploymentPlan()

	// Simulate a RecreateStrategy scenario:
	// 1. Old app and instance were deleted and backed up
	// 2. New app was created successfully
	// 3. New instance creation failed
	// 4. Rollback should: delete new app, restore old app, restore old instance
	oldApp := v2.App{
		Key: v2.AppKey{
			Organization: "test-org",
			Name:         "test-app",
			Version:      "1.0.0",
		},
		Deployment:         "kubernetes",
		DeploymentManifest: "old-manifest-content",
	}

	oldInstance := v2.AppInstance{
		Key: v2.AppInstanceKey{
			Organization: "test-org",
			Name:         "test-app-1.0.0-instance",
			CloudletKey: v2.CloudletKey{
				Organization: "test-cloudlet-org",
				Name:         "test-cloudlet",
			},
		},
		AppKey: v2.AppKey{
			Organization: "test-org",
			Name:         "test-app",
			Version:      "1.0.0",
		},
		Flavor: v2.Flavor{Name: "small"},
	}

	result := &ExecutionResult{
		Plan: plan,
		// Completed actions: new app was created before failure
		CompletedActions: []ActionResult{
			{
				Type:    ActionCreate,
				Target:  "test-app",
				Success: true,
			},
		},
		// Failed action: new instance creation failed
		FailedActions: []ActionResult{
			{
				Type:    ActionCreate,
				Target:  "test-app-1.0.0-instance",
				Success: false,
			},
		},
		// Backup of deleted resources
		DeletedAppBackup: &AppBackup{
			App:             oldApp,
			Region:          "US",
			ManifestContent: "old-manifest-content",
		},
		DeletedInstancesBackup: []InstanceBackup{
			{
				Instance: oldInstance,
				Region:   "US",
			},
		},
	}

	// Mock rollback operations in order:
	// 1. Delete newly created app (rollback create)
	mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(nil).Once()

	// 2. Restore old app (from backup)
	mockClient.On("CreateApp", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInput) bool {
		return input.App.Key.Name == "test-app" && input.App.DeploymentManifest == "old-manifest-content"
	})).Return(nil).Once()

	// 3. Restore old instance (from backup)
	mockClient.On("CreateAppInstance", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInstanceInput) bool {
		return input.AppInst.Key.Name == "test-app-1.0.0-instance"
	})).Return(nil).Once()

	ctx := context.Background()
	err := manager.RollbackDeployment(ctx, result)

	require.NoError(t, err)
	mockClient.AssertExpectations(t)

	// Verify rollback was logged
	assert.Greater(t, len(logger.messages), 0)
	// Should have messages about rolling back created resources and restoring deleted resources
	hasRestoreLog := false
	for _, msg := range logger.messages {
		if strings.Contains(msg, "Restoring deleted resources") {
			hasRestoreLog = true
			break
		}
	}
	assert.True(t, hasRestoreLog, "Should log restoration of deleted resources")
}

func TestConvertNetworkRules(t *testing.T) {
	network := &config.NetworkConfig{
		OutboundConnections: []config.OutboundConnection{
			{
				Protocol:     "tcp",
				PortRangeMin: 80,
				PortRangeMax: 80,
				RemoteCIDR:   "0.0.0.0/0",
			},
			{
				Protocol:     "tcp",
				PortRangeMin: 443,
				PortRangeMax: 443,
				RemoteCIDR:   "10.0.0.0/8",
			},
		},
	}

	rules := convertNetworkRules(network)
	require.Len(t, rules, 2)

	assert.Equal(t, "tcp", rules[0].Protocol)
	assert.Equal(t, 80, rules[0].PortRangeMin)
	assert.Equal(t, 80, rules[0].PortRangeMax)
	assert.Equal(t, "0.0.0.0/0", rules[0].RemoteCIDR)

	assert.Equal(t, "tcp", rules[1].Protocol)
	assert.Equal(t, 443, rules[1].PortRangeMin)
	assert.Equal(t, 443, rules[1].PortRangeMax)
	assert.Equal(t, "10.0.0.0/8", rules[1].RemoteCIDR)
}
