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

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

	"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
	"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
	"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driven/edgeconnect"
	"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 {
	mock.Mock
}

func (m *MockResourceClient) CreateApp(ctx context.Context, region string, app *domain.App) error {
	args := m.Called(ctx, region, app)
	return args.Error(0)
}

func (m *MockResourceClient) ShowApp(ctx context.Context, region string, appKey domain.AppKey) (*domain.App, error) {
	args := m.Called(ctx, region, appKey)
	return args.Get(0).(*domain.App), args.Error(1)
}

func (m *MockResourceClient) ShowApps(ctx context.Context, region string, appKey domain.AppKey) ([]domain.App, error) {
	args := m.Called(ctx, region, appKey)
	return args.Get(0).([]domain.App), args.Error(1)
}

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

func (m *MockResourceClient) UpdateApp(ctx context.Context, region string, app *domain.App) error {
	args := m.Called(ctx, region, app)
	return args.Error(0)
}

func (m *MockResourceClient) CreateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
	args := m.Called(ctx, region, appInst)
	return args.Error(0)
}

func (m *MockResourceClient) ShowAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) (*domain.AppInstance, error) {
	args := m.Called(ctx, region, appInstKey)
	return args.Get(0).(*domain.AppInstance), args.Error(1)
}

func (m *MockResourceClient) ShowAppInstances(ctx context.Context, region string, appInstKey domain.AppInstanceKey) ([]domain.AppInstance, error) {
	args := m.Called(ctx, region, appInstKey)
	return args.Get(0).([]domain.AppInstance), args.Error(1)
}

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

func (m *MockResourceClient) UpdateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
	args := m.Called(ctx, region, appInst)
	return args.Error(0)
}

func (m *MockResourceClient) RefreshAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
	args := m.Called(ctx, region, appInstKey)
	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) {
	mockAppRepo := &MockResourceClient{}
	mockAppInstRepo := &MockResourceClient{}
	manager := NewResourceManager(mockAppRepo, mockAppInstRepo)

	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) {
	mockAppRepo := &MockResourceClient{}
	mockAppInstRepo := &MockResourceClient{}
	logger := &TestLogger{}

	manager := NewResourceManager(mockAppRepo, mockAppInstRepo,
		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) {
	mockAppRepo := &MockResourceClient{}
	mockAppInstRepo := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockAppRepo, mockAppInstRepo, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))

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

	// Mock successful operations
	mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
		Return(nil)
	mockAppInstRepo.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.AppInstance")).
		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)

	mockAppRepo.AssertExpectations(t)
	mockAppInstRepo.AssertExpectations(t)
}

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

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

	// Mock app creation failure - deployment should stop here
	mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
		Return(&edgeconnect.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")

	mockAppRepo.AssertExpectations(t)
	mockAppInstRepo.AssertExpectations(t)
}

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

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

	// Mock successful app creation but failed instance creation
	mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
		Return(nil)
	mockAppInstRepo.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.AppInstance")).
		Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Instance creation failed"}})

	// Mock rollback operations
	mockAppRepo.On("DeleteApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppKey")).
		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")

	mockAppRepo.AssertExpectations(t)
	mockAppInstRepo.AssertExpectations(t)
}

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

	// 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")

	mockAppRepo.AssertNotCalled(t, "CreateApp")
	mockAppInstRepo.AssertNotCalled(t, "CreateAppInstance")
}

func TestApplyDeploymentMultipleInstances(t *testing.T) {
	mockAppRepo := &MockResourceClient{}
	mockAppInstRepo := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockAppRepo, mockAppInstRepo, 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
	mockAppRepo.On("CreateApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.App")).
		Return(nil)
	mockAppInstRepo.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("*domain.AppInstance")).
		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)

	mockAppRepo.AssertExpectations(t)
	mockAppInstRepo.AssertExpectations(t)
}

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

	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) {
	mockAppRepo := &MockResourceClient{}
	mockAppInstRepo := &MockResourceClient{}
	logger := &TestLogger{}
	manager := NewResourceManager(mockAppRepo, mockAppInstRepo, 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
	mockAppInstRepo.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppInstanceKey")).
		Return(nil)
	mockAppRepo.On("DeleteApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppKey")).
		Return(nil)

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

	require.NoError(t, err)
	mockAppRepo.AssertExpectations(t)
	mockAppInstRepo.AssertExpectations(t)

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

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

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

	// Mock rollback failure
	mockAppRepo.On("DeleteApp", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("domain.AppKey")).
		Return(&edgeconnect.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")
	mockAppRepo.AssertExpectations(t)
	mockAppInstRepo.AssertExpectations(t)
}

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)
}
