package procedure

import (
	"context"
	"fmt"
	"testing"

	cpv1beta1 "github.com/crossplane/provider-aws/apis/eks/v1beta1"
	. "github.com/onsi/gomega"
	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

const (
	corefileTemplate = `.:53 {
	errors
	health%s
}`
	emptyHostBlock = `
	hosts {
	}`
	hostBlockWithEntry = `
	hosts {
		10.10.10.10 tidbcloud.com
		fallthrough
	}`
	fakeCorednsIP     = "10.10.10.10"
	fakeCorednsHost   = "tidbcloud.com"
	expectCorednsFile = `.:53 {
	errors
	health
	hosts {
		10.10.10.10 tidbcloud.com
		fallthrough
	}
}`

	eksNamespace           = "tenant-ns"
	eksName                = "eks1"
	fakeCorednsEKSHostname = "https://fake.eks1.com"
	fakeFrpsClusterIP      = "10.10.10.10"
)

var (
	fakeCoreDNSCMInfo ProcedureInfo
	fakeCoreDNSEKSObj *cpv1beta1.Cluster
	fakeCoreDNSSvc    *corev1.Service
)

func init() {
	fakeCoreDNSEKSObj = &cpv1beta1.Cluster{}
	fakeCoreDNSEKSObj.Name = eksName
	fakeCoreDNSEKSObj.Status.AtProvider.Endpoint = fakeCorednsEKSHostname
	fakeCoreDNSCMInfo.Region = "us-west-2"
	fakeCoreDNSCMInfo.Cluster = "eks1"
	fakeCoreDNSCMInfo.ProviderConfigRef = "123456789012"
	fakeCoreDNSCMInfo.eksConnection = &eksv1alpha1.EKSConnection{}
	fakeCoreDNSCMInfo.eksConnection.Spec.Target = &corev1.ObjectReference{}
	fakeCoreDNSCMInfo.eksConnection.Spec.Target.Name = eksName
	fakeCoreDNSCMInfo.eksConnection.Spec.Target.Namespace = eksNamespace
	fakeCoreDNSCMInfo.namespace = ProcedureNamespace
	fakeCoreDNSSvc = getFrpsInternalService(fakeCoreDNSCMInfo)
	fakeCoreDNSSvc.Spec.ClusterIP = fakeFrpsClusterIP
}

func TestAddRecordToCorefile(t *testing.T) {
	cases := map[string]struct {
		oldCorefile    string
		records        []hostRecord
		expectErr      bool
		expectCoreFile string
	}{
		"without-hosts-block": {
			oldCorefile:    fmt.Sprintf(corefileTemplate, ""),
			records:        []hostRecord{{ip: fakeCorednsIP, host: fakeCorednsHost}},
			expectErr:      false,
			expectCoreFile: expectCorednsFile,
		},
		"empty-hosts-block": {
			oldCorefile:    fmt.Sprintf(corefileTemplate, emptyHostBlock),
			records:        []hostRecord{{ip: fakeCorednsIP, host: fakeCorednsHost}},
			expectErr:      false,
			expectCoreFile: expectCorednsFile,
		},
		"with-existing-record": {
			oldCorefile: fmt.Sprintf(corefileTemplate, hostBlockWithEntry),
			records:     []hostRecord{{ip: "20.20.20.20", host: "google.com"}},
			expectErr:   false,
			expectCoreFile: fmt.Sprintf(corefileTemplate, `
	hosts {
		20.20.20.20 google.com
		10.10.10.10 tidbcloud.com
		fallthrough
	}`),
		},
		"overwrite-record": {
			oldCorefile: fmt.Sprintf(corefileTemplate, hostBlockWithEntry),
			records:     []hostRecord{{ip: "20.20.20.20", host: fakeCorednsHost}},
			expectErr:   false,
			expectCoreFile: fmt.Sprintf(corefileTemplate, `
	hosts {
		20.20.20.20 tidbcloud.com
		fallthrough
	}`),
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			cfStr, err := addRecordToCorefile(tc.oldCorefile, tc.records)
			if !tc.expectErr {
				g.Expect(err).To(Succeed())
				g.Expect(cfStr).To(Equal(tc.expectCoreFile))
			} else {
				g.Expect(err).NotTo(Succeed())
			}
		})
	}
}

