package v1

import (
	"testing"

	"github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/utils/pointer"
)

func TestClusterWebhookValidateSqlEndpoints(t *testing.T) {
	reader := &fakeReader{}
	webhookClient.direct = reader
	webhookClient.cached = reader

	cases := map[string]struct {
		setup    func(c *SqlEndpoints)
		expectFn func(g *gomega.GomegaWithT, err error)
	}{
		"Reserved public endpoint port": {
			setup: func(ep *SqlEndpoints) {
				ep.Public = Service{Enabled: pointer.Bool(true), Port: pointer.Int32(10080)}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).To(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("port is reserved"))
			},
		},
		"Reserved private endpoint port": {
			setup: func(ep *SqlEndpoints) {
				ep.Private = &Service{Enabled: pointer.Bool(true), Port: pointer.Int32(10080)}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).To(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("port is reserved"))
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := gomega.NewGomegaWithT(t)
			c := &Cluster{}
			ep := &SqlEndpoints{}

			tc.setup(ep)
			err := c.validateSqlEndpoints(field.NewPath("spec.endpoints"), *ep)
			tc.expectFn(g, err.ToAggregate())
		})
	}

}

func TestClusterWebhookValidateUpdate(t *testing.T) {
	expectSucceed := func(g *gomega.GomegaWithT, err error) {
		g.Expect(err).Should(gomega.Succeed())
	}
	reader := &fakeReader{}
	webhookClient.direct = reader
	webhookClient.cached = reader

	truePtr := pointer.Bool(true)
	flasePtr := pointer.Bool(false)
	port4000Ptr := pointer.Int32(4000)
	port5000Ptr := pointer.Int32(5000)

	cases := map[string]struct {
		setCluster func(old *Cluster, cur *Cluster)
		expectFn   func(g *gomega.GomegaWithT, err error)
	}{
		"Change network": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.NetworkName = "old-network"
				cur.Spec.NetworkName = "new-network"
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("spec.network"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("field is immutable"))
			},
		},
		"Disable public endpoint": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Public = Service{Enabled: truePtr, Port: port4000Ptr}
				cur.Spec.SqlEndpoints.Public = Service{Enabled: flasePtr, Port: port5000Ptr}
			},
			expectFn: expectSucceed,
		},
		"Disable private endpoint": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Private = &Service{Enabled: truePtr, Port: port4000Ptr}
				cur.Spec.SqlEndpoints.Private = &Service{Enabled: flasePtr, Port: port5000Ptr}
			},
			expectFn: expectSucceed,
		},
		"Enable public endpoint": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Public = Service{Enabled: flasePtr, Port: port4000Ptr}
				cur.Spec.SqlEndpoints.Public = Service{Enabled: truePtr, Port: port5000Ptr}
			},
			expectFn: expectSucceed,
		},
		"Enable private endpoint": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Private = &Service{Enabled: flasePtr, Port: port4000Ptr}
				cur.Spec.SqlEndpoints.Private = &Service{Enabled: truePtr, Port: port5000Ptr}
			},
			expectFn: expectSucceed,
		},
		"Not change public endpoint port": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Public = Service{Enabled: truePtr, Port: port5000Ptr}
				cur.Spec.SqlEndpoints.Public = Service{Enabled: truePtr, Port: port5000Ptr}
			},
			expectFn: expectSucceed,
		},
		"Not change private endpoint port": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Private = &Service{Enabled: truePtr, Port: port5000Ptr}
				cur.Spec.SqlEndpoints.Private = &Service{Enabled: truePtr, Port: port5000Ptr}
			},
			expectFn: expectSucceed,
		},
		"Change public endpoint port": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Public = Service{Enabled: truePtr, Port: port4000Ptr}
				cur.Spec.SqlEndpoints.Public = Service{Enabled: truePtr, Port: port5000Ptr}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("spec.sqlEndpoints.public.port"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("field is immutable"))
			},
		},
		"Change private endpoint port": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.Private = &Service{Enabled: truePtr, Port: port4000Ptr}
				cur.Spec.SqlEndpoints.Private = &Service{Enabled: truePtr, Port: port5000Ptr}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("spec.sqlEndpoints.private.port"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("field is immutable"))
			},
		},
		"Scale local storage of TiKV": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.Cluster.TiKV = TiKV{
					UseLocalStorage: truePtr,
					StorageSize:     resource.MustParse("1Gi"),
				}
				cur.Spec.Cluster.TiKV = TiKV{
					UseLocalStorage: truePtr,
					StorageSize:     resource.MustParse("2Gi"),
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("cluster.tikv.storageSize"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("size of local storage is immutable"))
			},
		},
		"Scale local storage of TiFlash": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.Cluster.TiFlash = TiFlash{
					UseLocalStorage: truePtr,
					StorageSize:     resource.MustParse("1Gi"),
				}
				cur.Spec.Cluster.TiFlash = TiFlash{
					UseLocalStorage: truePtr,
					StorageSize:     resource.MustParse("2Gi"),
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("cluster.tiflash.storageSize"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("size of local storage is immutable"))
			},
		},
		"Change DedicatedRaftEngineStorage of TiKV to nil": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.Cluster.TiKV = TiKV{
					DedicatedRaftEngineStorage: &TiKVDedicatedRaftEngineStorage{
						ComponentStorageSpec: ComponentStorageSpec{
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: pointer.String("gp2"),
						},
					},
				}
				cur.Spec.Cluster.TiKV = TiKV{
					DedicatedRaftEngineStorage: nil,
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("cluster.tikv.dedicatedRaftEngineStorage"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("field is immutable"))
			},
		},
		"Change DedicatedRaftEngineStorage of TiKV from nil": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.Cluster.TiKV = TiKV{
					DedicatedRaftEngineStorage: nil,
				}
				cur.Spec.Cluster.TiKV = TiKV{
					DedicatedRaftEngineStorage: &TiKVDedicatedRaftEngineStorage{
						ComponentStorageSpec: ComponentStorageSpec{
							StorageSize:      resource.MustParse("1Gi"),
							StorageClassName: pointer.String("gp2"),
						},
					},
				}

			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("cluster.tikv.dedicatedRaftEngineStorage"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("field is immutable"))
			},
		},
		"Change DNS identity when current DNS is enabled should fail": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.DNS = DNS{
					Identity: "old-identity",
					Disabled: false,
				}
				cur.Spec.SqlEndpoints.DNS = DNS{
					Identity: "new-identity",
					Disabled: false, // DNS is enabled
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("spec.sqlEndpoints.dns.identity"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("DNS identity can only be modified when current DNS is disabled"))
			},
		},
		"Change DNS identity while disabling DNS in same update should fail": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.DNS = DNS{
					Identity: "old-identity",
					Disabled: false,
				}
				cur.Spec.SqlEndpoints.DNS = DNS{
					Identity: "new-identity",
					Disabled: true, // DNS is disabled
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("spec.sqlEndpoints.dns.identity"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("DNS identity can only be modified when current DNS is disabled"))
			},
		},
		"Change DNS identity when both old and current DNS are disabled should succeed": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.DNS = DNS{
					Identity: "old-identity",
					Disabled: true,
				}
				cur.Spec.SqlEndpoints.DNS = DNS{
					Identity: "new-identity",
					Disabled: true, // DNS remains disabled
				}
			},
			expectFn: expectSucceed,
		},
		"Change DNS identity from disabled to enabled should succeed": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.DNS = DNS{
					Identity: "old-identity",
					Disabled: true,
				}
				cur.Spec.SqlEndpoints.DNS = DNS{
					Identity: "new-identity",
					Disabled: false, // DNS is being enabled
				}
			},
			expectFn: expectSucceed,
		},
		"Keep same DNS identity when DNS state changes should succeed": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.DNS = DNS{
					Identity: "same-identity",
					Disabled: false,
				}
				cur.Spec.SqlEndpoints.DNS = DNS{
					Identity: "same-identity", // Identity unchanged
					Disabled: true,
				}
			},
			expectFn: expectSucceed,
		},
		"Change DNS identity when old identity is empty should succeed": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.SqlEndpoints.DNS = DNS{
					Identity: "", // Empty identity
					Disabled: false,
				}
				cur.Spec.SqlEndpoints.DNS = DNS{
					Identity: "new-identity",
					Disabled: false,
				}
			},
			expectFn: expectSucceed,
		},
		"Change DNS identity in extra group when current DNS is enabled should fail": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.ExtraGroups = []ExtraGroupSpec{
					{
						Name: "test-group",
						SqlEndpoints: SqlEndpoints{
							DNS: DNS{
								Identity: "old-group-identity",
								Disabled: false,
							},
						},
					},
				}
				cur.Spec.ExtraGroups = []ExtraGroupSpec{
					{
						Name: "test-group",
						SqlEndpoints: SqlEndpoints{
							DNS: DNS{
								Identity: "new-group-identity",
								Disabled: false, // DNS is enabled
							},
						},
					},
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("spec.extraGroups[test-group].sqlEndpoints.dns.identity"))
				g.Expect(err.Error()).Should(gomega.ContainSubstring("DNS identity can only be modified when current DNS is disabled"))
			},
		},
		"Change DNS identity in extra group when already disabled should succeed": {
			setCluster: func(old, cur *Cluster) {
				old.Spec.ExtraGroups = []ExtraGroupSpec{
					{
						Name: "test-group",
						SqlEndpoints: SqlEndpoints{
							DNS: DNS{
								Identity: "old-group-identity",
								Disabled: true,
							},
						},
					},
				}
				cur.Spec.ExtraGroups = []ExtraGroupSpec{
					{
						Name: "test-group",
						SqlEndpoints: SqlEndpoints{
							DNS: DNS{
								Identity: "new-group-identity",
								Disabled: true, // DNS is disabled
							},
						},
					},
				}
			},
			expectFn: expectSucceed,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := gomega.NewGomegaWithT(t)
			old := &Cluster{}
			old.Name = "webhook-ut"
			old.Namespace = "default"
			old.Spec.NetworkName = "aws-us-west-2"

			cur := old.DeepCopy()
			if tc.setCluster != nil {
				tc.setCluster(old, cur)
			}
			_, err := cur.ValidateUpdate(old)
			tc.expectFn(g, err)
		})
	}
}

