package integration

import (
	"context"
	"math/rand"
	"testing"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/tidbcloud/shared-provider/pkg/controllers/networkpolicy"
	sharedtypes "github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	corev1 "k8s.io/api/core/v1"
	netv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var testEnv *envtest.Environment
var k8sClient client.Client

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)
	rand.Seed(time.Now().UnixNano())
	RunSpecsWithDefaultAndCustomReporters(t,
		"Controllers Integration Suite",
		[]Reporter{printer.NewlineReporter{}})
}

var _ = BeforeSuite(func() {
	By("bootstrapping test environment")
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	testEnv = &envtest.Environment{}

	cfg, err := testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	manager, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
	})
	Expect(err).Should(Succeed())

	err = (&networkpolicy.Reconciler{
		Client:          manager.GetClient(),
		KubeClusterName: "test",
	}).SetupWithManager(manager)
	Expect(err).Should(Succeed())

	ctx := ctrl.SetupSignalHandler()
	go func() {
		defer GinkgoRecover()
		err = manager.Start(ctx)
		Expect(err).ToNot(HaveOccurred())
	}()
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	err := testEnv.Stop()
	Expect(err).NotTo(HaveOccurred())
})

var _ = Describe("NetworkPolocy controller", func() {
	const (
		timeout  = time.Second * 20
		interval = time.Millisecond * 200
	)

	var ns *corev1.Namespace

	BeforeEach(func() {
		ns = &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-policies",
				Labels: map[string]string{
					labels.LabelKeyOwnedBy:           "cloud.pingcap.com.v1.network",
					labels.LabelKeyKubernetesCluster: "test",
				},
			},
		}
		err := k8sClient.Create(context.Background(), ns)
		Expect(err).Should(Succeed())
	})

	AfterEach(func() {
		err := k8sClient.Delete(context.Background(), ns)
		Expect(err).Should(Succeed())
	})

	It("Should create/update policies when namespace created", func() {
		ctx := context.Background()
		By("Should create policies in ns")
		Eventually(func() error {
			for _, name := range networkpolicy.AllPolicyNames {
				policy := new(netv1.NetworkPolicy)
				key := types.NamespacedName{Namespace: ns.Name, Name: name}
				err := k8sClient.Get(ctx, key, policy)
				if err != nil {
					return err
				}
			}
			return nil
		}, timeout, interval).Should(Succeed())

		By("Should update policy if policy not as expect")
		key := types.NamespacedName{Namespace: ns.Name, Name: networkpolicy.AllowDNSAccessName}
		policy := new(netv1.NetworkPolicy)
		err := k8sClient.Get(ctx, key, policy)
		Expect(err).Should(Succeed())
		expect := networkpolicy.NewAllowDNSAccess(ns.Name)
		Expect(policy.Spec).Should(Equal(expect.Spec))

		policy.Spec.Egress = nil
		err = k8sClient.Update(ctx, policy)
		Expect(err).Should(Succeed())
		Eventually(func(g Gomega) {
			err = k8sClient.Get(ctx, key, policy)
			g.Expect(err).Should(Succeed())
			g.Expect(policy.Spec).Should(Equal(expect.Spec))
		}, timeout, interval).Should(Succeed())

		By("Should update policy depend on kubernete if endpoint changed")
		// first update endpoint
		endpointKey := types.NamespacedName{
			Name:      sharedtypes.KubernetesEndpointName,
			Namespace: corev1.NamespaceDefault,
		}
		endpoint := new(corev1.Endpoints)
		err = k8sClient.Get(ctx, endpointKey, endpoint)
		Expect(err).Should(Succeed())
		endpoint.Subsets = append(endpoint.Subsets, corev1.EndpointSubset{
			Addresses: []corev1.EndpointAddress{
				{
					IP: "1.1.1.1",
				},
			},
		})
		err = k8sClient.Update(ctx, endpoint)
		Expect(err).Should(Succeed())

		// check policy updated with new endpoint
		cidrs := networkpolicy.KubernetesEndpoint2CIDR(endpoint)
		Expect(cidrs).Should(ContainElement("1.1.1.1/32"))
		expect = networkpolicy.NewAllowToAPIServer(ns.Name, cidrs)
		Eventually(func(g Gomega) {
			key = types.NamespacedName{Namespace: ns.Name, Name: networkpolicy.AllowToAPIServer}
			policy = new(netv1.NetworkPolicy)
			err = k8sClient.Get(ctx, key, policy)
			g.Expect(err).Should(Succeed())
			g.Expect(policy.Spec).Should(Equal(expect.Spec))
		}, timeout, interval).Should(Succeed())
	})
})