func TestRemoveRecordFromCorefile(t *testing.T) {
	cases := map[string]struct {
		oldCorefile    string
		hosts          []string
		expectErr      bool
		expectCoreFile string
	}{
		"with-existing-record": {
			oldCorefile: fmt.Sprintf(corefileTemplate, hostBlockWithEntry),
			hosts:       []string{"tidbcloud.com"},
			expectErr:   false,
			expectCoreFile: fmt.Sprintf(corefileTemplate, `
	hosts {
		fallthrough
	}`),
		},
		"with-non-existing-record": {
			oldCorefile: fmt.Sprintf(corefileTemplate, hostBlockWithEntry),
			hosts:       []string{"google.com"},
			expectErr:   false,
			expectCoreFile: fmt.Sprintf(corefileTemplate, `
	hosts {
		10.10.10.10 tidbcloud.com
		fallthrough
	}`),
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			cfStr, err := removeHostsFromCorefile(tc.oldCorefile, tc.hosts)
			if !tc.expectErr {
				g.Expect(err).To(Succeed())
				g.Expect(cfStr).To(Equal(tc.expectCoreFile))
			} else {
				g.Expect(err).NotTo(Succeed())
			}
		})
	}
}

func TestCoreDNSCMDeploy(t *testing.T) {
	cases := map[string]struct {
		info      ProcedureInfo
		objs      []client.Object
		expectErr bool
	}{
		"coredns-cm-not-exist": {
			info:      fakeCoreDNSCMInfo,
			expectErr: true,
		},
		"coredns-cm-exist": {
			info: fakeCoreDNSCMInfo,
			objs: []client.Object{
				&corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      corednsName,
						Namespace: metav1.NamespaceSystem,
					},
					Data: map[string]string{
						corefileKey: fmt.Sprintf(corefileTemplate, ""),
					},
				},
				fakeCoreDNSEKSObj,
				fakeCoreDNSSvc,
			},
			expectErr: false,
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			s := runtime.NewScheme()
			g.Expect(scheme.AddToScheme(s)).To(Succeed())
			g.Expect(cpv1beta1.SchemeBuilder.AddToScheme(s)).To(Succeed())
			cli := fake.NewClientBuilder().WithScheme(s).WithObjects(tc.objs...).Build()
			p := NewCoreDNSCM()
			err := p.Deploy(context.TODO(), cli, cli, tc.info)
			if !tc.expectErr {
				g.Expect(err).To(Succeed())
			} else {
				g.Expect(err).NotTo(Succeed())
			}
		})
	}
}

func TestCoreDNSCMStatus(t *testing.T) {
	cases := map[string]struct {
		info        ProcedureInfo
		objs        []client.Object
		expectReady bool
		expectErr   bool
	}{
		"not-exist": {
			info:        fakeCoreDNSCMInfo,
			expectErr:   true,
			expectReady: false,
		},
		"cm-content-correct": {
			info: fakeCoreDNSCMInfo,
			objs: []client.Object{
				&corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      corednsName,
						Namespace: metav1.NamespaceSystem,
					},
					Data: map[string]string{
						corefileKey: fmt.Sprintf(corefileTemplate, `hosts {
		10.10.10.10 fake.eks1.com
		fallthrough
	}`),
					},
				},
				fakeCoreDNSEKSObj,
				fakeCoreDNSSvc,
			},
			expectErr:   false,
			expectReady: true,
		},
		"cm-content-not-exist": {
			info: fakeCoreDNSCMInfo,
			objs: []client.Object{
				&corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      corednsName,
						Namespace: metav1.NamespaceSystem,
					},
					Data: map[string]string{"foo": "bar"},
				},
				fakeCoreDNSEKSObj,
				fakeCoreDNSSvc,
			},
			expectErr:   true,
			expectReady: false,
		},
		"cm-content-wrong": {
			info: fakeCoreDNSCMInfo,
			objs: []client.Object{
				&corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      corednsName,
						Namespace: metav1.NamespaceSystem,
					},
					Data: map[string]string{corefileKey: "bar"},
				},
				fakeCoreDNSEKSObj,
				fakeCoreDNSSvc,
			},
			expectErr:   false,
			expectReady: false,
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			s := runtime.NewScheme()
			g.Expect(scheme.AddToScheme(s)).To(Succeed())
			g.Expect(cpv1beta1.SchemeBuilder.AddToScheme(s)).To(Succeed())
			cli := fake.NewClientBuilder().WithScheme(s).WithObjects(tc.objs...).Build()
			p := NewCoreDNSCM()
			ready, err := p.Status(context.TODO(), cli, cli, tc.info)
			if !tc.expectErr {
				g.Expect(err).To(Succeed())
			} else {
				g.Expect(err).NotTo(Succeed())
				return
			}
			g.Expect(ready).To(Equal(tc.expectReady))
		})
	}
}
