package checkpoint

import (
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/metering/pkg/common/config"
	"github.com/tidbcloud/metering/pkg/common/constants"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest"
)

// setupFakeAWSEnv installs dummy AWS credentials for the duration of a test run
// and restores the previous environment afterwards.
func setupFakeAWSEnv(t *testing.T) {
	t.Helper()

	type envState struct {
		value   string
		present bool
	}

	saved := make(map[string]envState)

	setEnv := func(key, value string) {
		if _, recorded := saved[key]; !recorded {
			if current, ok := os.LookupEnv(key); ok {
				saved[key] = envState{value: current, present: true}
			} else {
				saved[key] = envState{present: false}
			}
		}
		if err := os.Setenv(key, value); err != nil {
			t.Fatalf("failed to set %s: %v", key, err)
		}
	}

	unsetEnv := func(key string) {
		if _, recorded := saved[key]; !recorded {
			if current, ok := os.LookupEnv(key); ok {
				saved[key] = envState{value: current, present: true}
			} else {
				saved[key] = envState{present: false}
			}
		}
		if err := os.Unsetenv(key); err != nil {
			t.Fatalf("failed to unset %s: %v", key, err)
		}
	}

	setEnv("AWS_ACCESS_KEY_ID", "test-access-key")
	setEnv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
	setEnv("AWS_SESSION_TOKEN", "test-session-token")
	setEnv("AWS_REGION", "us-east-1")
	setEnv("AWS_DEFAULT_REGION", "us-east-1")

	unsetEnv("AWS_ROLE_ARN")
	unsetEnv("AWS_WEB_IDENTITY_TOKEN_FILE")
	unsetEnv("AWS_PROFILE")
	unsetEnv("AWS_DEFAULT_PROFILE")

	t.Cleanup(func() {
		for key, state := range saved {
			if state.present {
				if err := os.Setenv(key, state.value); err != nil {
					t.Fatalf("failed to restore %s: %v", key, err)
				}
			} else if err := os.Unsetenv(key); err != nil {
				t.Fatalf("failed to clear %s: %v", key, err)
			}
		}
	})
}

// TestNewFactory tests the Factory constructor
func TestNewFactory(t *testing.T) {
	tests := []struct {
		name   string
		conf   *config.ServiceConfig
		logger bool // whether to provide logger or nil
	}{
		{
			name: "valid_configuration",
			conf: &config.ServiceConfig{
				Vendor: "aws",
				Bucket: "test-bucket",
				Region: "us-west-2",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{
					CheckpointPrefix: "test-prefix",
				},
			},
			logger: true,
		},
		{
			name:   "nil_configuration",
			conf:   nil,
			logger: true,
		},
		{
			name: "nil_logger",
			conf: &config.ServiceConfig{
				Vendor: "aws",
			},
			logger: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var log *zap.Logger
			if tt.logger {
				log = zaptest.NewLogger(t)
			}

			factory := NewFactory(tt.conf, log)

			assert.NotNil(t, factory)
			assert.Equal(t, tt.conf, factory.conf)
			if tt.logger {
				assert.NotNil(t, factory.log)
			} else {
				assert.Nil(t, factory.log)
			}
		})
	}
}

// TestCreateCheckpointManager_ErrorCases tests various error scenarios
func TestCreateCheckpointManager_ErrorCases(t *testing.T) {
	log := zaptest.NewLogger(t)

	tests := []struct {
		name        string
		conf        *config.ServiceConfig
		expectedErr string
	}{
		{
			name: "nil_nextgen_config",
			conf: &config.ServiceConfig{
				Vendor: "aws",
				Bucket: "test-bucket",
				Region: "us-west-2",
				// NextGenMeteringConfig is nil
			},
			expectedErr: "NextGenMeteringConfig is required",
		},
		{
			name: "empty_vendor",
			conf: &config.ServiceConfig{
				Vendor:                "", // Empty vendor
				Bucket:                "test-bucket",
				Region:                "us-west-2",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{},
			},
			expectedErr: "vendor is required in service configuration",
		},
		{
			name: "unsupported_vendor_gcp",
			conf: &config.ServiceConfig{
				Vendor:                "gcp",
				Bucket:                "test-bucket",
				Region:                "us-west-2",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{},
			},
			expectedErr: "GCP checkpoint manager not implemented yet",
		},
		{
			name: "unknown_vendor",
			conf: &config.ServiceConfig{
				Vendor:                "unknown",
				Bucket:                "test-bucket",
				Region:                "us-west-2",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{},
			},
			expectedErr: "unsupported storage vendor: unknown",
		},
		{
			name: "case_insensitive_vendor_gcp",
			conf: &config.ServiceConfig{
				Vendor:                "GCP", // Upper case
				Bucket:                "test-bucket",
				Region:                "us-west-2",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{},
			},
			expectedErr: "GCP checkpoint manager not implemented yet",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			factory := NewFactory(tt.conf, log)

			manager, err := factory.CreateCheckpointManager()

			assert.Error(t, err)
			assert.Nil(t, manager)
			assert.Contains(t, err.Error(), tt.expectedErr)
		})
	}
}

