package alicloud

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"testing"

	"github.com/onsi/gomega"
	"github.com/stretchr/testify/assert"
	"github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/metrics"
	"github.com/tidbcloud/observability/pkg/resourceworker/providers/types"
	mocks "github.com/tidbcloud/observability/test/mockresourceworker/providers/alicloud"
	"go.uber.org/mock/gomock"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
)

func newMockS3(cfg oss.Config, mock OssClient) S3Client {
	return S3Client{
		cli:            mock,
		cfg:            cfg,
		redundancyType: oss.DataRedundancyZRS,
	}
}

// TestNewS3 tests the NewS3 constructor
func TestNewS3(t *testing.T) {
	cfg := &oss.Config{
		Region: oss.Ptr("cn-beijing"),
	}

	// Test with default ZRS
	client := NewS3(cfg, "")
	assert.NotNil(t, client.cli)
	assert.Equal(t, *cfg.Region, *client.cfg.Region)
	assert.Equal(t, oss.DataRedundancyZRS, client.redundancyType)

	// Test with explicit ZRS
	clientZRS := NewS3(cfg, "ZRS")
	assert.NotNil(t, clientZRS.cli)
	assert.Equal(t, oss.DataRedundancyZRS, clientZRS.redundancyType)

	// Test with LRS
	clientLRS := NewS3(cfg, "LRS")
	assert.NotNil(t, clientLRS.cli)
	assert.Equal(t, oss.DataRedundancyLRS, clientLRS.redundancyType)
}

// TestMockS3 tests the MockS3 function
func TestMockS3(t *testing.T) {
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	mockClient := mocks.NewMockOssClient(ctl)
	s3Client := MockS3(mockClient)

	assert.NotNil(t, s3Client.cli)
	assert.Equal(t, mockClient, s3Client.cli)
}

func TestS3CreateBucket(t *testing.T) {
	g := gomega.NewGomegaWithT(t)
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("oss-cn-shanghai")

	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	// Test successful creation
	mockclient.EXPECT().PutBucket(context.TODO(), &oss.PutBucketRequest{
		Bucket: oss.Ptr("test-bucket"),
		Acl:    oss.BucketACLPrivate,
		CreateBucketConfiguration: &oss.CreateBucketConfiguration{
			DataRedundancyType: oss.DataRedundancyZRS,
		},
	}).Return(&oss.PutBucketResult{}, nil)

	g.Expect(s3client.CreateBucket(context.TODO(), "test-bucket")).Should(gomega.Succeed())

	// Test failed creation
	mockclient.EXPECT().PutBucket(context.TODO(), &oss.PutBucketRequest{
		Bucket: oss.Ptr("error-bucket"),
		Acl:    oss.BucketACLPrivate,
		CreateBucketConfiguration: &oss.CreateBucketConfiguration{
			DataRedundancyType: oss.DataRedundancyZRS,
		},
	}).Return(nil, errors.New(errors.UnrecognizedError, "bucket creation failed"))

	err := s3client.CreateBucket(context.TODO(), "error-bucket")
	g.Expect(err).Should(gomega.HaveOccurred())
}

func TestS3BucketExists(t *testing.T) {
	metrics.SetupMetrics()
	g := gomega.NewGomegaWithT(t)
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("cn")
	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	// Bucket exists
	mockclient.EXPECT().IsBucketExist(context.TODO(), "test-bucket").Return(true, nil)
	exists, err := s3client.BucketExists(context.TODO(), "test-bucket")
	g.Expect(err).ShouldNot(gomega.HaveOccurred())
	g.Expect(exists).Should(gomega.Equal(true))

	// Bucket does not exist
	mockclient.EXPECT().IsBucketExist(context.TODO(), "notfound").Return(false, nil)
	exists, err = s3client.BucketExists(context.TODO(), "notfound")
	g.Expect(err).ShouldNot(gomega.HaveOccurred())
	g.Expect(exists).Should(gomega.Equal(false))

	// Expect error
	mockclient.EXPECT().IsBucketExist(context.TODO(), "error-bucket").Return(false, errors.New(errors.UnrecognizedError, "some error"))
	exists, err = s3client.BucketExists(context.TODO(), "error-bucket")
	g.Expect(err).Should(gomega.HaveOccurred())
	g.Expect(exists).Should(gomega.Equal(false))
}

