package cluster

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

	"github.com/aws/aws-sdk-go-v2/aws"
	elbapi "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
	elbtypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	awskmsv1alpha1 "github.com/crossplane/provider-aws/apis/kms/v1alpha1"
	"github.com/go-logr/logr"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	"k8s.io/utils/pointer"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"github.com/tidbcloud/config-service/sdk/api"
	"github.com/tidbcloud/config-service/sdk/mock"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	awsfake "github.com/tidbcloud/infra-api/pkg/providers/aws/fake"
	"github.com/tidbcloud/shared-provider/pkg/gitops"
	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/namespace"
)

const (
	recorderBufSize  = 4096
	defaultNamespace = "cluster-test"
)

func TestReconcile(t *testing.T) {
	deletionTimestamp := metav1.Now()
	objs := []runtime.Object{
		&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "shared-tier-test-network-ns"}},
	}
	sharedProviderSpec := &types.SharedProviderSpec{
		TiDB: map[string]types.Resource{
			"tidb.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("0.8"),
						corev1.ResourceMemory: resource.MustParse("16Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("8"),
						corev1.ResourceMemory: resource.MustParse("16Gi"),
					},
				},
			},
		},
		TiKV: map[string]types.Resource{
			"tikv.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:     resource.MustParse("0.8"),
						corev1.ResourceMemory:  resource.MustParse("16Gi"),
						corev1.ResourceStorage: resource.MustParse("10Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("8"),
						corev1.ResourceMemory: resource.MustParse("16Gi"),
					},
				},
			},
		},
		PD: map[string]types.Resource{
			"pd.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:     resource.MustParse("0.8"),
						corev1.ResourceMemory:  resource.MustParse("16Gi"),
						corev1.ResourceStorage: resource.MustParse("10Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("8"),
						corev1.ResourceMemory: resource.MustParse("16Gi"),
					},
				},
			},
		},
		TiFlash: map[string]types.Resource{
			"tiflash.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:     resource.MustParse("0.8"),
						corev1.ResourceMemory:  resource.MustParse("16Gi"),
						corev1.ResourceStorage: resource.MustParse("10Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("8"),
						corev1.ResourceMemory: resource.MustParse("16Gi"),
					},
				},
			},
		},
	}
	provider := &cloudv1.Provider{
		ObjectMeta: metav1.ObjectMeta{
			Name: types.ProviderTypeAWSSharedTier,
		},
		Spec: cloudv1.ProviderSpec{
			Type: types.ProviderTypeAWSSharedTier,
			Regions: []cloudv1.Region{{
				Name:  "test-region",
				Zones: []string{"z-a", "z-b", "z-c"},
			}},
			StorageClasses: []cloudv1.StorageClass{
				{
					Name: "default",
				},
			},
		},
	}
	types.SetSharedProviderSpec(provider, sharedProviderSpec)
	infraObjs := []runtime.Object{
		&cloudv1.Network{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-network",
				Namespace: defaultNamespace,
				Labels: map[string]string{
					cloudv1.ProjectLabel: "1",
				},
				Annotations: map[string]string{
					namespace.NamespaceKey: "shared-tier-test-network-ns",
				},
			},
			Spec: cloudv1.NetworkSpec{
				Region:   "test-region",
				Provider: types.ProviderTypeAWSSharedTier,
				CIDR:     "10.240.0.1/12",
			},
			Status: cloudv1.NetworkStatus{
				Phase: cloudv1.NetworkPhaseNormal,
			},
		},
		provider,
	}

	cases := []struct {
		name string

		objs      []runtime.Object
		infraObjs []runtime.Object

		requeue      bool
		requeueAfter time.Duration

		errstr string

		events []string
	}{
		{
			name: "create_cluster",
			objs: []runtime.Object{
				&awskmsv1alpha1.Key{
					ObjectMeta: metav1.ObjectMeta{
						Name: defaultNamespace + "-create_cluster",
					},
					Spec: awskmsv1alpha1.KeySpec{
						ResourceSpec: xpv1.ResourceSpec{
							ProviderConfigReference: &xpv1.Reference{
								Name: "provider-aws",
							},
						},
						ForProvider: awskmsv1alpha1.KeyParameters{
							Region: "test-region",
						},
					},
					Status: awskmsv1alpha1.KeyStatus{
						AtProvider: awskmsv1alpha1.KeyObservation{
							KeyID: pointer.StringPtr("my-key-id"),
						},
					},
				},
			},
			infraObjs: []runtime.Object{
				&cloudv1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "create_cluster",
						Namespace: defaultNamespace,
						Labels: map[string]string{
							cloudv1.ProviderTypeLabel:    types.ProviderTypeAWSSharedTier,
							"tags.tidbcloud.com/tenant":  "1",
							"tags.tidbcloud.com/project": "1",
						},
					},
					Spec: cloudv1.ClusterSpec{
						Cluster: cloudv1.ClusterTopology{
							TiDB: cloudv1.TiDB{
								Replicas:     2,
								InstanceType: "tidb.sharedtier",
							},
							TiKV: cloudv1.TiKV{
								Replicas:     3,
								InstanceType: "tikv.sharedtier",
								StorageSize:  resource.MustParse("10Gi"),
							},
							PD: cloudv1.PD{
								Replicas:     3,
								InstanceType: "pd.sharedtier",
								StorageSize:  resource.MustParse("1Gi"),
							},
							TiFlash: cloudv1.TiFlash{
								Replicas:     1,
								InstanceType: "tiflash.sharedtier",
								StorageSize:  resource.MustParse("15Gi"),
							},
						},
						NetworkName: "test-network",
					},
				},
			},
			requeueAfter: defaultWaitInterval, // no ready condition in tc
		},
		{
			requeueAfter: defaultAccumulatePeriodDuration,
			name:         "reconcile_cluster",
			objs: []runtime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "dbreconcile_cluster",
						Namespace: "shared-tier-test-network-ns",
					},
					Spec: v1alpha1.TidbClusterSpec{
						TiDB:    &v1alpha1.TiDBSpec{Replicas: 1},
						TiKV:    &v1alpha1.TiKVSpec{Replicas: 1},
						PD:      &v1alpha1.PDSpec{Replicas: 1},
						TiFlash: &v1alpha1.TiFlashSpec{Replicas: 1},
					},
				},
				&v1alpha1.TidbMonitor{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "dbreconcile_cluster",
						Namespace: "shared-tier-test-network-ns",
					},
				},
			},
			infraObjs: []runtime.Object{
				&cloudv1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "reconcile_cluster",
						Namespace: defaultNamespace,
						Labels: map[string]string{
							cloudv1.ProviderTypeLabel:    types.ProviderTypeAWSSharedTier,
							"tags.tidbcloud.com/tenant":  "1",
							"tags.tidbcloud.com/project": "1",
						},
					},
					Spec: cloudv1.ClusterSpec{
						Cluster: cloudv1.ClusterTopology{
							TiDB: cloudv1.TiDB{
								Replicas:     2,
								InstanceType: "tidb.sharedtier",
							},
							TiKV: cloudv1.TiKV{
								Replicas:     3,
								InstanceType: "tikv.sharedtier",
								StorageSize:  resource.MustParse("10Gi"),
							},
							PD: cloudv1.PD{
								Replicas:     3,
								InstanceType: "pd.sharedtier",
								StorageSize:  resource.MustParse("1Gi"),
							},
							TiFlash: cloudv1.TiFlash{
								Replicas:     1,
								InstanceType: "tiflash.sharedtier",
								StorageSize:  resource.MustParse("15Gi"),
							},
						},
						NetworkName: "test-network",
					},
					Status: cloudv1.ClusterStatus{
						Phase: cloudv1.ClusterPhaseRunning,
					},
				},
			},
		},
		{
			requeueAfter: defaultAccumulatePeriodDuration,
			name:         "unavailable_cluster",
			objs: []runtime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "dbunavailable_cluster",
						Namespace: "shared-tier-test-network-ns",
					},
					Spec: v1alpha1.TidbClusterSpec{
						TiDB:    &v1alpha1.TiDBSpec{Replicas: 1},
						TiKV:    &v1alpha1.TiKVSpec{Replicas: 1},
						PD:      &v1alpha1.PDSpec{Replicas: 1},
						TiFlash: &v1alpha1.TiFlashSpec{Replicas: 1},
					},
				},
				&v1alpha1.TidbMonitor{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "dbunavailable_cluster",
						Namespace: "shared-tier-test-network-ns",
					},
				},
			},
			infraObjs: []runtime.Object{
				&cloudv1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "unavailable_cluster",
						Namespace: defaultNamespace,
						Labels: map[string]string{
							cloudv1.ProviderTypeLabel:    types.ProviderTypeAWSSharedTier,
							"tags.tidbcloud.com/tenant":  "1",
							"tags.tidbcloud.com/project": "1",
						},
					},
					Spec: cloudv1.ClusterSpec{
						Cluster: cloudv1.ClusterTopology{
							TiDB: cloudv1.TiDB{
								Replicas:     2,
								InstanceType: "tidb.sharedtier",
							},
							TiKV: cloudv1.TiKV{
								Replicas:     3,
								InstanceType: "tikv.sharedtier",
								StorageSize:  resource.MustParse("10Gi"),
							},
							PD: cloudv1.PD{
								Replicas:     3,
								InstanceType: "pd.sharedtier",
								StorageSize:  resource.MustParse("1Gi"),
							},
							TiFlash: cloudv1.TiFlash{
								Replicas:     1,
								InstanceType: "tiflash.sharedtier",
								StorageSize:  resource.MustParse("15Gi"),
							},
						},
						NetworkName: "test-network",
					},
					Status: cloudv1.ClusterStatus{
						Phase: cloudv1.ClusterPhaseUnavailable,
					},
				},
			},
		},
		{
			name: "delete_cluster",
			objs: []runtime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "delete_cluster",
						Namespace: "shared-tier-test-network-ns",
					},
					Spec: v1alpha1.TidbClusterSpec{
						TiDB:    &v1alpha1.TiDBSpec{Replicas: 1},
						TiKV:    &v1alpha1.TiKVSpec{Replicas: 1},
						PD:      &v1alpha1.PDSpec{Replicas: 1},
						TiFlash: &v1alpha1.TiFlashSpec{Replicas: 1},
					},
				},
				&v1alpha1.TidbMonitor{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "dbdelete_cluster",
						Namespace: "shared-tier-test-network-ns",
					},
				},
			},
			infraObjs: []runtime.Object{
				&cloudv1.Cluster{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "delete_cluster",
						Namespace: defaultNamespace,
						Labels: map[string]string{
							cloudv1.ProviderTypeLabel:    types.ProviderTypeAWSSharedTier,
							"tags.tidbcloud.com/tenant":  "1",
							"tags.tidbcloud.com/project": "1",
						},
						Finalizers:        []string{clusterFinalizer},
						DeletionTimestamp: &deletionTimestamp,
					},
					Spec: cloudv1.ClusterSpec{
						Cluster: cloudv1.ClusterTopology{
							TiDB: cloudv1.TiDB{
								Replicas:     2,
								InstanceType: "tidb.sharedtier",
							},
							TiKV: cloudv1.TiKV{
								Replicas:     3,
								InstanceType: "tikv.sharedtier",
								StorageSize:  resource.MustParse("10Gi"),
							},
							PD: cloudv1.PD{
								Replicas:     3,
								InstanceType: "pd.sharedtier",
								StorageSize:  resource.MustParse("1Gi"),
							},
							TiFlash: cloudv1.TiFlash{
								Replicas:     1,
								InstanceType: "tiflash.sharedtier",
								StorageSize:  resource.MustParse("15Gi"),
							},
						},
						NetworkName: "test-network",
					},
					Status: cloudv1.ClusterStatus{
						Phase: cloudv1.ClusterPhaseRunning,
					},
				},
			},
		},
	}

	for _, c := range cases {
		t.Log("test ", c.name)

		fakeAwsClient := awsfake.NewMockClient(t)
		fakeAwsClient.MockElbClient.EXPECT().DescribeLoadBalancers(gomock.All(), gomock.All()).Return(
			&elbapi.DescribeLoadBalancersOutput{
				LoadBalancers: []elbtypes.LoadBalancer{{
					CanonicalHostedZoneId: aws.String("test/test/zone-id"),
				}},
			}, nil).AnyTimes()
		fakeAwsClient.MockKmsClient.EXPECT().CreateGrant(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
		fakeAwsClient.MockKmsClient.EXPECT().
			DisableKeyRotation(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()

		r := newFakeReconciler(append(objs, c.objs...), append(infraObjs, c.infraObjs...))
		r.AwsClient = fakeAwsClient
		r.QuerierForTidbMonitor = func(_ client.ObjectKey) Querier {
			return &mockQuerier{}
		}

		req := ctrl.Request{
			NamespacedName: client.ObjectKey{
				Namespace: defaultNamespace,
				Name:      c.name,
			},
		}
		// nolint: forcetypeassert // nolint
		fakeRecorder := r.Recorder.(*record.FakeRecorder)
		var events []string
		go func() {
			for event := range fakeRecorder.Events {
				events = append(events, event)
			}
		}()

		res, err := r.Reconcile(context.Background(), req)
		if c.errstr != "" {
			assert.Error(t, err)
			assert.Equal(t, c.errstr, err.Error())
		} else {
			assert.NoError(t, err)
		}

		assert.Equal(t, c.requeueAfter, res.RequeueAfter)
		if c.requeueAfter == 0 {
			assert.Equal(t, c.requeue, res.Requeue)
		}
		assert.Equal(t, c.events, events)

		close(fakeRecorder.Events)
	}
}

func newFakeReconciler(objs, infraObjs []runtime.Object) *Reconciler {
	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme.Scheme).
		WithRuntimeObjects(objs...).
		Build()
	fakeInfraClient := fake.NewClientBuilder().
		WithScheme(scheme.Scheme).
		WithRuntimeObjects(infraObjs...).
		Build()

	fakeRecorder := record.NewFakeRecorder(recorderBufSize)
	fakeInfraRecorder := record.NewFakeRecorder(recorderBufSize)
	bandwidthHandler := NewBandwidthHandler(fakeClient, fakeInfraClient)
	return &Reconciler{
		Client:                 fakeClient,
		Cache:                  fakeClient,
		Recorder:               fakeRecorder,
		InfraClient:            fakeInfraClient,
		InfraCache:             fakeInfraClient,
		InfraRecorder:          fakeInfraRecorder,
		ConfigService:          &mock.MockClient{ClusterConfigOutput: getMockedClusterConfig()},
		PublicHostedZoneDomain: "shared.test.aws.tidbcloud.com",
		PublicHostedZoneId:     "Z329W86VRKITEST",
		BaseProxy:              "fakeproxy.com",
		EvictDuration:          time.Second,
		ConfigProvider:         gitops.NewProviderConfigProvider(),
		bandwidthHandler:       bandwidthHandler,
		Logger:                 logr.Discard(),
	}
}