func TestCreateCheckpointManager_Azure_Success(t *testing.T) {
	t.Setenv(constants.AzureStorageAccountNameEnv, "teststorageaccount")
	t.Setenv(constants.AzureStorageAccountKeyEnv, "dGVzdC1hY2NvdW50LWtleQ==")
	t.Setenv(constants.AzureStorageSASTokenEnv, "")

	factory := NewFactory(&config.ServiceConfig{
		Vendor: "azure",
		Bucket: "test-container",
		NextGenMeteringConfig: &config.NextGenMeteringConfig{
			CheckpointPrefix: "checkpoints",
			CachePath:        t.TempDir(),
		},
	}, zaptest.NewLogger(t))

	manager, err := factory.CreateCheckpointManager()

	require.NoError(t, err)
	assert.NotNil(t, manager)
}

// TestCreateCheckpointManager_AWS_ErrorCases tests AWS-specific error scenarios
func TestCreateCheckpointManager_AWS_ErrorCases(t *testing.T) {
	setupFakeAWSEnv(t)

	log := zaptest.NewLogger(t)

	tests := []struct {
		name        string
		conf        *config.ServiceConfig
		expectedErr string
	}{
		{
			name: "aws_missing_bucket",
			conf: &config.ServiceConfig{
				Vendor:                "aws",
				Bucket:                "", // Missing bucket
				Region:                "us-west-2",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{},
			},
			expectedErr: "bucket is required in service configuration",
		},
		{
			name: "aws_missing_region",
			conf: &config.ServiceConfig{
				Vendor:                "aws",
				Bucket:                "test-bucket",
				Region:                "", // Missing region
				NextGenMeteringConfig: &config.NextGenMeteringConfig{},
			},
			expectedErr: "region is required in service configuration",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			factory := NewFactory(tt.conf, log)

			manager, err := factory.CreateCheckpointManager()

			assert.Error(t, err)
			assert.Nil(t, manager)
			assert.Contains(t, err.Error(), tt.expectedErr)
		})
	}
}

func TestCreateCheckpointManager_Alicloud_ErrorCases(t *testing.T) {
	log := zaptest.NewLogger(t)

	tests := []struct {
		name        string
		conf        *config.ServiceConfig
		setupEnv    func(t *testing.T)
		expectedErr string
	}{
		{
			name: "missing_environment_credentials",
			conf: &config.ServiceConfig{
				Vendor: constants.VendorAlicloud,
				Bucket: "alicloud-bucket",
				Region: "cn-hangzhou",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{
					BucketEndpoint: "https://oss-cn-hangzhou.aliyuncs.com",
				},
			},
			setupEnv: func(t *testing.T) {
				t.Setenv(constants.AlicloudAccessKeyIDEnv, "")
				t.Setenv(constants.AlicloudAccessKeySecretEnv, "")
			},
			expectedErr: "environment variables are required for Alicloud",
		},
		{
			name: "missing_bucket",
			conf: &config.ServiceConfig{
				Vendor: constants.VendorAlicloud,
				Bucket: "",
				Region: "cn-hangzhou",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{
					BucketEndpoint: "https://oss-cn-hangzhou.aliyuncs.com",
				},
			},
			setupEnv: func(t *testing.T) {
				t.Setenv(constants.AlicloudAccessKeyIDEnv, "test-ak")
				t.Setenv(constants.AlicloudAccessKeySecretEnv, "test-sk")
			},
			expectedErr: "bucket is required in service configuration",
		},
		{
			name: "missing_region",
			conf: &config.ServiceConfig{
				Vendor: constants.VendorAlicloud,
				Bucket: "alicloud-bucket",
				Region: "",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{
					BucketEndpoint: "https://oss-cn-hangzhou.aliyuncs.com",
				},
			},
			setupEnv: func(t *testing.T) {
				t.Setenv(constants.AlicloudAccessKeyIDEnv, "test-ak")
				t.Setenv(constants.AlicloudAccessKeySecretEnv, "test-sk")
			},
			expectedErr: "region is required in service configuration",
		},
		{
			name: "missing_bucket_endpoint",
			conf: &config.ServiceConfig{
				Vendor:                constants.VendorAlicloud,
				Bucket:                "alicloud-bucket",
				Region:                "cn-hangzhou",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{},
			},
			setupEnv: func(t *testing.T) {
				t.Setenv(constants.AlicloudAccessKeyIDEnv, "test-ak")
				t.Setenv(constants.AlicloudAccessKeySecretEnv, "test-sk")
			},
			expectedErr: "bucketEndpoint is required in NextGenMeteringConfig for Alicloud",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt := tt
			tt.setupEnv(t)

			factory := NewFactory(tt.conf, log)
			manager, err := factory.CreateCheckpointManager()

			assert.Error(t, err)
			assert.Nil(t, manager)
			assert.Contains(t, err.Error(), tt.expectedErr)
		})
	}
}