func TestS3GetObject(t *testing.T) {
	metrics.SetupMetrics()
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("cn")
	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	// Expect error
	mockclient.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "some error"))
	_, err := s3client.Get(context.TODO(), "test-bucket", "test-object")
	assert.Error(t, err)

	// Expect success
	mockclient.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&oss.GetObjectResult{
		Body: io.NopCloser(bytes.NewReader([]byte("hello world"))),
	}, nil)

	obj, err := s3client.Get(context.TODO(), "test-bucket", "test-object")
	assert.NoError(t, err)
	assert.NotNil(t, obj)
}

func TestS3PutObject(t *testing.T) {
	metrics.SetupMetrics()
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("cn")
	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	// Expect error
	mockclient.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "some error"))
	err := s3client.Put(context.TODO(), "test-bucket", "test-object", nil)

	assert.Error(t, err)

	// Expect success
	mockclient.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&oss.PutObjectResult{}, nil)
	r := bytes.NewReader([]byte("test-data"))
	err = s3client.Put(context.TODO(), "test-bucket", "test-object", r)
	assert.NoError(t, err)
}

// TestS3Delete tests the Delete function
func TestS3Delete(t *testing.T) {
	metrics.SetupMetrics()
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("cn")
	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	// Test successful deletion of single object
	mockclient.EXPECT().DeleteMultipleObjects(context.TODO(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, req *oss.DeleteMultipleObjectsRequest, optFns ...func(*oss.Options)) (*oss.DeleteMultipleObjectsResult, error) {
			// Verify the request structure
			assert.Equal(t, "test-bucket", *req.Bucket)
			assert.Len(t, req.Objects, 1)
			assert.Equal(t, "test-object1", *req.Objects[0].Key)
			return &oss.DeleteMultipleObjectsResult{}, nil
		})

	err := s3client.Delete(context.TODO(), "test-bucket", "test-object1")
	assert.NoError(t, err)

	// Test successful deletion of multiple objects
	mockclient.EXPECT().DeleteMultipleObjects(context.TODO(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, req *oss.DeleteMultipleObjectsRequest, optFns ...func(*oss.Options)) (*oss.DeleteMultipleObjectsResult, error) {
			// Verify the request structure
			assert.Equal(t, "test-bucket", *req.Bucket)
			assert.Len(t, req.Objects, 3)
			assert.Equal(t, "test-object1", *req.Objects[0].Key)
			assert.Equal(t, "test-object2", *req.Objects[1].Key)
			assert.Equal(t, "test-object3", *req.Objects[2].Key)
			return &oss.DeleteMultipleObjectsResult{}, nil
		})

	err = s3client.Delete(context.TODO(), "test-bucket", "test-object1", "test-object2", "test-object3")
	assert.NoError(t, err)

	// Test deletion failure
	mockclient.EXPECT().DeleteMultipleObjects(context.TODO(), gomock.Any(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "delete failed"))

	err = s3client.Delete(context.TODO(), "test-bucket", "error-object")
	assert.Error(t, err)

	// Test deletion with empty object list - this should still call DeleteMultipleObjects
	mockclient.EXPECT().DeleteMultipleObjects(context.TODO(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, req *oss.DeleteMultipleObjectsRequest, optFns ...func(*oss.Options)) (*oss.DeleteMultipleObjectsResult, error) {
			// Verify the request structure
			assert.Equal(t, "test-bucket", *req.Bucket)
			assert.Len(t, req.Objects, 0) // Should be empty
			return &oss.DeleteMultipleObjectsResult{}, nil
		})

	err = s3client.Delete(context.TODO(), "test-bucket")
	assert.NoError(t, err) // Should succeed with empty objects list
}

