// ABOUTME: Comprehensive tests for EdgeConnect deployment planner with mock scenarios
// ABOUTME: Tests planning logic, state comparison, and various deployment scenarios
package v2

import (
	"context"
	"os"
	"path/filepath"
	"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"
)

// MockEdgeConnectClient is a mock implementation of the EdgeConnect client
type MockEdgeConnectClient struct {
	mock.Mock
}

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

func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error) {
	args := m.Called(ctx, instanceKey, region)
	if args.Get(0) == nil {
		return v2.AppInstance{}, args.Error(1)
	}
	return args.Get(0).(v2.AppInstance), args.Error(1)
}

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

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

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

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

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

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

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

func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
	args := m.Called(ctx, instanceKey, region)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]v2.AppInstance), args.Error(1)
}

func TestNewPlanner(t *testing.T) {
	mockClient := &MockEdgeConnectClient{}
	planner := NewPlanner(mockClient)

	assert.NotNil(t, planner)
	assert.IsType(t, &EdgeConnectPlanner{}, planner)
}

func TestDefaultPlanOptions(t *testing.T) {
	opts := DefaultPlanOptions()

	assert.False(t, opts.DryRun)
	assert.False(t, opts.Force)
	assert.False(t, opts.SkipStateCheck)
	assert.True(t, opts.ParallelQueries)
	assert.Equal(t, 30*time.Second, opts.Timeout)
}

func createTestConfig(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:  "TestCloudletOrg",
					CloudletName: "TestCloudlet",
					FlavorName:   "small",
				},
			},
			Network: &config.NetworkConfig{
				OutboundConnections: []config.OutboundConnection{
					{
						Protocol:     "tcp",
						PortRangeMin: 80,
						PortRangeMax: 80,
						RemoteCIDR:   "0.0.0.0/0",
					},
				},
			},
		},
	}
}

func TestPlanNewDeployment(t *testing.T) {
	mockClient := &MockEdgeConnectClient{}
	planner := NewPlanner(mockClient)
	testConfig := createTestConfig(t)

	// Mock API calls to return "not found" errors
	mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})

	mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
		Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})

	ctx := context.Background()
	result, err := planner.Plan(ctx, testConfig)

	require.NoError(t, err)
	require.NotNil(t, result)
	require.NotNil(t, result.Plan)
	require.NoError(t, result.Error)

	plan := result.Plan
	assert.Equal(t, "test-app", plan.ConfigName)
	assert.Equal(t, ActionCreate, plan.AppAction.Type)
	assert.Equal(t, "Application does not exist", plan.AppAction.Reason)

	require.Len(t, plan.InstanceActions, 1)
	assert.Equal(t, ActionCreate, plan.InstanceActions[0].Type)
	assert.Equal(t, "Instance does not exist", plan.InstanceActions[0].Reason)

	assert.Equal(t, 2, plan.TotalActions) // 1 app + 1 instance
	assert.False(t, plan.IsEmpty())

	mockClient.AssertExpectations(t)
}

func TestPlanExistingDeploymentNoChanges(t *testing.T) {
	mockClient := &MockEdgeConnectClient{}
	planner := NewPlanner(mockClient)
	testConfig := createTestConfig(t)

	// Note: We would calculate expected manifest hash here when API supports it

	// Mock existing app with same manifest hash and outbound connections
	manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n  name: test\n"
	existingApp := &v2.App{
		Key: v2.AppKey{
			Organization: "testorg",
			Name:         "test-app",
			Version:      "1.0.0",
		},
		Deployment:         "kubernetes",
		DeploymentManifest: manifestContent,
		RequiredOutboundConnections: []v2.SecurityRule{
			{
				Protocol:     "tcp",
				PortRangeMin: 80,
				PortRangeMax: 80,
				RemoteCIDR:   "0.0.0.0/0",
			},
		},
		// Note: Manifest hash tracking would be implemented when API supports annotations
	}

	// Mock existing instance
	existingInstance := &v2.AppInstance{
		Key: v2.AppInstanceKey{
			Organization: "testorg",
			Name:         "test-app-1.0.0-instance",
			CloudletKey: v2.CloudletKey{
				Organization: "TestCloudletOrg",
				Name:         "TestCloudlet",
			},
		},
		AppKey: v2.AppKey{
			Organization: "testorg",
			Name:         "test-app",
			Version:      "1.0.0",
		},
		Flavor: v2.Flavor{
			Name: "small",
		},
		State: "Ready",
	}

	mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(*existingApp, nil)

	mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
		Return(*existingInstance, nil)

	ctx := context.Background()
	result, err := planner.Plan(ctx, testConfig)

	require.NoError(t, err)
	require.NotNil(t, result)
	require.NotNil(t, result.Plan)

	plan := result.Plan
	assert.Equal(t, ActionNone, plan.AppAction.Type)
	assert.Len(t, plan.InstanceActions, 1)
	assert.Equal(t, ActionNone, plan.InstanceActions[0].Type)
	assert.Equal(t, 0, plan.TotalActions)
	assert.True(t, plan.IsEmpty())
	assert.Contains(t, plan.Summary, "No changes required")

	mockClient.AssertExpectations(t)
}

