package ack

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/bucket/task"
	"github.com/tidbcloud/infra-provider/regional-server/test/kubemock"
	"go.uber.org/mock/gomock"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
)

func TestBucketPolicy_makeOssBucketPolicy(t *testing.T) {
	tests := []struct {
		name                   string
		bucket                 *v1alpha1.Bucket
		currentDataPlaneCR     *v1alpha1.DataPlane
		expectedPolicyKeys     []string
		expectedStatementCount int
		expectError            bool
		setupMocks             func(*kubemock.MockClient)
	}{
		{
			name: "success with no default statement and no granted dataplanes",
			bucket: &v1alpha1.Bucket{
				Spec: v1alpha1.BucketSpec{
					GrantedDataplaneIds: []string{},
				},
				Status: v1alpha1.BucketStatus{
					Bucket: &v1alpha1.Reference{
						Name: "test-bucket",
					},
				},
			},
			currentDataPlaneCR: &v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name:              "test-datapane",
					DeletionTimestamp: &metav1.Time{}, // This will make IsDefaultStatementInBucketPolicyNeeded return false
				},
			},
			expectedPolicyKeys:     []string{"Version", "Statement"},
			expectedStatementCount: 0,
			expectError:            false,
			setupMocks: func(mockClient *kubemock.MockClient) {
				// No mock expectations needed since no statements will be created
			},
		},
		{
			name: "success with default statement needed and no granted dataplanes",
			bucket: &v1alpha1.Bucket{
				Spec: v1alpha1.BucketSpec{
					GrantedDataplaneIds: []string{},
				},
				Status: v1alpha1.BucketStatus{
					Bucket: &v1alpha1.Reference{
						Name: "test-bucket",
					},
				},
			},
			currentDataPlaneCR: &v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-datapane",
					// No DeletionTimestamp, so IsDefaultStatementInBucketPolicyNeeded will return true
				},
			},
			expectedPolicyKeys:     []string{"Version", "Statement"},
			expectedStatementCount: 1, // One statement for the default dataplane
			expectError:            false,
			setupMocks: func(mockClient *kubemock.MockClient) {
				// Mock DataPlane Get call
				mockClient.EXPECT().Get(
					gomock.Any(),
					types.NamespacedName{Namespace: "test-namespace", Name: "test-datapane"},
					gomock.Any(),
				).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
					if dataPlane, ok := obj.(*v1alpha1.DataPlane); ok {
						dataPlane.Name = key.Name
						dataPlane.Namespace = key.Namespace
						dataPlane.Status.AppBox = &v1alpha1.Reference{
							Name: "test-appbox",
						}
					}
					return nil
				})

				// Mock APPBox Get call
				mockClient.EXPECT().Get(
					gomock.Any(),
					types.NamespacedName{Namespace: "test-namespace", Name: "test-appbox"},
					gomock.Any(),
				).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
					if appBox, ok := obj.(*v1alpha1.APPBox); ok {
						appBox.Name = key.Name
						appBox.Namespace = key.Namespace
						appBox.Status.IAM.NodeRoleName = "test-role"
					}
					return nil
				})
			},
		},
		{
			name: "success with no default statement and granted dataplanes",
			bucket: &v1alpha1.Bucket{
				Spec: v1alpha1.BucketSpec{
					GrantedDataplaneIds: []string{"granted-dp-1", "granted-dp-2"},
				},
				Status: v1alpha1.BucketStatus{
					Bucket: &v1alpha1.Reference{
						Name: "test-bucket",
					},
				},
			},
			currentDataPlaneCR: &v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name:              "test-datapane",
					DeletionTimestamp: &metav1.Time{}, // This will make IsDefaultStatementInBucketPolicyNeeded return false
				},
			},
			expectedPolicyKeys:     []string{"Version", "Statement"},
			expectedStatementCount: 2, // Two statements for granted dataplanes
			expectError:            false,
			setupMocks: func(mockClient *kubemock.MockClient) {
				// Mock DataPlane Get calls for granted dataplanes
				for _, dpId := range []string{"granted-dp-1", "granted-dp-2"} {
					mockClient.EXPECT().Get(
						gomock.Any(),
						types.NamespacedName{Namespace: "test-namespace", Name: dpId},
						gomock.Any(),
					).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
						if dataPlane, ok := obj.(*v1alpha1.DataPlane); ok {
							dataPlane.Name = key.Name
							dataPlane.Namespace = key.Namespace
							dataPlane.Status.AppBox = &v1alpha1.Reference{
								Name: "test-appbox-" + key.Name,
							}
						}
						return nil
					})

					// Mock APPBox Get calls for granted dataplanes
					mockClient.EXPECT().Get(
						gomock.Any(),
						types.NamespacedName{Namespace: "test-namespace", Name: "test-appbox-" + dpId},
						gomock.Any(),
					).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
						if appBox, ok := obj.(*v1alpha1.APPBox); ok {
							appBox.Name = key.Name
							appBox.Namespace = key.Namespace
							appBox.Status.IAM.NodeRoleName = "test-role-" + key.Name
						}
						return nil
					})
				}
			},
		},
		{
			name: "success with both default statement and granted dataplanes",
			bucket: &v1alpha1.Bucket{
				Spec: v1alpha1.BucketSpec{
					GrantedDataplaneIds: []string{"granted-dp-1"},
				},
				Status: v1alpha1.BucketStatus{
					Bucket: &v1alpha1.Reference{
						Name: "test-bucket",
					},
				},
			},
			currentDataPlaneCR: &v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test-datapane",
					// No DeletionTimestamp, so IsDefaultStatementInBucketPolicyNeeded will return true
				},
			},
			expectedPolicyKeys:     []string{"Version", "Statement"},
			expectedStatementCount: 2, // One for default + one for granted dataplane
			expectError:            false,
			setupMocks: func(mockClient *kubemock.MockClient) {
				// Mock DataPlane Get call for default dataplane
				mockClient.EXPECT().Get(
					gomock.Any(),
					types.NamespacedName{Namespace: "test-namespace", Name: "test-datapane"},
					gomock.Any(),
				).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
					if dataPlane, ok := obj.(*v1alpha1.DataPlane); ok {
						dataPlane.Name = key.Name
						dataPlane.Namespace = key.Namespace
						dataPlane.Status.AppBox = &v1alpha1.Reference{
							Name: "test-appbox",
						}
					}
					return nil
				})

				// Mock APPBox Get call for default dataplane
				mockClient.EXPECT().Get(
					gomock.Any(),
					types.NamespacedName{Namespace: "test-namespace", Name: "test-appbox"},
					gomock.Any(),
				).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
					if appBox, ok := obj.(*v1alpha1.APPBox); ok {
						appBox.Name = key.Name
						appBox.Namespace = key.Namespace
						appBox.Status.IAM.NodeRoleName = "test-role"
					}
					return nil
				})

				// Mock DataPlane Get call for granted dataplane
				mockClient.EXPECT().Get(
					gomock.Any(),
					types.NamespacedName{Namespace: "test-namespace", Name: "granted-dp-1"},
					gomock.Any(),
				).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
					if dataPlane, ok := obj.(*v1alpha1.DataPlane); ok {
						dataPlane.Name = key.Name
						dataPlane.Namespace = key.Namespace
						dataPlane.Status.AppBox = &v1alpha1.Reference{
							Name: "test-appbox-granted",
						}
					}
					return nil
				})

				// Mock APPBox Get call for granted dataplane
				mockClient.EXPECT().Get(
					gomock.Any(),
					types.NamespacedName{Namespace: "test-namespace", Name: "test-appbox-granted"},
					gomock.Any(),
				).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
					if appBox, ok := obj.(*v1alpha1.APPBox); ok {
						appBox.Name = key.Name
						appBox.Namespace = key.Namespace
						appBox.Status.IAM.NodeRoleName = "test-role-granted"
					}
					return nil
				})
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create gomock controller
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			// Create mock client
			mockClient := kubemock.NewMockClient(ctrl)

			// Setup mocks
			tt.setupMocks(mockClient)

			// Create BucketPolicy instance with mock dependencies
			bucketPolicy := &BucketPolicy{
				BaseTask: &task.BaseTask{
					Context: &task.Context{
						Config: config.RootConfig{
							Namespace: "test-namespace",
						},
						Bucket: tt.bucket,
						Local:  mockClient,
					},
				},
			}

			// Call the function
			result, err := bucketPolicy.makeOssBucketPolicy(context.Background(), tt.currentDataPlaneCR)

			// Verify error
			if tt.expectError {
				assert.Error(t, err)
				assert.Nil(t, result)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, result)

				// Parse the result JSON and verify structure
				var actualPolicy map[string]interface{}
				err = json.Unmarshal([]byte(*result), &actualPolicy)
				assert.NoError(t, err)

				// Verify policy has expected keys
				for _, key := range tt.expectedPolicyKeys {
					assert.Contains(t, actualPolicy, key)
				}

				// Verify Version is "1"
				assert.Equal(t, "1", actualPolicy["Version"])

				// Verify statement count
				statements, ok := actualPolicy["Statement"].([]interface{})
				assert.True(t, ok)
				assert.Equal(t, tt.expectedStatementCount, len(statements))

				// If we have statements, verify they have the expected structure and content
				if tt.expectedStatementCount > 0 {
					for _, stmt := range statements {
						stmtMap, ok := stmt.(map[string]interface{})
						assert.True(t, ok)

						// Verify basic structure
						assert.Contains(t, stmtMap, "Effect")
						assert.Contains(t, stmtMap, "Principal")
						assert.Contains(t, stmtMap, "Action")
						assert.Contains(t, stmtMap, "Resource")
						assert.Equal(t, "Allow", stmtMap["Effect"])
						assert.Equal(t, "oss:*", stmtMap["Action"])

						// Verify Principal is a string array with correct ARN format
						principal, ok := stmtMap["Principal"].([]interface{})
						assert.True(t, ok)
						assert.Len(t, principal, 1)
						principalStr, ok := principal[0].(string)
						assert.True(t, ok)
						assert.Contains(t, principalStr, "arn:sts::")
						assert.Contains(t, principalStr, ":assumed-role/")
						assert.Contains(t, principalStr, "/*")

						// Verify Resource is a string array with correct OSS format
						resource, ok := stmtMap["Resource"].([]interface{})
						assert.True(t, ok)
						assert.Len(t, resource, 2)

						// First resource should be the bucket itself
						resource1, ok := resource[0].(string)
						assert.True(t, ok)
						assert.Equal(t, "acs:oss:*:*:test-bucket", resource1)

						// Second resource should be the bucket with wildcard
						resource2, ok := resource[1].(string)
						assert.True(t, ok)
						assert.Equal(t, "acs:oss:*:*:test-bucket/*", resource2)
					}
				}
			}
		})
	}
}