// TestS3ListObjects tests the ListObjects function
func TestS3ListObjects(t *testing.T) {
	// Since we can't easily mock the OSS paginator (it's a concrete type),
	// we'll test that the function calls the right methods with the right parameters
	// and skip the actual pagination logic testing for now.
	// This still provides valuable test coverage for parameter handling.

	metrics.SetupMetrics()
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("cn")
	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	// Test parameter passing without MaxKeys
	options := types.ListOptions{}

	mockclient.EXPECT().NewListObjectsV2Paginator(gomock.Any(), gomock.Any()).DoAndReturn(
		func(req *oss.ListObjectsV2Request, optFns ...func(*oss.PaginatorOptions)) *oss.ListObjectsV2Paginator {
			// Verify the request parameters
			assert.Equal(t, "test-bucket", *req.Bucket)
			assert.Equal(t, "test-prefix", *req.Prefix)
			assert.Equal(t, int32(0), req.MaxKeys) // Should be default value

			// Return nil since we can't easily mock the paginator behavior
			// In a real scenario, the paginator would handle the pagination
			return nil
		})

	// This will panic due to nil paginator, but we're mainly testing parameter validation
	assert.Panics(t, func() {
		s3client.ListObjects(context.TODO(), "test-bucket", "test-prefix", options)
	})

	// Test parameter passing with MaxKeys
	maxKeys := int32(10)
	optionsWithMaxKeys := types.ListOptions{MaxKeys: &maxKeys}

	mockclient.EXPECT().NewListObjectsV2Paginator(gomock.Any(), gomock.Any()).DoAndReturn(
		func(req *oss.ListObjectsV2Request, optFns ...func(*oss.PaginatorOptions)) *oss.ListObjectsV2Paginator {
			// Verify the request parameters
			assert.Equal(t, "test-bucket", *req.Bucket)
			assert.Equal(t, "test-prefix", *req.Prefix)
			assert.Equal(t, int32(10), req.MaxKeys) // MaxKeys should be set

			return nil
		})

	// This will also panic, but we've verified the parameter passing
	assert.Panics(t, func() {
		s3client.ListObjects(context.TODO(), "test-bucket", "test-prefix", optionsWithMaxKeys)
	})
}