func newFakeReconcileContext(cluster *cloudv1.Cluster, reconciler *Reconciler) *reconcileContext {
	if reconciler == nil {
		reconciler = newFakeReconciler(nil, []runtime.Object{cluster})
	}

	// get from ProviderTypeLabel for convenience.
	// the ProviderTypeLabel generate from infra webhook in real env.
	providerType := cluster.GetProviderType()

	sharedProviderSpec := &types.SharedProviderSpec{
		TiDB: map[string]types.Resource{
			"tidb.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("0.8"),
						corev1.ResourceMemory: resource.MustParse("2Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("8"),
						corev1.ResourceMemory: resource.MustParse("16Gi"),
					},
				},
			},
		},
		TiKV: map[string]types.Resource{
			"tikv.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:     resource.MustParse("0.8"),
						corev1.ResourceMemory:  resource.MustParse("2Gi"),
						corev1.ResourceStorage: resource.MustParse("10Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("8"),
						corev1.ResourceMemory: resource.MustParse("32Gi"),
					},
				},
			},
		},
		PD: map[string]types.Resource{
			"pd.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:     resource.MustParse("0.2"),
						corev1.ResourceMemory:  resource.MustParse("1Gi"),
						corev1.ResourceStorage: resource.MustParse("1Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("2"),
						corev1.ResourceMemory: resource.MustParse("4Gi"),
					},
				},
			},
		},
		TiFlash: map[string]types.Resource{
			"tiflash.sharedtier": {
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceCPU:     resource.MustParse("0.8"),
						corev1.ResourceMemory:  resource.MustParse("2Gi"),
						corev1.ResourceStorage: resource.MustParse("15Gi"),
					},
					Limits: corev1.ResourceList{
						corev1.ResourceCPU:    resource.MustParse("8"),
						corev1.ResourceMemory: resource.MustParse("32Gi"),
					},
				},
			},
		},
	}
	provider := &cloudv1.Provider{
		ObjectMeta: metav1.ObjectMeta{
			Name: providerType,
		},
		Spec: cloudv1.ProviderSpec{
			Type: providerType,
			Regions: []cloudv1.Region{{
				Name:  "test-region",
				Zones: []string{"z-a", "z-b", "z-c"},
			}},
			StorageClasses: []cloudv1.StorageClass{
				{
					Name: "default",
				},
			},
		},
	}
	types.SetSharedProviderSpec(provider, sharedProviderSpec)
	network := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test",
			Namespace: cluster.Namespace,
			Labels: map[string]string{
				cloudv1.ProjectLabel: "1",
			},
			Annotations: map[string]string{
				namespace.NamespaceKey: "test-user-ns",
			},
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "test-region",
			Provider: providerType,
			CIDR:     "10.240.0.1/12",
		},
		Status: cloudv1.NetworkStatus{
			Phase: cloudv1.NetworkPhaseNormal,
		},
	}
	types.SetSharedNetworkStatus(network, &types.SharedNetworkStatus{
		BackupBucket: "s3_bucket",
	})
	cluster.Spec.NetworkName = network.Name

	ns := namespace.GetOwnedNamespaceNameFromNetwork(network)

	return &reconcileContext{
		Reconciler:   reconciler,
		cluster:      cluster,
		network:      network,
		provider:     provider,
		providerType: providerType,
		tcKey:        client.ObjectKey{Name: cluster.Name, Namespace: ns},
		tmKey:        client.ObjectKey{Name: cluster.Name, Namespace: ns},
		ctx:          context.TODO(),
		querier:      &mockQuerier{},
		Logger:       logr.Discard(),
		backupBucket: "s3_bucket",
		backupRole:   "",
	}
}

