package controllers

import (
	"context"
	"fmt"
	"testing"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/stretchr/testify/assert"
	"github.com/upbound/provider-aws/apis/ec2/v1beta1"

	alivpcv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/vpc/v1alpha1"
	. "github.com/onsi/gomega"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/test/kubemock"
	"go.uber.org/mock/gomock"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestEnsureChartDeploymentReady(t *testing.T) {
	cases := map[string]struct {
		initGetter func(cli client.Client) k8s.ClientGetter
		expect     func(g *WithT, err error)
	}{
		"empty Getter": {
			initGetter: func(cli client.Client) k8s.ClientGetter {
				return &k8s.EmptyGetter{}
			},
			expect: func(g *WithT, err error) {
				g.Expect(err).Should(Equal(fmt.Errorf("empty getter")))
			},
		},
		"deloyment not found": {
			initGetter: func(cli client.Client) k8s.ClientGetter {
				app := &appsv1.Deployment{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "notfound",
						Namespace: "notfound",
					},
				}
				cli.Create(context.Background(), app)
				return &MockClientGetter{
					Client: fake.NewClientBuilder().Build(),
				}
			},

			expect: func(g *WithT, err error) {
				g.Expect(client.IgnoreNotFound(err)).Should(BeNil())
			},
		},
		"deployment not ready": {
			initGetter: func(cli client.Client) k8s.ClientGetter {
				app := &appsv1.Deployment{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "kube-state-metrics",
						Namespace: "monitoring",
					},
				}
				cli.Create(context.Background(), app)
				return &MockClientGetter{
					Client: cli,
				}
			},
			expect: func(g *WithT, err error) {
				g.Expect(err).Should(Equal(fmt.Errorf("chart deployment monitoring/kube-state-metrics not ready")))
			},
		},
		"deployment condition ready": {
			initGetter: func(cli client.Client) k8s.ClientGetter {
				app := &appsv1.Deployment{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "kube-state-metrics",
						Namespace: "monitoring",
					},
					Status: appsv1.DeploymentStatus{
						Conditions: []appsv1.DeploymentCondition{
							{
								Type:   appsv1.DeploymentAvailable,
								Status: "True",
							},
						},
					},
				}
				cli.Create(context.Background(), app)
				return &MockClientGetter{
					Client: cli,
				}
			},
			expect: func(g *WithT, err error) {
				g.Expect(err).Should(Succeed())
			},
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewWithT(t)
			cli := fake.NewClientBuilder().Build()
			getter := c.initGetter(cli)

			c.expect(g, EnsureChartDeploymentReady(context.Background(), getter, types.NamespacedName{
				Namespace: "monitoring",
				Name:      "kube-state-metrics",
			}))
		})
	}
}