// TestS3AddLifecycle tests the AddLifecycle function
func TestS3AddLifecycle(t *testing.T) {
	metrics.SetupMetrics()
	g := gomega.NewGomegaWithT(t)
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("cn")
	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	t.Run("add new rules when no existing lifecycle rules", func(t *testing.T) {
		// Mock GetBucketLifecycle returning "not found" error
		serviceErr := &oss.ServiceError{
			Code:       "NoSuchLifecycleConfiguration",
			StatusCode: http.StatusNotFound,
		}
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, serviceErr)

		// Mock PutBucketLifecycle
		mockclient.EXPECT().PutBucketLifecycle(context.TODO(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, req *oss.PutBucketLifecycleRequest, optFns ...func(*oss.Options)) (*oss.PutBucketLifecycleResult, error) {
				g.Expect(req.Bucket).ToNot(gomega.BeNil())
				g.Expect(*req.Bucket).To(gomega.Equal("test-bucket"))
				g.Expect(req.LifecycleConfiguration).ToNot(gomega.BeNil())
				// In OSS, Expiration and Transitions can be in the same rule
				g.Expect(req.LifecycleConfiguration.Rules).To(gomega.HaveLen(1))
				g.Expect(*req.LifecycleConfiguration.Rules[0].ID).To(gomega.Equal("testRule"))
				g.Expect(req.LifecycleConfiguration.Rules[0].Expiration).ToNot(gomega.BeNil())
				g.Expect(*req.LifecycleConfiguration.Rules[0].Expiration.Days).To(gomega.Equal(int32(30)))
				g.Expect(req.LifecycleConfiguration.Rules[0].Transitions).To(gomega.HaveLen(1))
				return &oss.PutBucketLifecycleResult{}, nil
			},
		)

		rules := []message.LifecycleRuleParam{
			{RuleName: "testRule", Prefix: "test/", ExpirationDays: 30, ColdDays: 90},
		}
		err := s3client.AddLifecycle(context.TODO(), "test-bucket", rules, []string{})
		g.Expect(err).Should(gomega.Succeed())
	})

	t.Run("add new rules with existing lifecycle rules", func(t *testing.T) {
		existingRuleID := "existing-rule"
		existingRule := oss.LifecycleRule{
			ID:     oss.Ptr(existingRuleID),
			Prefix: oss.Ptr("existing/"),
			Status: oss.Ptr("Enabled"),
		}

		// Mock GetBucketLifecycle returning existing rules
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(&oss.GetBucketLifecycleResult{
			LifecycleConfiguration: &oss.LifecycleConfiguration{
				Rules: []oss.LifecycleRule{existingRule},
			},
		}, nil)

		// Mock PutBucketLifecycle
		mockclient.EXPECT().PutBucketLifecycle(context.TODO(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, req *oss.PutBucketLifecycleRequest, optFns ...func(*oss.Options)) (*oss.PutBucketLifecycleResult, error) {
				// existing + 1 new rule (Expiration and Transitions are in the same rule)
				g.Expect(req.LifecycleConfiguration.Rules).To(gomega.HaveLen(2))
				// Verify existing rule is preserved
				foundExisting := false
				for _, rule := range req.LifecycleConfiguration.Rules {
					if rule.ID != nil && *rule.ID == existingRuleID {
						foundExisting = true
						break
					}
				}
				g.Expect(foundExisting).To(gomega.BeTrue())
				return &oss.PutBucketLifecycleResult{}, nil
			},
		)

		rules := []message.LifecycleRuleParam{
			{RuleName: "newRule", Prefix: "new/", ExpirationDays: 10},
		}
		err := s3client.AddLifecycle(context.TODO(), "test-bucket", rules, []string{})
		g.Expect(err).Should(gomega.Succeed())
	})

	t.Run("filter out rules with unused prefixes", func(t *testing.T) {
		unusedRuleID := "unused-prefix-rule"
		existingRuleID := "existing-rule"
		existingRules := []oss.LifecycleRule{
			{
				ID:     oss.Ptr(unusedRuleID),
				Prefix: oss.Ptr("unused/"),
				Status: oss.Ptr("Enabled"),
			},
			{
				ID:     oss.Ptr(existingRuleID),
				Prefix: oss.Ptr("existing/"),
				Status: oss.Ptr("Enabled"),
			},
		}

		// Mock GetBucketLifecycle returning existing rules
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(&oss.GetBucketLifecycleResult{
			LifecycleConfiguration: &oss.LifecycleConfiguration{
				Rules: existingRules,
			},
		}, nil)

		// Mock PutBucketLifecycle
		mockclient.EXPECT().PutBucketLifecycle(context.TODO(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, req *oss.PutBucketLifecycleRequest, optFns ...func(*oss.Options)) (*oss.PutBucketLifecycleResult, error) {
				// Should have new rule + existing rule (unused rule should be filtered out)
				g.Expect(req.LifecycleConfiguration.Rules).To(gomega.HaveLen(2))
				// Verify unused rule is not present
				for _, rule := range req.LifecycleConfiguration.Rules {
					if rule.ID != nil {
						g.Expect(*rule.ID).ToNot(gomega.Equal(unusedRuleID))
					}
				}
				return &oss.PutBucketLifecycleResult{}, nil
			},
		)

		rules := []message.LifecycleRuleParam{
			{RuleName: "newRule", Prefix: "new/", ExpirationDays: 10},
		}
		unusedRulePrefixes := []string{"unused-prefix"}
		err := s3client.AddLifecycle(context.TODO(), "test-bucket", rules, unusedRulePrefixes)
		g.Expect(err).Should(gomega.Succeed())
	})

	t.Run("get lifecycle error", func(t *testing.T) {
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "get lifecycle error"))

		rules := []message.LifecycleRuleParam{
			{RuleName: "testRule", Prefix: "test/", ExpirationDays: 30},
		}
		err := s3client.AddLifecycle(context.TODO(), "test-bucket", rules, []string{})
		g.Expect(err).Should(gomega.HaveOccurred())
	})

	t.Run("put lifecycle error", func(t *testing.T) {
		serviceErr := &oss.ServiceError{
			Code:       "NoSuchLifecycleConfiguration",
			StatusCode: http.StatusNotFound,
		}
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, serviceErr)
		mockclient.EXPECT().PutBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "put lifecycle error"))

		rules := []message.LifecycleRuleParam{
			{RuleName: "testRule", Prefix: "test/", ExpirationDays: 30},
		}
		err := s3client.AddLifecycle(context.TODO(), "test-bucket", rules, []string{})
		g.Expect(err).Should(gomega.HaveOccurred())
	})

	t.Run("keep zero prefix fallback with overlapping profile rules", func(t *testing.T) {
		serviceErr := &oss.ServiceError{
			Code:       "NoSuchLifecycleConfiguration",
			StatusCode: http.StatusNotFound,
		}
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, serviceErr)
		mockclient.EXPECT().PutBucketLifecycle(context.TODO(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, req *oss.PutBucketLifecycleRequest, optFns ...func(*oss.Options)) (*oss.PutBucketLifecycleResult, error) {
				g.Expect(req.AllowSameActionOverlap).ToNot(gomega.BeNil())
				g.Expect(*req.AllowSameActionOverlap).To(gomega.Equal("true"))
				g.Expect(req.LifecycleConfiguration.Rules).To(gomega.HaveLen(2))

				foundZeroPrefix := false
				foundProfile := false
				for _, rule := range req.LifecycleConfiguration.Rules {
					if rule.ID != nil && *rule.ID == "zero_prefix_fallback" {
						foundZeroPrefix = true
						g.Expect(rule.Prefix).ToNot(gomega.BeNil())
						g.Expect(*rule.Prefix).To(gomega.Equal("0/"))
						g.Expect(rule.Expiration).ToNot(gomega.BeNil())
						g.Expect(*rule.Expiration.Days).To(gomega.Equal(int32(180)))
					}
					if rule.ID != nil && *rule.ID == "profile_deletion_app-1" {
						foundProfile = true
						g.Expect(rule.Prefix).ToNot(gomega.BeNil())
						g.Expect(*rule.Prefix).To(gomega.Equal("0/tenant/project/app-1/profiles/"))
						g.Expect(rule.Expiration).ToNot(gomega.BeNil())
						g.Expect(*rule.Expiration.Days).To(gomega.Equal(int32(32)))
					}
				}
				g.Expect(foundZeroPrefix).To(gomega.BeTrue())
				g.Expect(foundProfile).To(gomega.BeTrue())
				return &oss.PutBucketLifecycleResult{}, nil
			},
		)

		rules := []message.LifecycleRuleParam{
			{
				RuleName:       "profile_deletion_app-1",
				Prefix:         "0/tenant/project/app-1/profiles/",
				ExpirationDays: 32,
			},
			{
				RuleName:       "zero_prefix_fallback",
				Prefix:         "0/",
				ExpirationDays: 180,
			},
		}
		err := s3client.AddLifecycle(context.TODO(), "test-bucket", rules, []string{})
		g.Expect(err).Should(gomega.Succeed())
	})
}