func getMockedClusterConfig() *api.DescribeClusterConfigOutput {
	return &api.DescribeClusterConfigOutput{
		Version: "v5.0.2",
		ConfigMeta: &api.ConfigMeta{
			ID:            "base",
			GitCommitID:   "TEST",
			ConfigVersion: 1,
		},
		Components: []api.ComponentOutput{{
			Component: api.ComponentPD,
			Image:     api.Image{BaseImage: "gcr.io/pingcap-public/dbaas/pd"},
			Config:    api.NewTomlConfig(map[string]interface{}{}),
		}, {
			Component: api.ComponentTiDB,
			Image:     api.Image{BaseImage: "gcr.io/pingcap-public/dbaas/tidb"},
			Config:    api.NewTomlConfig(map[string]interface{}{}),
		}, {
			Component: api.ComponentTiKV,
			Image:     api.Image{BaseImage: "gcr.io/pingcap-public/dbaas/tikv"},
			Config:    api.NewTomlConfig(map[string]interface{}{}),
		}, {
			Component: api.ComponentTiFlash,
			Image:     api.Image{BaseImage: "gcr.io/pingcap-public/dbaas/tiflash"},
			Config:    api.NewTomlConfig(map[string]interface{}{}),
		}},
	}
}

type mockQuerier struct {
	Response QueryResponse
}

var _ Querier = &mockQuerier{}

func (q *mockQuerier) Query(ctx context.Context, path string, values map[string]string) (body []byte, err error) {
	body, err = json.Marshal(q)
	return
}