func TestConvertToCloudTags(t *testing.T) {
	inputTags := map[string]string{
		"tags.tidbcloud.com/tenant":   "my-tenant",
		"tags.tidbcloud.com/project":  "project-alpha",
		"tags.tidbcloud.com/env-name": "prod-env-1.0",
		"tags.tidbcloud.com/very-long-key-name-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890": "very-long-value-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890xxxxx33323456789efghijklmnopqrstuvwxyz1234567890xxxxx33323456789",
		"tags.tidbcloud.com/-invalid-gcp-key": "some-value", // For GCP specific test
		"another.prefix/unrelated":            "data",       // Should be ignored
	}

	tests := []struct {
		name      string
		cloudType string
		input     map[string]string
		expected  map[string]string
	}{
		{
			name:      "AWS Conversion",
			cloudType: consts.ProviderTypeEKS,
			input:     inputTags,
			expected: map[string]string{
				"tenant":           "my-tenant",
				"project":          "project-alpha",
				"env-name":         "prod-env-1.0",
				"-invalid-gcp-key": "some-value",
				"very-long-key-name-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijkl": "very-long-value-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890xxxxx33323456789efghijklmnopqrstuvw",
			},
		},
		{
			name:      "GCP Conversion",
			cloudType: consts.ProviderTypeGKE,
			input:     inputTags,
			expected: map[string]string{
				"tenant":   "my-tenant",
				"project":  "project-alpha",
				"env-name": "prod-env-10",
				// "-invalid-gcp-key" will be dropped because it doesn't start with a letter
				"very-long-key-name-that-exceeds-many-limits-abcdefghijklmnopqrs": "very-long-value-that-exceeds-many-limits-abcdefghijklmnopqrstuv",
			},
		},
		{
			name:      "Azure Conversion",
			cloudType: consts.ProviderTypeAKS,
			input:     inputTags,
			expected: map[string]string{
				"tenant":           "my-tenant",
				"project":          "project-alpha",
				"env-name":         "prod-env-1.0",
				"-invalid-gcp-key": "some-value",
				"very-long-key-name-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890": "very-long-value-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890xxxxx33323456789efghijklmnopqrstuvw",
			},
		},
		{
			name:      "Aliyun Conversion",
			cloudType: consts.ProviderTypeACK,
			input:     inputTags,
			expected: map[string]string{
				"tenant":           "my-tenant",
				"project":          "project-alpha",
				"env-name":         "prod-env-1.0",
				"-invalid-gcp-key": "some-value",
				"very-long-key-name-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijkl": "very-long-value-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmno",
			},
		},
		{
			name:      "Empty Input",
			cloudType: consts.ProviderTypeEKS,
			input:     map[string]string{},
			expected:  map[string]string{},
		},
		{
			name:      "No Matching Prefix",
			cloudType: consts.ProviderTypeGKE,
			input: map[string]string{
				"other.tag/name": "value",
			},
			expected: map[string]string{},
		},
		{
			name:      "Unknown Cloud Type",
			cloudType: "unknown", // An unsupported cloud type
			input: map[string]string{
				"tags.tidbcloud.com/test": "value",
			},
			expected: map[string]string{}, // Should return empty for unknown types
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			actualTags := ConvertToCloudTags(tt.input, tt.cloudType)
			assert.Equal(t, tt.expected, actualTags, "Tags mismatch for cloud type %s", tt.cloudType)
		})
	}
}

func TestEffectiveCloudTagsPrecedence(t *testing.T) {
	got := EffectiveCloudTags(
		consts.ProviderTypeEKS,
		map[string]string{
			"owner": "additional-owner",
			"tier":  "additional-tier",
		},
		map[string]string{
			"tags.tidbcloud.com/owner":  "label-owner",
			"tags.tidbcloud.com/region": "label-region",
		},
		map[string]string{
			"tags.tidbcloud.com/owner": "spec-owner",
			"tags.tidbcloud.com/team":  "spec-team",
		},
		map[string]string{
			"tags.tidbcloud.com/owner": "dataplane-owner",
			"tags.tidbcloud.com/env":   "prod",
		},
	)

	assert.Equal(t, map[string]string{
		"owner":  "dataplane-owner",
		"region": "label-region",
		"tier":   "additional-tier",
		"team":   "spec-team",
		"env":    "prod",
	}, got)
}

func TestEnsureDataPlaneIDLabel(t *testing.T) {
	obj := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Labels: map[string]string{
				"existing": "label",
			},
		},
	}

	EnsureDataPlaneIDLabel(obj, "dp-1")

	assert.Equal(t, "dp-1", obj.Labels[v1alpha1.DataPlaneIDKey])
	assert.Equal(t, "label", obj.Labels["existing"])
}

func TestManagementPoliciesPatch(t *testing.T) {
	g := NewWithT(t)

	// Create a fake client with an existing resource
	existingResource := &v1beta1.VPCEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test-endpoint",
		},
		Spec: v1beta1.VPCEndpointSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ManagementPolicies: xpv1.ManagementPolicies{xpv1.ManagementActionCreate},
			},
		},
	}
	s := runtime.NewScheme()
	v1beta1.SchemeBuilder.AddToScheme(s)
	fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(existingResource).Build()

	// Apply the patch
	err := k8s.PatchObject(context.Background(), fakeClient, existingResource, ManagementPoliciesPatch)
	g.Expect(err).Should(Succeed())

	// Fetch the updated resource
	fetchedResource := &v1beta1.VPCEndpoint{}
	err = fakeClient.Get(context.Background(), types.NamespacedName{Name: "test-endpoint"}, fetchedResource)
	g.Expect(err).Should(Succeed())

	// Verify that the management policies are set correctly
	g.Expect(fetchedResource.Spec.ResourceSpec.ManagementPolicies).Should(Equal(xpv1.ManagementPolicies{xpv1.ManagementActionAll}))
}

