package cluster

import (
	"context"
	"fmt"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	ec2v1alpha1 "github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	"github.com/golang/mock/gomock"
	. "github.com/onsi/gomega"
	appv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	storagev1 "k8s.io/api/storage/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/record"
	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/gardener/core/apis/v1beta1"
	"github.com/tidbcloud/infra-api/pkg/o11y"
	o11yfake "github.com/tidbcloud/infra-api/pkg/o11y/fake"
	awsfake "github.com/tidbcloud/infra-api/pkg/providers/aws/fake"
)

func TestMetricAgent(t *testing.T) {
	// Define crd objects
	provider := "aws"
	region := "us-west-2"
	vpcID01 := "vpc-fake-id-01"
	shootTechnicalID01 := "shoot--unit--test"
	o11yID := "fake-o11y-id"
	providerConfig := "provider-aws"
	network01 := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name: "network_01",
		},
		Spec: cloudv1.NetworkSpec{
			Provider: provider,
			Region:   region,
		},
		Status: cloudv1.NetworkStatus{
			FromProvider: runtime.RawExtension{
				Object: &cloudv1.GardenerNetworkStatus{
					NodeRole: fmt.Sprintf("%s-%s", shootTechnicalID01, "node"),
					AWS: cloudv1.GardenerNetworkAWSStatus{
						VpcID: &vpcID01,
					},
				},
				Raw: nil,
			},
		},
	}
	cluster01 := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: "cluster_01",
			Labels: map[string]string{
				consts.ClusterTagKey: "cluster_01",
				consts.ProjectTagKey: "project_01",
				consts.TenantTagKey:  "tenant_01",
			},
		},
		Spec: cloudv1.ClusterSpec{
			Cluster: cloudv1.ClusterTopology{
				TiDB: cloudv1.TiDB{Replicas: 1},
				PD:   cloudv1.PD{Replicas: 1},
				TiKV: cloudv1.TiKV{Replicas: 1},
			},
		},
	}
	shoot01 := &v1beta1.Shoot{
		ObjectMeta: metav1.ObjectMeta{
			Name: "shoot_01",
		},
		Status: v1beta1.ShootStatus{
			TechnicalID: shootTechnicalID01,
		},
	}
	agentDeploy01 := &appv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "agent_01",
			Namespace: fmt.Sprintf("tidb%s", cluster01.Name),
		},
		Spec: appv1.DeploymentSpec{},
	}
	promConfigMap := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      prometheusConfigMapName,
			Namespace: fmt.Sprintf("tidb%s", cluster01.Name),
		},
		Data: map[string]string{
			"prometheus.yml": `
# my global config
global:
  scrape_interval: 15s
  evaluation_interval: 30s
  # scrape_timeout is set to the global default (10s).

  external_labels:
    monitor: codelab
    foo: bar
scrape_configs:
  - job_name: prometheus
    honor_labels: true
  - job_name: service-kubernetes-namespaces
    kubernetes_sd_configs:
      - role: endpoints
        api_server: "https://localhost:1234"
        namespaces:
          names:
            - default
`,
		},
	}
	endpointName := "fake-dns-name-shoot_01-m"

	// Set up context
	g := NewGomegaWithT(t)
	g.Expect(ec2v1alpha1.SchemeBuilder.AddToScheme(scheme.Scheme)).Should(Succeed())
	g.Expect(cloudv1.AddToScheme(scheme.Scheme)).Should(Succeed())
	infraClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(network01, cluster01).Build()
	shootClient := fake.NewClientBuilder().WithObjects(promConfigMap).Build()
	ctx := context.Background()
	awsClient := awsfake.NewMockClient(t)
	awsClient.MockEc2Client.EXPECT().DescribeSubnets(gomock.All(), gomock.All(), gomock.All()).Return(
		&ec2.DescribeSubnetsOutput{
			Subnets: []ec2types.Subnet{
				{
					SubnetId: aws.String("subnet_id"),
					Tags: []ec2types.Tag{
						{
							Key:   aws.String("k"),
							Value: aws.String("v"),
						},
					},
				},
			},
		}, nil).MinTimes(1)
	awsClient.MockEc2Client.EXPECT().DescribeVpcEndpointServices(gomock.Any(), gomock.Any(), gomock.Any()).Return(
		&ec2.DescribeVpcEndpointServicesOutput{
			ServiceDetails: []ec2types.ServiceDetail{
				{
					AvailabilityZones: []string{"az-1"},
				},
			},
		}, nil).MinTimes(1)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	o11yApi := o11yfake.NewMockApi(ctrl)
	o11yApi.EXPECT().CreateAwsO11yInfra(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
		&o11y.ObservabilityCluster{
			ClusterBaseInfo: o11y.ClusterBaseInfo{
				ID: o11yID,
			},
			MonitorInfo: o11y.MonitorInfo{
				EndpointServiceName: "fake-endpoint-service-name",
				DnsName:             aws.String("fake-dns-name.example.com"),
			},
		}, nil).MinTimes(1)
	o11yApi.EXPECT().DeleteO11yInfra(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
		&o11y.ObservabilityCluster{
			MonitorInfo: o11y.MonitorInfo{
				EndpointServiceName: "fake-endpoint-service-name",
				DnsName:             aws.String("fake-dns-name"),
			},
		}, nil).MinTimes(1)
	o11yApi.EXPECT().InstallAgentWithHelm(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
		// An closure to mock `kubectl apply rendered_manifest.yaml` behavior.
		func(manifestDirectoryPrefix, chartRelativePath, releaseName, namespace, k8sVersion, kubeConfig, agentChartValues interface{}) (controllerutil.OperationResult, string, error) {
			chartValues := string(agentChartValues.([]byte))
			op, err := controllerutil.CreateOrUpdate(ctx, shootClient, agentDeploy01, func() error { return nil })
			return op, chartValues, err
		},
	).MinTimes(1)
	r := &reconcileContext{}
	r.ClusterReconciler = &ClusterReconciler{}
	r.Client = infraClient
	r.shootClient = shootClient
	r.Target = cluster01
	r.Network = network01
	r.networkGardenStatus = network01.Status.FromProvider.Object.(*cloudv1.GardenerNetworkStatus)
	r.o11yApi = o11yApi
	r.shoot = shoot01
	r.AWS = awsClient
	r.shootKubeConfig = []byte{}
	r.ClusterReconciler.recorder = record.NewFakeRecorder(100)
	r.providerGardenerSpec = &cloudv1.GardenerProviderSpec{
		AWS: cloudv1.AwsConfig{
			CrossplaneProvider: providerConfig,
		},
	}
	r.o11yConfig = &cloudv1.ObservabilityInfraConfig{
		Version: 0,
	}

	// Run the creation.
	err := r.ensureClusterAgent(ctx)

	// Check the creation output.
	g.Expect(err).Should(Succeed())
	// Assert storage class is created.
	storageClass := &storagev1.StorageClass{
		ObjectMeta: metav1.ObjectMeta{
			Name: agentStorageClassName,
		},
	}
	err = shootClient.Get(ctx, ctrlclient.ObjectKeyFromObject(storageClass), storageClass)
	g.Expect(err).Should(Succeed())
	// Assert vpc interface endpoint crd is created.
	endpoint := &v1alpha1.VPCEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: endpointName,
		},
	}
	err = infraClient.Get(ctx, ctrlclient.ObjectKeyFromObject(endpoint), endpoint)
	g.Expect(err).Should(Succeed())
	// Assert agent deployment is created.
	deployRes := &appv1.Deployment{}
	err = shootClient.Get(ctx, ctrlclient.ObjectKeyFromObject(agentDeploy01), deployRes)
	g.Expect(err).Should(Succeed())
	// Assert o11y annotations are added to cluster crd.
	g.Expect(cluster01.Annotations).Should(HaveKeyWithValue(consts.MetricInterfaceEndpointNameAnnotationKey, endpointName))
	g.Expect(cluster01.Annotations).Should(HaveKeyWithValue(consts.O11yIDAnnotationKey, o11yID))

	// Run the deletion.
	err = r.deleteClusterAgent(ctx)

	// Check the deletion output.
	g.Expect(err).Should(Succeed())
}