// Test the JSON marshaling edge case
func TestBucketPolicy_makeOssBucketPolicy_JSONMarshal(t *testing.T) {
	// Create gomock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Create mock client
	mockClient := kubemock.NewMockClient(ctrl)

	// Create a bucket with a name that could potentially cause JSON marshaling issues
	bucket := &v1alpha1.Bucket{
		Spec: v1alpha1.BucketSpec{
			GrantedDataplaneIds: []string{},
		},
		Status: v1alpha1.BucketStatus{
			Bucket: &v1alpha1.Reference{
				Name: "test-bucket-with-special-chars-!@#$%^&*()",
			},
		},
	}

	currentDataPlaneCR := &v1alpha1.DataPlane{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "test-datapane",
			DeletionTimestamp: &metav1.Time{}, // No default statement needed
		},
	}

	bucketPolicy := &BucketPolicy{
		BaseTask: &task.BaseTask{
			Context: &task.Context{
				Config: config.RootConfig{
					Namespace: "test-namespace",
				},
				Bucket: bucket,
				Local:  mockClient,
			},
		},
	}

	// This should not cause JSON marshaling errors
	result, err := bucketPolicy.makeOssBucketPolicy(context.Background(), currentDataPlaneCR)
	assert.NoError(t, err)
	assert.NotNil(t, result)

	// Verify the JSON is valid
	var policy map[string]interface{}
	err = json.Unmarshal([]byte(*result), &policy)
	assert.NoError(t, err)
	assert.Equal(t, "1", policy["Version"])
}