func TestPlanWithOptions(t *testing.T) {
	mockClient := &MockEdgeConnectClient{}
	planner := NewPlanner(mockClient)
	testConfig := createTestConfig(t)

	opts := PlanOptions{
		DryRun:         true,
		SkipStateCheck: true,
		Timeout:        10 * time.Second,
	}

	ctx := context.Background()
	result, err := planner.PlanWithOptions(ctx, testConfig, opts)

	require.NoError(t, err)
	require.NotNil(t, result)
	require.NotNil(t, result.Plan)

	plan := result.Plan
	assert.True(t, plan.DryRun)
	assert.Equal(t, ActionCreate, plan.AppAction.Type)
	assert.Contains(t, plan.AppAction.Reason, "state check skipped")

	// No API calls should be made when SkipStateCheck is true
	mockClient.AssertNotCalled(t, "ShowApp")
	mockClient.AssertNotCalled(t, "ShowAppInstance")
}

func TestPlanMultipleInfrastructures(t *testing.T) {
	mockClient := &MockEdgeConnectClient{}
	planner := NewPlanner(mockClient)
	testConfig := createTestConfig(t)

	// Add a second infrastructure target
	testConfig.Spec.InfraTemplate = append(testConfig.Spec.InfraTemplate, config.InfraTemplate{
		Region:       "EU",
		CloudletOrg:  "EUCloudletOrg",
		CloudletName: "EUCloudlet",
		FlavorName:   "medium",
	})

	// Mock API calls to return "not found" errors
	mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})

	mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
		Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})

	mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "EU").
		Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})

	ctx := context.Background()
	result, err := planner.Plan(ctx, testConfig)

	require.NoError(t, err)
	require.NotNil(t, result)
	require.NotNil(t, result.Plan)

	plan := result.Plan
	assert.Equal(t, ActionCreate, plan.AppAction.Type)

	// Should have 2 instance actions, one for each infrastructure
	require.Len(t, plan.InstanceActions, 2)
	assert.Equal(t, ActionCreate, plan.InstanceActions[0].Type)
	assert.Equal(t, ActionCreate, plan.InstanceActions[1].Type)

	assert.Equal(t, 3, plan.TotalActions) // 1 app + 2 instances

	// Test cloudlet and region aggregation
	cloudlets := plan.GetTargetCloudlets()
	regions := plan.GetTargetRegions()
	assert.Len(t, cloudlets, 2)
	assert.Len(t, regions, 2)

	mockClient.AssertExpectations(t)
}

func TestCalculateManifestHash(t *testing.T) {
	planner := &EdgeConnectPlanner{}
	tempDir := t.TempDir()

	// Create test file
	testFile := filepath.Join(tempDir, "test.yaml")
	content := "test content for hashing"
	err := os.WriteFile(testFile, []byte(content), 0644)
	require.NoError(t, err)

	hash1, err := planner.calculateManifestHash(testFile)
	require.NoError(t, err)
	assert.NotEmpty(t, hash1)
	assert.Len(t, hash1, 64) // SHA256 hex string length

	// Same content should produce same hash
	hash2, err := planner.calculateManifestHash(testFile)
	require.NoError(t, err)
	assert.Equal(t, hash1, hash2)

	// Different content should produce different hash
	err = os.WriteFile(testFile, []byte("different content"), 0644)
	require.NoError(t, err)

	hash3, err := planner.calculateManifestHash(testFile)
	require.NoError(t, err)
	assert.NotEqual(t, hash1, hash3)

	// Empty file path should return empty hash
	hash4, err := planner.calculateManifestHash("")
	require.NoError(t, err)
	assert.Empty(t, hash4)

	// Non-existent file should return error
	_, err = planner.calculateManifestHash("/non/existent/file")
	assert.Error(t, err)
}

