package gardener

import (
	"context"
	"fmt"
	"sort"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/iam"
	"github.com/aws/aws-sdk-go-v2/service/iam/types"
	"github.com/golang/mock/gomock"
	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	awsfake "github.com/tidbcloud/infra-api/pkg/providers/aws/fake"
	gcpfake "github.com/tidbcloud/infra-api/pkg/providers/gcp/fake"
)

func TestEnsureGcpDnsZone(t *testing.T) {
	type expect struct {
		called       bool
		project      string
		region       string
		vpcName      string
		publicDomain string
	}
	cases := map[string]struct {
		network  *cloudv1.Network
		provider *cloudv1.Provider
		vpcName  string
		expect   expect
	}{
		"CreateDnsZone": {
			network: &cloudv1.Network{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
				Spec: cloudv1.NetworkSpec{
					Region: "test-region",
				},
			},
			provider: &cloudv1.Provider{
				Spec: cloudv1.ProviderSpec{
					ForProvider: &runtime.RawExtension{
						Object: &cloudv1.GardenerProviderSpec{
							GCP: cloudv1.GcpConfig{
								ProjectID:              "test-project",
								PublicHostedZoneDomain: "test.com",
							},
						},
					},
				},
			},
			vpcName: "test",
			expect: expect{
				called:       true,
				project:      "test-project",
				region:       "test-region",
				vpcName:      "test",
				publicDomain: "test.com",
			},
		},
		"AlreadyCreated": {
			network: &cloudv1.Network{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test2",
				},
				Spec: cloudv1.NetworkSpec{
					Region: "test-region",
				},
				Status: cloudv1.NetworkStatus{
					FromProvider: runtime.RawExtension{
						Object: &cloudv1.GardenerNetworkStatus{
							GCP: cloudv1.GardenerNetworkGCPStatus{
								HostedZoneID: pointer.String("test-id"),
							},
						},
					},
				},
			},
			provider: &cloudv1.Provider{
				Spec: cloudv1.ProviderSpec{
					ForProvider: &runtime.RawExtension{
						Object: &cloudv1.GardenerProviderSpec{
							GCP: cloudv1.GcpConfig{
								ProjectID:              "test-project",
								PublicHostedZoneDomain: "test.com",
							},
						},
					},
				},
			},
			vpcName: "test",
			expect: expect{
				called: false,
			},
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			ctx := context.Background()
			client := fake.NewClientBuilder().Build()
			ctrl := gomock.NewController(t)
			gcpApi := gcpfake.NewMockGCPAPI(ctrl)

			r := &NetworkReconciler{Client: client, gcpapi: gcpApi}
			dnsName := fmt.Sprintf("%s.%s.", tc.expect.region, tc.expect.publicDomain)

			expectCall := gcpApi.EXPECT().CreateDnsZone(tc.expect.project, tc.expect.vpcName, tc.expect.vpcName, dnsName)
			if tc.expect.called {
				expectCall.MinTimes(1)
			} else {
				expectCall.MaxTimes(0)
			}

			err := r.ensureGCPDnsZone(ctx, tc.provider, tc.network, "test")
			g.Expect(err)
			t.Log(err)
		})
	}
}

func TestGetNetworkClusters(t *testing.T) {
	g := NewGomegaWithT(t)

	ns := "ns1"
	networkName := "net"
	var objs []client.Object = []client.Object{
		// a match cluster
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ns,
				Name:      "name1",
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: networkName,
			},
		},
		// a match cluster
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ns,
				Name:      "name2",
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: networkName,
			},
		},
		// a not match cluster with different network name
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ns,
				Name:      "name3",
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: "not-match",
			},
		},
		// a not match cluster with different namespace
		&cloudv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "not-match",
				Name:      "name1",
			},
			Spec: cloudv1.ClusterSpec{
				NetworkName: networkName,
			},
		},
	}

	client := fake.NewClientBuilder().WithObjects(objs...).Build()
	ctx := context.Background()

	clusters, err := getNetworkClusters(ctx, client, ns, networkName)
	g.Expect(err).Should(Succeed())
	g.Expect(clusters).Should(HaveLen(2))
	if clusters[0].Name == "name2" {
		clusters[0], clusters[1] = clusters[1], clusters[0]
	}
	g.Expect(clusters[0].Name).Should(Equal("name1"))
	g.Expect(clusters[1].Name).Should(Equal("name2"))
}