// Test detailed policy content verification
func TestBucketPolicy_makeOssBucketPolicy_DetailedContent(t *testing.T) {
	// Create gomock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Create mock client
	mockClient := kubemock.NewMockClient(ctrl)

	// Setup mock expectations for DataPlane
	mockClient.EXPECT().Get(
		gomock.Any(),
		types.NamespacedName{Namespace: "test-namespace", Name: "test-datapane"},
		gomock.Any(),
	).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
		if dataPlane, ok := obj.(*v1alpha1.DataPlane); ok {
			dataPlane.Name = key.Name
			dataPlane.Namespace = key.Namespace
			dataPlane.Spec.AccountRef.AccountID = "123456789"
			dataPlane.Status.AppBox = &v1alpha1.Reference{
				Name: "test-appbox",
			}
		}
		return nil
	})

	// Setup mock expectations for APPBox
	mockClient.EXPECT().Get(
		gomock.Any(),
		types.NamespacedName{Namespace: "test-namespace", Name: "test-appbox"},
		gomock.Any(),
	).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
		if appBox, ok := obj.(*v1alpha1.APPBox); ok {
			appBox.Name = key.Name
			appBox.Namespace = key.Namespace
			appBox.Status.IAM.NodeRoleName = "test-node-role"
		}
		return nil
	})

	bucket := &v1alpha1.Bucket{
		Spec: v1alpha1.BucketSpec{
			GrantedDataplaneIds: []string{},
		},
		Status: v1alpha1.BucketStatus{
			Bucket: &v1alpha1.Reference{
				Name: "my-test-bucket",
			},
		},
	}

	currentDataPlaneCR := &v1alpha1.DataPlane{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test-datapane",
			// No DeletionTimestamp, so IsDefaultStatementInBucketPolicyNeeded will return true
		},
	}

	bucketPolicy := &BucketPolicy{
		BaseTask: &task.BaseTask{
			Context: &task.Context{
				Config: config.RootConfig{
					Namespace: "test-namespace",
				},
				Bucket: bucket,
				Local:  mockClient,
			},
		},
	}

	// Call the function
	result, err := bucketPolicy.makeOssBucketPolicy(context.Background(), currentDataPlaneCR)
	assert.NoError(t, err)
	assert.NotNil(t, result)

	// Parse and verify the detailed policy content
	var policy map[string]interface{}
	err = json.Unmarshal([]byte(*result), &policy)
	assert.NoError(t, err)

	// Verify Version
	assert.Equal(t, "1", policy["Version"])

	// Verify Statement array has exactly one statement
	statements, ok := policy["Statement"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, statements, 1)

	// Get the first (and only) statement
	stmt, ok := statements[0].(map[string]interface{})
	assert.True(t, ok)

	// Verify Effect
	assert.Equal(t, "Allow", stmt["Effect"])

	// Verify Action
	assert.Equal(t, "oss:*", stmt["Action"])

	// Verify Principal - should be an array with one ARN
	principal, ok := stmt["Principal"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, principal, 1)
	principalStr, ok := principal[0].(string)
	assert.True(t, ok)
	assert.Equal(t, "arn:sts::123456789:assumed-role/test-node-role/*", principalStr)

	// Verify Resource - should be an array with two OSS ARNs
	resource, ok := stmt["Resource"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, resource, 2)

	resource1, ok := resource[0].(string)
	assert.True(t, ok)
	assert.Equal(t, "acs:oss:*:*:my-test-bucket", resource1)

	resource2, ok := resource[1].(string)
	assert.True(t, ok)
	assert.Equal(t, "acs:oss:*:*:my-test-bucket/*", resource2)
}