func TestCompareAppStates(t *testing.T) {
	planner := &EdgeConnectPlanner{}

	current := &AppState{
		Name:         "test-app",
		Version:      "1.0.0",
		AppType:      AppTypeK8s,
		ManifestHash: "old-hash",
	}

	desired := &AppState{
		Name:         "test-app",
		Version:      "1.0.0",
		AppType:      AppTypeK8s,
		ManifestHash: "new-hash",
	}

	changes, manifestChanged := planner.compareAppStates(current, desired)
	assert.Len(t, changes, 1)
	assert.True(t, manifestChanged)
	assert.Contains(t, changes[0], "Manifest hash changed")

	// Test no changes
	desired.ManifestHash = "old-hash"
	changes, manifestChanged = planner.compareAppStates(current, desired)
	assert.Empty(t, changes)
	assert.False(t, manifestChanged)

	// Test app type change
	desired.AppType = AppTypeDocker
	changes, manifestChanged = planner.compareAppStates(current, desired)
	assert.Len(t, changes, 1)
	assert.False(t, manifestChanged)
	assert.Contains(t, changes[0], "App type changed")
}

func TestCompareAppStatesOutboundConnections(t *testing.T) {
	planner := &EdgeConnectPlanner{}

	// Test with no outbound connections
	current := &AppState{
		Name:                "test-app",
		Version:             "1.0.0",
		AppType:             AppTypeK8s,
		OutboundConnections: nil,
	}

	desired := &AppState{
		Name:                "test-app",
		Version:             "1.0.0",
		AppType:             AppTypeK8s,
		OutboundConnections: nil,
	}

	changes, _ := planner.compareAppStates(current, desired)
	assert.Empty(t, changes, "No changes expected when both have no outbound connections")

	// Test adding outbound connections
	desired.OutboundConnections = []SecurityRule{
		{
			Protocol:     "tcp",
			PortRangeMin: 80,
			PortRangeMax: 80,
			RemoteCIDR:   "0.0.0.0/0",
		},
	}

	changes, _ = planner.compareAppStates(current, desired)
	assert.Len(t, changes, 1)
	assert.Contains(t, changes[0], "Outbound connections changed")

	// Test identical outbound connections
	current.OutboundConnections = []SecurityRule{
		{
			Protocol:     "tcp",
			PortRangeMin: 80,
			PortRangeMax: 80,
			RemoteCIDR:   "0.0.0.0/0",
		},
	}

	changes, _ = planner.compareAppStates(current, desired)
	assert.Empty(t, changes, "No changes expected when outbound connections are identical")

	// Test different outbound connections (different port)
	desired.OutboundConnections[0].PortRangeMin = 443
	desired.OutboundConnections[0].PortRangeMax = 443

	changes, _ = planner.compareAppStates(current, desired)
	assert.Len(t, changes, 1)
	assert.Contains(t, changes[0], "Outbound connections changed")

	// Test same connections but different order (should be considered equal)
	current.OutboundConnections = []SecurityRule{
		{
			Protocol:     "tcp",
			PortRangeMin: 80,
			PortRangeMax: 80,
			RemoteCIDR:   "0.0.0.0/0",
		},
		{
			Protocol:     "tcp",
			PortRangeMin: 443,
			PortRangeMax: 443,
			RemoteCIDR:   "0.0.0.0/0",
		},
	}

	desired.OutboundConnections = []SecurityRule{
		{
			Protocol:     "tcp",
			PortRangeMin: 443,
			PortRangeMax: 443,
			RemoteCIDR:   "0.0.0.0/0",
		},
		{
			Protocol:     "tcp",
			PortRangeMin: 80,
			PortRangeMax: 80,
			RemoteCIDR:   "0.0.0.0/0",
		},
	}

	changes, _ = planner.compareAppStates(current, desired)
	assert.Empty(t, changes, "No changes expected when outbound connections are same but in different order")

	// Test removing outbound connections
	desired.OutboundConnections = nil

	changes, _ = planner.compareAppStates(current, desired)
	assert.Len(t, changes, 1)
	assert.Contains(t, changes[0], "Outbound connections changed")
}

func TestCompareInstanceStates(t *testing.T) {
	planner := &EdgeConnectPlanner{}

	current := &InstanceState{
		Name:         "test-instance",
		FlavorName:   "small",
		CloudletName: "oldcloudlet",
		CloudletOrg:  "oldorg",
	}

	desired := &InstanceState{
		Name:         "test-instance",
		FlavorName:   "medium",
		CloudletName: "newcloudlet",
		CloudletOrg:  "neworg",
	}

	changes := planner.compareInstanceStates(current, desired)
	assert.Len(t, changes, 3)
	assert.Contains(t, changes[0], "Flavor changed")
	assert.Contains(t, changes[1], "Cloudlet changed")
	assert.Contains(t, changes[2], "Cloudlet org changed")

	// Test no changes
	desired.FlavorName = "small"
	desired.CloudletName = "oldcloudlet"
	desired.CloudletOrg = "oldorg"
	changes = planner.compareInstanceStates(current, desired)
	assert.Empty(t, changes)
}