func TestGetNetworkBackupNames(t *testing.T) {
	var (
		g = NewGomegaWithT(t)

		ns        = "ns-network-backup-test"
		shootName = "shoot"
		network   = &cloudv1.Network{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ns,
				Name:      "network",
			},
		}
		backups = []client.Object{
			&cloudv1.Backup{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: ns,
					Name:      "backup1",
				},
			},
			&cloudv1.Backup{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: ns,
					Name:      "backup2",
				},
			},
			&cloudv1.Backup{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: ns,
					Name:      "backup3",
				},
			},
		}
	)

	cloudv1.SetGardenerNetworkStatus(network, &cloudv1.GardenerNetworkStatus{ShootName: shootName})
	for _, b := range backups {
		cloudv1.SetShootBackupStatus(b.(*cloudv1.Backup), &cloudv1.ShootBackupStatus{ShootName: shootName})
	}
	// a backup in another namespace
	bAnotherNs := &cloudv1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "another-namespace",
			Name:      "backup4",
		},
	}
	cloudv1.SetShootBackupStatus(bAnotherNs, &cloudv1.ShootBackupStatus{ShootName: shootName})
	backups = append(backups, bAnotherNs)
	// a backup with another shoot name
	bAnotherShoot := &cloudv1.Backup{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      "backup5",
		},
	}
	cloudv1.SetShootBackupStatus(bAnotherShoot, &cloudv1.ShootBackupStatus{ShootName: "another-shoot"})
	backups = append(backups, bAnotherShoot)

	cli := fake.NewClientBuilder().WithObjects(backups...).Build()

	backupNames, err := getNetworkBackupNames(context.Background(), cli, network)
	g.Expect(err).Should(Succeed())
	g.Expect(backupNames).Should(HaveLen(3))

	sort.Strings(backupNames)
	g.Expect(backupNames).Should(Equal([]string{"backup1", "backup2", "backup3"}))
}

func TestCleanUpAccessKeyIfExceedQuota(t *testing.T) {
	g := NewGomegaWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	iamClient := awsfake.NewMockIAMClient(ctrl)
	ctx := context.TODO()
	username := "test"

	output := &iam.ListAccessKeysOutput{
		AccessKeyMetadata: []types.AccessKeyMetadata{
			{
				AccessKeyId: aws.String("1"),
				UserName:    aws.String(username),
			},
			{
				AccessKeyId: aws.String("2"),
				UserName:    aws.String(username),
			},
		},
	}
	iamClient.EXPECT().ListAccessKeys(gomock.Any(), &iam.ListAccessKeysInput{
		UserName: aws.String(username)}).Times(1).Return(output, nil)

	iamClient.EXPECT().DeleteAccessKey(gomock.Any(), &iam.DeleteAccessKeyInput{
		AccessKeyId: aws.String("1"),
		UserName:    aws.String(username)}).Times(1).Return(nil, nil)
	iamClient.EXPECT().DeleteAccessKey(gomock.Any(), &iam.DeleteAccessKeyInput{
		AccessKeyId: aws.String("2"),
		UserName:    aws.String(username)}).Times(1).Return(nil, nil)

	err := cleanUpAccessKeyIfExceedQuota(ctx, iamClient, username)
	g.Expect(err).Should(BeNil())
}

func TestEnsureAccessKeyForUser(t *testing.T) {
	g := NewGomegaWithT(t)
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	iamClient := awsfake.NewMockIAMClient(ctrl)
	ctx := context.TODO()
	username := "test"

	iamClient.EXPECT().CreateAccessKey(gomock.Any(), &iam.CreateAccessKeyInput{
		UserName: aws.String(username)}).Times(1).Return(&iam.CreateAccessKeyOutput{
		AccessKey: &types.AccessKey{
			AccessKeyId: aws.String("3"),
		},
	}, nil)

	ak, err := ensureAccessKeyForUser(ctx, iamClient, username)
	g.Expect(err).Should(BeNil())
	g.Expect(ak.AccessKeyId).Should(Equal(aws.String("3")))
}

func TestGetNetworkPeerNames(t *testing.T) {
	g := NewGomegaWithT(t)

	ns := "test"
	networkName := "aws-us-west-2"

	network := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      networkName,
		},
	}

	var objs []client.Object = []client.Object{
		// a match networkpeer
		&cloudv1.NetworkPeer{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ns,
				Name:      "test1",
			},
			Spec: cloudv1.NetworkPeerSpec{
				Source: networkName,
			},
		},
		// a not match networkpeer with different network name
		&cloudv1.NetworkPeer{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: ns,
				Name:      "test2",
			},
			Spec: cloudv1.NetworkPeerSpec{
				Source: "not-match",
			},
		},
		// a not match networkpeer with different namespace
		&cloudv1.NetworkPeer{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "not-match",
				Name:      "test3",
			},
			Spec: cloudv1.NetworkPeerSpec{
				Source: networkName,
			},
		},
	}

	client := fake.NewClientBuilder().WithObjects(objs...).Build()
	ctx := context.Background()

	clusters, err := getNetworkPeerNames(ctx, client, network)
	g.Expect(err).Should(Succeed())
	g.Expect(clusters).Should(HaveLen(1))
}