func TestToAliyunValidTag(t *testing.T) {
	testcases := []struct {
		input          string
		expectedOutput string
	}{
		{
			input:          "acs:abc",
			expectedOutput: "abc",
		},
		{
			input:          "aliyunabc",
			expectedOutput: "abc",
		},
		{
			input:          "abc-http://def.com",
			expectedOutput: "abc-def.com",
		},
		{
			input:          "abc-https://def.com",
			expectedOutput: "abc-def.com",
		},
		{
			input:          "http://aliyundef.com",
			expectedOutput: "def.com",
		},
		{
			input:          "https://acs:aliyundef.com",
			expectedOutput: "def.com",
		},
		{
			input:          "acs:aliyunaliyunacs:def",
			expectedOutput: "def",
		},
		{
			input:          "acs:very-long-key-name-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890",
			expectedOutput: "very-long-key-name-that-exceeds-many-limits-abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghijkl",
		},
	}
	for _, tc := range testcases {
		validTag := toAliyunValidTag(tc.input)
		assert.Equal(t, tc.expectedOutput, validTag)
	}
}

func TestDeleteUpboundResource(t *testing.T) {
	t.Run("get object error", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		obj := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-vpc",
			},
		}
		mockCli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError)
		err := DeleteUpboundResource(context.Background(), mockCli, obj)
		assert.Error(t, err)
		assert.Equal(t, assert.AnError, err)
	})
	t.Run("object is not found", func(t *testing.T) {
		ctl := gomock.NewController(t)
		ctl.Finish()
		mockCli := kubemock.NewMockClient(ctl)
		obj := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-vpc",
			},
		}
		gvk := obj.GroupVersionKind()
		mockCli.EXPECT().Get(gomock.Any(), client.ObjectKey{Name: "test-vpc"}, obj).Return(
			apierrors.NewNotFound(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, obj.Name))
		err := DeleteUpboundResource(context.Background(), mockCli, obj)
		assert.NoError(t, err)
	})
	t.Run("object is deleting", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		obj := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-vpc",
			},
		}
		mockCli.EXPECT().Get(gomock.Any(), client.ObjectKey{Name: "test-vpc"}, obj).DoAndReturn(
			func(_ context.Context, _ client.ObjectKey, obj client.Object, _ ...client.GetOption) error {
				obj.SetDeletionTimestamp(&metav1.Time{Time: time.Now()})
				return nil
			})
		err := DeleteUpboundResource(context.Background(), mockCli, obj)
		assert.NoError(t, err)
	})
	t.Run("patch object error", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		obj := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-vpc",
			},
		}
		mockCli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
		mockCli.EXPECT().Patch(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError)
		err := DeleteUpboundResource(context.Background(), mockCli, obj)
		assert.Error(t, err)
		assert.Equal(t, assert.AnError, err)
	})
	t.Run("delete object error", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		obj := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-vpc",
			},
		}
		mockCli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
		mockCli.EXPECT().Patch(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
		mockCli.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(assert.AnError)
		err := DeleteUpboundResource(context.Background(), mockCli, obj)
		assert.Error(t, err)
		assert.Equal(t, assert.AnError, err)
	})
	t.Run("delete object succcessfully", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		obj := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-vpc",
			},
		}
		mockCli.EXPECT().Get(gomock.Any(), client.ObjectKey{Name: "test-vpc"}, obj).Return(nil)
		mockCli.EXPECT().Patch(gomock.Any(), obj, gomock.Any()).Return(nil)
		mockCli.EXPECT().Delete(gomock.Any(), obj).Return(nil)
		err := DeleteUpboundResource(context.Background(), mockCli, obj)
		assert.NoError(t, err)
	})
}