// TestS3RemoveLifecycle tests the RemoveLifecycle function
func TestS3RemoveLifecycle(t *testing.T) {
	metrics.SetupMetrics()
	g := gomega.NewGomegaWithT(t)
	ctl := gomock.NewController(t)
	defer ctl.Finish()

	var cfg oss.Config
	cfg.Region = oss.Ptr("cn")
	mockclient := mocks.NewMockOssClient(ctl)
	s3client := newMockS3(cfg, mockclient)

	t.Run("remove rule when lifecycle configuration exists", func(t *testing.T) {
		ruleToRemove := "rule-to-remove"
		ruleToKeep := "rule-to-keep"
		existingRules := []oss.LifecycleRule{
			{
				ID:     oss.Ptr(ruleToRemove),
				Prefix: oss.Ptr("remove/"),
				Status: oss.Ptr("Enabled"),
			},
			{
				ID:     oss.Ptr(ruleToKeep),
				Prefix: oss.Ptr("keep/"),
				Status: oss.Ptr("Enabled"),
			},
		}

		// Mock GetBucketLifecycle returning existing rules
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(&oss.GetBucketLifecycleResult{
			LifecycleConfiguration: &oss.LifecycleConfiguration{
				Rules: existingRules,
			},
		}, nil)

		// Mock PutBucketLifecycle
		mockclient.EXPECT().PutBucketLifecycle(context.TODO(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, req *oss.PutBucketLifecycleRequest, optFns ...func(*oss.Options)) (*oss.PutBucketLifecycleResult, error) {
				// Should only have the rule to keep
				g.Expect(req.LifecycleConfiguration.Rules).To(gomega.HaveLen(1))
				g.Expect(*req.LifecycleConfiguration.Rules[0].ID).To(gomega.Equal(ruleToKeep))
				return &oss.PutBucketLifecycleResult{}, nil
			},
		)

		err := s3client.RemoveLifecycle(context.TODO(), "test-bucket", ruleToRemove)
		g.Expect(err).Should(gomega.Succeed())
	})

	t.Run("remove last rule should delete entire lifecycle configuration", func(t *testing.T) {
		ruleToRemove := "last-rule"
		existingRules := []oss.LifecycleRule{
			{
				ID:     oss.Ptr(ruleToRemove),
				Prefix: oss.Ptr("remove/"),
				Status: oss.Ptr("Enabled"),
			},
		}

		// Mock GetBucketLifecycle returning existing rules
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(&oss.GetBucketLifecycleResult{
			LifecycleConfiguration: &oss.LifecycleConfiguration{
				Rules: existingRules,
			},
		}, nil)

		// Mock DeleteBucketLifecycle (should be called when no rules left)
		mockclient.EXPECT().DeleteBucketLifecycle(context.TODO(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, req *oss.DeleteBucketLifecycleRequest, optFns ...func(*oss.Options)) (*oss.DeleteBucketLifecycleResult, error) {
				g.Expect(req.Bucket).ToNot(gomega.BeNil())
				g.Expect(*req.Bucket).To(gomega.Equal("test-bucket"))
				return &oss.DeleteBucketLifecycleResult{}, nil
			},
		)

		err := s3client.RemoveLifecycle(context.TODO(), "test-bucket", ruleToRemove)
		g.Expect(err).Should(gomega.Succeed())
	})

	t.Run("lifecycle configuration does not exist", func(t *testing.T) {
		serviceErr := &oss.ServiceError{
			Code:       "NoSuchLifecycleConfiguration",
			StatusCode: http.StatusNotFound,
		}
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, serviceErr)

		err := s3client.RemoveLifecycle(context.TODO(), "test-bucket", "non-existent-rule")
		g.Expect(err).Should(gomega.Succeed())
	})

	t.Run("get lifecycle error", func(t *testing.T) {
		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "get lifecycle error"))

		err := s3client.RemoveLifecycle(context.TODO(), "test-bucket", "rule-name")
		g.Expect(err).Should(gomega.HaveOccurred())
	})

	t.Run("delete lifecycle error", func(t *testing.T) {
		ruleToRemove := "last-rule"
		existingRules := []oss.LifecycleRule{
			{
				ID:     oss.Ptr(ruleToRemove),
				Prefix: oss.Ptr("remove/"),
				Status: oss.Ptr("Enabled"),
			},
		}

		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(&oss.GetBucketLifecycleResult{
			LifecycleConfiguration: &oss.LifecycleConfiguration{
				Rules: existingRules,
			},
		}, nil)

		mockclient.EXPECT().DeleteBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "delete lifecycle error"))

		err := s3client.RemoveLifecycle(context.TODO(), "test-bucket", ruleToRemove)
		g.Expect(err).Should(gomega.HaveOccurred())
	})

	t.Run("put lifecycle error when updating", func(t *testing.T) {
		ruleToRemove := "rule-to-remove"
		ruleToKeep := "rule-to-keep"
		existingRules := []oss.LifecycleRule{
			{
				ID:     oss.Ptr(ruleToRemove),
				Prefix: oss.Ptr("remove/"),
				Status: oss.Ptr("Enabled"),
			},
			{
				ID:     oss.Ptr(ruleToKeep),
				Prefix: oss.Ptr("keep/"),
				Status: oss.Ptr("Enabled"),
			},
		}

		mockclient.EXPECT().GetBucketLifecycle(context.TODO(), gomock.Any()).Return(&oss.GetBucketLifecycleResult{
			LifecycleConfiguration: &oss.LifecycleConfiguration{
				Rules: existingRules,
			},
		}, nil)

		mockclient.EXPECT().PutBucketLifecycle(context.TODO(), gomock.Any()).Return(nil, errors.New(errors.UnrecognizedError, "put lifecycle error"))

		err := s3client.RemoveLifecycle(context.TODO(), "test-bucket", ruleToRemove)
		g.Expect(err).Should(gomega.HaveOccurred())
	})
}