// Test multiple statements with different dataplanes
func TestBucketPolicy_makeOssBucketPolicy_MultipleStatements(t *testing.T) {
	// Create gomock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Create mock client
	mockClient := kubemock.NewMockClient(ctrl)

	// Setup mock expectations for default DataPlane
	mockClient.EXPECT().Get(
		gomock.Any(),
		types.NamespacedName{Namespace: "test-namespace", Name: "default-dp"},
		gomock.Any(),
	).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
		if dataPlane, ok := obj.(*v1alpha1.DataPlane); ok {
			dataPlane.Name = key.Name
			dataPlane.Namespace = key.Namespace
			dataPlane.Spec.AccountRef.AccountID = "111111111"
			dataPlane.Status.AppBox = &v1alpha1.Reference{
				Name: "default-appbox",
			}
		}
		return nil
	})

	// Setup mock expectations for default APPBox
	mockClient.EXPECT().Get(
		gomock.Any(),
		types.NamespacedName{Namespace: "test-namespace", Name: "default-appbox"},
		gomock.Any(),
	).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
		if appBox, ok := obj.(*v1alpha1.APPBox); ok {
			appBox.Name = key.Name
			appBox.Namespace = key.Namespace
			appBox.Status.IAM.NodeRoleName = "default-role"
		}
		return nil
	})

	// Setup mock expectations for granted DataPlane
	mockClient.EXPECT().Get(
		gomock.Any(),
		types.NamespacedName{Namespace: "test-namespace", Name: "granted-dp"},
		gomock.Any(),
	).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
		if dataPlane, ok := obj.(*v1alpha1.DataPlane); ok {
			dataPlane.Name = key.Name
			dataPlane.Namespace = key.Namespace
			dataPlane.Spec.AccountRef.AccountID = "222222222"
			dataPlane.Status.AppBox = &v1alpha1.Reference{
				Name: "granted-appbox",
			}
		}
		return nil
	})

	// Setup mock expectations for granted APPBox
	mockClient.EXPECT().Get(
		gomock.Any(),
		types.NamespacedName{Namespace: "test-namespace", Name: "granted-appbox"},
		gomock.Any(),
	).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj interface{}, opts ...interface{}) error {
		if appBox, ok := obj.(*v1alpha1.APPBox); ok {
			appBox.Name = key.Name
			appBox.Namespace = key.Namespace
			appBox.Status.IAM.NodeRoleName = "granted-role"
		}
		return nil
	})

	bucket := &v1alpha1.Bucket{
		Spec: v1alpha1.BucketSpec{
			GrantedDataplaneIds: []string{"granted-dp"},
		},
		Status: v1alpha1.BucketStatus{
			Bucket: &v1alpha1.Reference{
				Name: "multi-bucket",
			},
		},
	}

	currentDataPlaneCR := &v1alpha1.DataPlane{
		ObjectMeta: metav1.ObjectMeta{
			Name: "default-dp",
			// No DeletionTimestamp, so IsDefaultStatementInBucketPolicyNeeded will return true
		},
	}

	bucketPolicy := &BucketPolicy{
		BaseTask: &task.BaseTask{
			Context: &task.Context{
				Config: config.RootConfig{
					Namespace: "test-namespace",
				},
				Bucket: bucket,
				Local:  mockClient,
			},
		},
	}

	// Call the function
	result, err := bucketPolicy.makeOssBucketPolicy(context.Background(), currentDataPlaneCR)
	assert.NoError(t, err)
	assert.NotNil(t, result)

	// Parse and verify the detailed policy content
	var policy map[string]interface{}
	err = json.Unmarshal([]byte(*result), &policy)
	assert.NoError(t, err)

	// Verify Version
	assert.Equal(t, "1", policy["Version"])

	// Verify Statement array has exactly two statements
	statements, ok := policy["Statement"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, statements, 2)

	// Verify first statement (default dataplane)
	stmt1, ok := statements[0].(map[string]interface{})
	assert.True(t, ok)
	assert.Equal(t, "Allow", stmt1["Effect"])
	assert.Equal(t, "oss:*", stmt1["Action"])

	// Verify first statement's Principal
	principal1, ok := stmt1["Principal"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, principal1, 1)
	principalStr1, ok := principal1[0].(string)
	assert.True(t, ok)
	assert.Equal(t, "arn:sts::111111111:assumed-role/default-role/*", principalStr1)

	// Verify first statement's Resource
	resource1, ok := stmt1["Resource"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, resource1, 2)
	assert.Equal(t, "acs:oss:*:*:multi-bucket", resource1[0])
	assert.Equal(t, "acs:oss:*:*:multi-bucket/*", resource1[1])

	// Verify second statement (granted dataplane)
	stmt2, ok := statements[1].(map[string]interface{})
	assert.True(t, ok)
	assert.Equal(t, "Allow", stmt2["Effect"])
	assert.Equal(t, "oss:*", stmt2["Action"])

	// Verify second statement's Principal
	principal2, ok := stmt2["Principal"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, principal2, 1)
	principalStr2, ok := principal2[0].(string)
	assert.True(t, ok)
	assert.Equal(t, "arn:sts::222222222:assumed-role/granted-role/*", principalStr2)

	// Verify second statement's Resource
	resource2, ok := stmt2["Resource"].([]interface{})
	assert.True(t, ok)
	assert.Len(t, resource2, 2)
	assert.Equal(t, "acs:oss:*:*:multi-bucket", resource2[0])
	assert.Equal(t, "acs:oss:*:*:multi-bucket/*", resource2[1])
}