func TestClusterValidateLBTarget(t *testing.T) {
	cases := map[string]struct {
		setCluster func(cluster *Cluster)
		expectFn   func(g *gomega.GomegaWithT, err error)
	}{
		"UseTiProxyAsLBTarget is true but TiProxy replicas is 0 in the default group should raise error": {
			setCluster: func(cluster *Cluster) {
				cluster.Spec.SqlEndpoints.UseTiProxyAsLBTarget = true
				cluster.Spec.Cluster.TiProxy.Replicas = 0
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("tiProxy replicas must be greater than 0 when useTiProxyAsLBTarget is true"))
			},
		},
		"UseTiProxyAsLBTarget is true and TiProxy replicas is 1 that is OK": {
			setCluster: func(cluster *Cluster) {
				cluster.Spec.SqlEndpoints.UseTiProxyAsLBTarget = true
				cluster.Spec.Cluster.TiProxy.Replicas = 1
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).ShouldNot(gomega.HaveOccurred())
			},
		},
		"Both tiproxy and tidb is enabled but useTiProxyAsLBTarget is false is OK": {
			setCluster: func(cluster *Cluster) {
				cluster.Spec.SqlEndpoints.UseTiProxyAsLBTarget = false
				cluster.Spec.Cluster.TiProxy.Replicas = 1
				cluster.Spec.Cluster.TiDB.Replicas = 1
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).ShouldNot(gomega.HaveOccurred())
			},
		},
		"ExtraGroup UseTiProxyAsLBTarget is true but TiProxy replicas is 0 should raise error": {
			setCluster: func(cluster *Cluster) {
				cluster.Spec.ExtraGroups = []ExtraGroupSpec{
					{
						Name: "test-group",
						SqlEndpoints: SqlEndpoints{
							UseTiProxyAsLBTarget: true,
						},
						TiProxy: TiProxy{
							Replicas: 0,
						},
						TiDB: TiDB{
							Replicas: 1,
						},
					},
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).Should(gomega.HaveOccurred())
				g.Expect(err.Error()).Should(gomega.ContainSubstring("tiProxy replicas must be greater than 0 when useTiProxyAsLBTarget is true"))
			},
		},
		"ExtraGroup UseTiProxyAsLBTarget is true and TiProxy replicas is 1 that is OK": {
			setCluster: func(cluster *Cluster) {
				cluster.Spec.ExtraGroups = []ExtraGroupSpec{
					{
						Name: "test-group",
						SqlEndpoints: SqlEndpoints{
							UseTiProxyAsLBTarget: true,
						},
						TiProxy: TiProxy{
							Replicas: 1,
						},
					},
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).ShouldNot(gomega.HaveOccurred())
			},
		},
		"ExtraGroup is deleted that is OK": {
			setCluster: func(cluster *Cluster) {
				cluster.Spec.ExtraGroups = []ExtraGroupSpec{
					{
						Name: "test-group",
						SqlEndpoints: SqlEndpoints{
							UseTiProxyAsLBTarget: true,
						},
						TiProxy: TiProxy{
							Replicas: 0, // represent it's deleted.
						},
					},
				}
			},
			expectFn: func(g *gomega.GomegaWithT, err error) {
				g.Expect(err).ShouldNot(gomega.HaveOccurred())
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := gomega.NewGomegaWithT(t)
			cluster := &Cluster{}
			cluster.Name = "webhook-ut"
			cluster.Namespace = "default"
			cluster.Spec.NetworkName = "aws-us-west-2"

			if tc.setCluster != nil {
				tc.setCluster(cluster)
			}
			err := cluster.validateLBTarget()
			tc.expectFn(g, err.ToAggregate())
		})
	}
}