// func TestOSSExites(t *testing.T) {
// 	ststoken, err := getStSToken()
// 	if err != nil {
// 		t.Errorf("getStSToken failed: %v", err)
// 	}
// 	config := new(openapicred.Config).
// 		SetType("sts").
// 		// set AccessKey Id
// 		SetAccessKeyId(ststoken.AccessKeyId).
// 		// set AccessKey Secret
// 		SetAccessKeySecret(ststoken.AccessKeySecret).
// 		// set token
// 		SetSecurityToken(ststoken.StsToken)

// 	stsCredential, err := openapicred.NewCredential(config)
// 	if err != nil {
// 		t.Fatalf("failed to create credential: %v", err)
// 	}
// 	provider := credentials.CredentialsProviderFunc(func(ctx context.Context) (credentials.Credentials, error) {
// 		cred, err := stsCredential.GetCredential()
// 		if err != nil {
// 			return credentials.Credentials{}, err
// 		}
// 		return credentials.Credentials{
// 			AccessKeyID:     *cred.AccessKeyId,
// 			AccessKeySecret: *cred.AccessKeySecret,
// 			SecurityToken:   *cred.SecurityToken,
// 		}, nil
// 	})
// 	cfg := oss.LoadDefaultConfig().WithRegion("ap-southeast-1").WithCredentialsProvider(provider)
// 	s3 := NewS3(cfg)
// 	s3.BucketExists(context.Background(), "o11y-test-shared-ap-southeast-1-tmp")
// }