func TestCreateCheckpointManager_Alicloud_Success(t *testing.T) {
	log := zaptest.NewLogger(t)

	t.Setenv(constants.AlicloudAccessKeyIDEnv, "test-ak")
	t.Setenv(constants.AlicloudAccessKeySecretEnv, "test-sk")

	conf := &config.ServiceConfig{
		Vendor: constants.VendorAlicloud,
		Bucket: "alicloud-bucket",
		Region: "cn-hangzhou",
		NextGenMeteringConfig: &config.NextGenMeteringConfig{
			CheckpointPrefix: "ali-checkpoints",
			CachePath:        "/opt/cache",
			BucketEndpoint:   "https://oss-cn-hangzhou.aliyuncs.com",
		},
	}

	factory := NewFactory(conf, log)
	manager, err := factory.CreateCheckpointManager()

	assert.NoError(t, err)
	assert.NotNil(t, manager)

	_, ok := manager.(*S3CheckpointManager)
	assert.True(t, ok, "expected manager to be *S3CheckpointManager when using Alicloud")
}

// TestCreateCheckpointManager_AWS_IntegrationTests tests AWS success scenarios
// Note: These are integration tests that depend on AWS SDK behavior
func TestCreateCheckpointManager_AWS_IntegrationTests(t *testing.T) {
	setupFakeAWSEnv(t)

	log := zaptest.NewLogger(t)

	tests := []struct {
		name                     string
		conf                     *config.ServiceConfig
		expectedBucket           string
		expectedRegion           string
		expectedCheckpointPrefix string
		expectedCachePath        string
	}{
		{
			name: "aws_with_default_values",
			conf: &config.ServiceConfig{
				Vendor:                "aws",
				Bucket:                "test-bucket",
				Region:                "us-west-2",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{
					// Using default values for CheckpointPrefix and CachePath
				},
			},
			expectedBucket:           "test-bucket",
			expectedRegion:           "us-west-2",
			expectedCheckpointPrefix: "checkpoints", // Default value
			expectedCachePath:        "/tmp",        // Default value
		},
		{
			name: "aws_with_custom_values",
			conf: &config.ServiceConfig{
				Vendor: "AWS", // Case insensitive
				Bucket: "custom-bucket",
				Region: "eu-west-1",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{
					CheckpointPrefix: "custom-checkpoint-prefix",
					CachePath:        "/custom/cache/path",
				},
			},
			expectedBucket:           "custom-bucket",
			expectedRegion:           "eu-west-1",
			expectedCheckpointPrefix: "custom-checkpoint-prefix",
			expectedCachePath:        "/custom/cache/path",
		},
		{
			name: "aws_case_insensitive",
			conf: &config.ServiceConfig{
				Vendor: "AwS", // Mixed case
				Bucket: "case-test-bucket",
				Region: "ap-southeast-1",
				NextGenMeteringConfig: &config.NextGenMeteringConfig{
					CheckpointPrefix: "case-test-prefix",
				},
			},
			expectedBucket:           "case-test-bucket",
			expectedRegion:           "ap-southeast-1",
			expectedCheckpointPrefix: "case-test-prefix",
			expectedCachePath:        "/tmp", // Default when CachePath not set
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			factory := NewFactory(tt.conf, log)

			manager, err := factory.CreateCheckpointManager()

			// With proper environment variables set, we should always get a valid manager
			assert.NoError(t, err, "CreateCheckpointManager should succeed with valid AWS credentials")
			assert.NotNil(t, manager, "Manager should not be nil")

			// Type assertion to verify we got the right type
			s3Manager, ok := manager.(*S3CheckpointManager)
			assert.True(t, ok, "Manager should be of type *S3CheckpointManager")

			if s3Manager != nil {
				// Verify the S3 manager was configured correctly
				// Note: We can't easily inspect private fields without reflection
				// but the successful creation itself validates the configuration was correct
				t.Logf("Successfully created S3 checkpoint manager with bucket: %s, region: %s",
					tt.expectedBucket, tt.expectedRegion)
			}
		})
	}
}