func TestDeploymentPlanMethods(t *testing.T) {
	plan := &DeploymentPlan{
		ConfigName: "test-plan",
		AppAction: AppAction{
			Type:    ActionCreate,
			Desired: &AppState{Name: "test-app"},
		},
		InstanceActions: []InstanceAction{
			{
				Type: ActionCreate,
				Target: config.InfraTemplate{
					CloudletOrg:  "org1",
					CloudletName: "cloudlet1",
					Region:       "US",
				},
				InstanceName: "instance1",
				Desired:      &InstanceState{Name: "instance1"},
			},
			{
				Type: ActionUpdate,
				Target: config.InfraTemplate{
					CloudletOrg:  "org2",
					CloudletName: "cloudlet2",
					Region:       "EU",
				},
				InstanceName: "instance2",
				Desired:      &InstanceState{Name: "instance2"},
			},
		},
	}

	// Test IsEmpty
	assert.False(t, plan.IsEmpty())

	// Test GetTargetCloudlets
	cloudlets := plan.GetTargetCloudlets()
	assert.Len(t, cloudlets, 2)
	assert.Contains(t, cloudlets, "org1:cloudlet1")
	assert.Contains(t, cloudlets, "org2:cloudlet2")

	// Test GetTargetRegions
	regions := plan.GetTargetRegions()
	assert.Len(t, regions, 2)
	assert.Contains(t, regions, "US")
	assert.Contains(t, regions, "EU")

	// Test GenerateSummary
	summary := plan.GenerateSummary()
	assert.Contains(t, summary, "test-plan")
	assert.Contains(t, summary, "CREATE application")
	assert.Contains(t, summary, "CREATE 1 instance")
	assert.Contains(t, summary, "UPDATE 1 instance")

	// Test Validate
	err := plan.Validate()
	assert.NoError(t, err)

	// Test validation failure
	plan.AppAction.Desired = nil
	err = plan.Validate()
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "must have desired state")
}

func TestEstimateDeploymentDuration(t *testing.T) {
	planner := &EdgeConnectPlanner{}

	plan := &DeploymentPlan{
		AppAction: AppAction{Type: ActionCreate},
		InstanceActions: []InstanceAction{
			{Type: ActionCreate},
			{Type: ActionUpdate},
		},
	}

	duration := planner.estimateDeploymentDuration(plan)
	assert.Greater(t, duration, time.Duration(0))
	assert.Less(t, duration, 10*time.Minute) // Reasonable upper bound

	// Test with no actions
	emptyPlan := &DeploymentPlan{
		AppAction:       AppAction{Type: ActionNone},
		InstanceActions: []InstanceAction{},
	}

	emptyDuration := planner.estimateDeploymentDuration(emptyPlan)
	assert.Greater(t, emptyDuration, time.Duration(0))
	assert.Less(t, emptyDuration, duration) // Should be less than plan with actions
}

func TestIsResourceNotFoundError(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		expected bool
	}{
		{"nil error", nil, false},
		{"not found error", &v2.APIError{StatusCode: 404, Messages: []string{"Resource not found"}}, true},
		{"does not exist error", &v2.APIError{Messages: []string{"App does not exist"}}, true},
		{"404 in message", &v2.APIError{Messages: []string{"HTTP 404 error"}}, true},
		{"other error", &v2.APIError{StatusCode: 500, Messages: []string{"Server error"}}, false},
	}

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

func TestPlanErrorHandling(t *testing.T) {
	mockClient := &MockEdgeConnectClient{}
	planner := NewPlanner(mockClient)
	testConfig := createTestConfig(t)

	// Mock API call to return a non-404 error
	mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
		Return(nil, &v2.APIError{StatusCode: 500, Messages: []string{"Server error"}})

	ctx := context.Background()
	result, err := planner.Plan(ctx, testConfig)

	assert.Error(t, err)
	assert.NotNil(t, result)
	assert.NotNil(t, result.Error)
	assert.Contains(t, err.Error(), "failed to query current app state")

	mockClient.AssertExpectations(t)
}
