// Copyright (c) 2022 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package clientv3_test

import (
	"context"
	"fmt"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	apiv3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/projectcalico/calico/libcalico-go/lib/apiconfig"
	"github.com/projectcalico/calico/libcalico-go/lib/backend"
	"github.com/projectcalico/calico/libcalico-go/lib/clientv3"
	"github.com/projectcalico/calico/libcalico-go/lib/options"
	"github.com/projectcalico/calico/libcalico-go/lib/testutils"
	"github.com/projectcalico/calico/libcalico-go/lib/watch"
)

var _ = testutils.E2eDatastoreDescribe("BGPFilter tests", testutils.DatastoreAll, func(config apiconfig.CalicoAPIConfig) {
	type testConf struct {
		IPv4 bool
		IPv6 bool
	}
	for _, testConfig := range []testConf{
		{true, false},
		{false, true},
		{true, true},
	} {
		ipv4 := testConfig.IPv4
		ipv6 := testConfig.IPv6

		Describe(fmt.Sprintf("ipv4: %v, ipv6: %v", ipv4, ipv6), func() {
			ctx := context.Background()
			name1 := "filter-1"
			name2 := "filter-2"
			acceptRuleV4 := apiv3.BGPFilterRuleV4{
				CIDR:          "10.10.10.0/24",
				MatchOperator: apiv3.MatchOperatorIn,
				Action:        apiv3.Accept,
			}
			rejectRuleV4 := apiv3.BGPFilterRuleV4{
				CIDR:          "11.11.11.0/24",
				MatchOperator: apiv3.MatchOperatorNotEqual,
				Action:        apiv3.Reject,
			}
			acceptRuleV6 := apiv3.BGPFilterRuleV6{
				CIDR:          "dead:beef:1::/64",
				MatchOperator: apiv3.MatchOperatorEqual,
				Action:        apiv3.Accept,
			}
			rejectRuleV6 := apiv3.BGPFilterRuleV6{
				CIDR:          "dead:beef:2::/64",
				MatchOperator: apiv3.MatchOperatorNotIn,
				Action:        apiv3.Reject,
			}
			spec1 := apiv3.BGPFilterSpec{}
			if ipv4 {
				spec1.ExportV4 = []apiv3.BGPFilterRuleV4{acceptRuleV4}
				spec1.ImportV4 = []apiv3.BGPFilterRuleV4{rejectRuleV4}
			}
			if ipv6 {
				spec1.ExportV6 = []apiv3.BGPFilterRuleV6{acceptRuleV6}
				spec1.ImportV6 = []apiv3.BGPFilterRuleV6{rejectRuleV6}
			}
			spec2 := apiv3.BGPFilterSpec{}
			if ipv4 {
				spec2.ExportV4 = []apiv3.BGPFilterRuleV4{rejectRuleV4, acceptRuleV4}
			}
			if ipv6 {
				spec2.ExportV6 = []apiv3.BGPFilterRuleV6{rejectRuleV6, acceptRuleV6}
			}

			DescribeTable("BGPFilter e2e CRUD tests",
				func(name1, name2 string) {
					c, err := clientv3.New(config)
					Expect(err).NotTo(HaveOccurred())

					be, err := backend.NewClient(config)
					Expect(err).NotTo(HaveOccurred())
					be.Clean()

					By("Updating the BGPFilter before it is created")
					_, outError := c.BGPFilter().Update(ctx, &apiv3.BGPFilter{
						ObjectMeta: metav1.ObjectMeta{Name: name1, ResourceVersion: "1234", CreationTimestamp: metav1.Now(), UID: uid},
						Spec:       spec1,
					}, options.SetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(ContainSubstring("resource does not exist: BGPFilter(" + name1 + ") with error:"))

					By("Attempting to creating a new BGPFilter with name1/spec1 and a non-empty ResourceVersion")
					_, outError = c.BGPFilter().Create(ctx, &apiv3.BGPFilter{
						ObjectMeta: metav1.ObjectMeta{Name: name1, ResourceVersion: "12345"},
						Spec:       spec1,
					}, options.SetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(Equal("error with field Metadata.ResourceVersion = '12345' (field must not be set for a Create request)"))

					By("Creating a new BGPFilter with name1/spec1")
					res1, outError := c.BGPFilter().Create(ctx, &apiv3.BGPFilter{
						ObjectMeta: metav1.ObjectMeta{Name: name1},
						Spec:       spec1,
					}, options.SetOptions{})
					Expect(outError).NotTo(HaveOccurred())

					// Track the version of the original data for name1.
					rv1_1 := res1.ResourceVersion

					By("Attempting to create the same BGPFilter with name1 but with spec2")
					_, outError = c.BGPFilter().Create(ctx, &apiv3.BGPFilter{
						ObjectMeta: metav1.ObjectMeta{Name: name1},
						Spec:       spec2,
					}, options.SetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(ContainSubstring("resource already exists: BGPFilter(" + name1 + ") with error:"))

					By("Getting BGPFilter (name1) and comparing the output against spec1")
					res, outError := c.BGPFilter().Get(ctx, name1, options.GetOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(res).To(MatchResource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec1))
					Expect(res.ResourceVersion).To(Equal(res1.ResourceVersion))

					By("Getting BGPFilter (name2) before it is created")
					_, outError = c.BGPFilter().Get(ctx, name2, options.GetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(ContainSubstring("resource does not exist: BGPFilter(" + name2 + ") with error:"))

					By("Listing all the BGPFilter, expecting a single result with name1/spec1")
					outList, outError := c.BGPFilter().List(ctx, options.ListOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(outList.Items).To(ConsistOf(
						testutils.Resource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec1),
					))

					By("Creating a new BGPFilter with name2")
					res2, outError := c.BGPFilter().Create(ctx, &apiv3.BGPFilter{
						ObjectMeta: metav1.ObjectMeta{Name: name2},
						Spec:       spec2,
					}, options.SetOptions{})
					Expect(outError).NotTo(HaveOccurred())

					By("Getting BGPFilter (name2) and comparing the output against spec2")
					res, outError = c.BGPFilter().Get(ctx, name2, options.GetOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(res2).To(MatchResource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name2, spec2))
					Expect(res.ResourceVersion).To(Equal(res2.ResourceVersion))

					By("Listing all the BGPFilter, expecting two results with name1/spec1 and name2/spec2")
					outList, outError = c.BGPFilter().List(ctx, options.ListOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(outList.Items).To(ConsistOf(
						testutils.Resource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec1),
						testutils.Resource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name2, spec2),
					))

					By("Updating BGPFilter name1's Export with spec2's values")
					if ipv4 {
						res1.Spec.ExportV4 = []apiv3.BGPFilterRuleV4{rejectRuleV4, acceptRuleV4}
						res1.Spec.ImportV4 = nil
					}
					if ipv6 {
						res1.Spec.ExportV6 = []apiv3.BGPFilterRuleV6{rejectRuleV6, acceptRuleV6}
						res1.Spec.ImportV6 = nil
					}
					res1, outError = c.BGPFilter().Update(ctx, res1, options.SetOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(res1).To(MatchResource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec2))

					By("Attempting to update the BGPFilter without a Creation Timestamp")
					res, outError = c.BGPFilter().Update(ctx, &apiv3.BGPFilter{
						ObjectMeta: metav1.ObjectMeta{Name: name1, ResourceVersion: "1234", UID: uid},
						Spec:       spec1,
					}, options.SetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(res).To(BeNil())
					Expect(outError.Error()).To(Equal("error with field Metadata.CreationTimestamp = '0001-01-01 00:00:00 +0000 UTC' (field must be set for an Update request)"))

					By("Attempting to update the BGPFilter without a UID")
					res, outError = c.BGPFilter().Update(ctx, &apiv3.BGPFilter{
						ObjectMeta: metav1.ObjectMeta{Name: name1, ResourceVersion: "1234", CreationTimestamp: metav1.Now()},
						Spec:       spec1,
					}, options.SetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(res).To(BeNil())
					Expect(outError.Error()).To(Equal("error with field Metadata.UID = '' (field must be set for an Update request)"))

					// Track the version of the updated name1 data.
					rv1_2 := res1.ResourceVersion

					By("Updating BGPFilter name1 without specifying a resource version")
					res1.Spec = spec1
					res1.ObjectMeta.ResourceVersion = ""
					_, outError = c.BGPFilter().Update(ctx, res1, options.SetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(Equal("error with field Metadata.ResourceVersion = '' (field must be set for an Update request)"))

					By("Updating BGPFilter name1 using the previous resource version")
					res1.Spec = spec1
					res1.ResourceVersion = rv1_1
					_, outError = c.BGPFilter().Update(ctx, res1, options.SetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(Equal("update conflict: BGPFilter(" + name1 + ")"))

					if config.Spec.DatastoreType != apiconfig.Kubernetes {
						By("Getting BGPFilter (name1) with the original resource version and comparing the output against spec1")
						res, outError = c.BGPFilter().Get(ctx, name1, options.GetOptions{ResourceVersion: rv1_1})
						Expect(outError).NotTo(HaveOccurred())
						Expect(res).To(MatchResource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec1))
						Expect(res.ResourceVersion).To(Equal(rv1_1))
					}

					By("Getting BGPFilter (name1) with the updated resource version and comparing the output against spec2")
					res, outError = c.BGPFilter().Get(ctx, name1, options.GetOptions{ResourceVersion: rv1_2})
					Expect(outError).NotTo(HaveOccurred())
					Expect(res).To(MatchResource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec2))
					Expect(res.ResourceVersion).To(Equal(rv1_2))

					if config.Spec.DatastoreType != apiconfig.Kubernetes {
						By("Listing BGPFilter with the original resource version and checking for a single result with name1/spec1")
						outList, outError = c.BGPFilter().List(ctx, options.ListOptions{ResourceVersion: rv1_1})
						Expect(outError).NotTo(HaveOccurred())
						Expect(outList.Items).To(ConsistOf(
							testutils.Resource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec1),
						))
					}

					By("Listing BGPFilter with the latest resource version and checking for two results with name1/spec1 and name2/spec2")
					outList, outError = c.BGPFilter().List(ctx, options.ListOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(outList.Items).To(ConsistOf(
						// Use spec2 for name1 as it was changed previously
						testutils.Resource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec2),
						testutils.Resource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name2, spec2),
					))

					if config.Spec.DatastoreType != apiconfig.Kubernetes {
						By("Deleting BGPFilter (name1) with the old resource version")
						_, outError = c.BGPFilter().Delete(ctx, name1, options.DeleteOptions{ResourceVersion: rv1_1})
						Expect(outError).To(HaveOccurred())
						Expect(outError.Error()).To(Equal("update conflict: BGPFilter(" + name1 + ")"))
					}

					By("Deleting BGPFilter (name1) with the new resource version")
					dres, outError := c.BGPFilter().Delete(ctx, name1, options.DeleteOptions{ResourceVersion: rv1_2})
					Expect(outError).NotTo(HaveOccurred())
					Expect(dres).To(MatchResource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name1, spec2))

					if config.Spec.DatastoreType != apiconfig.Kubernetes {
						By("Updating BGPFilter name2 with a 2s TTL and waiting for the entry to be deleted")
						_, outError = c.BGPFilter().Update(ctx, res2, options.SetOptions{TTL: 2 * time.Second})
						Expect(outError).NotTo(HaveOccurred())
						Eventually(func() string {
							_, err := c.BGPFilter().Get(ctx, name2, options.GetOptions{})
							if err != nil {
								return err.Error()
							}
							return ""
						}, 5*time.Second, 200*time.Millisecond).Should(ContainSubstring("resource does not exist: BGPFilter(" + name2 + ") with error:"))

						By("Creating BGPFilter name2 with a 2s TTL and waiting for the entry to be deleted")
						_, outError = c.BGPFilter().Create(ctx, &apiv3.BGPFilter{
							ObjectMeta: metav1.ObjectMeta{Name: name2},
							Spec:       spec2,
						}, options.SetOptions{TTL: 2 * time.Second})
						Expect(outError).NotTo(HaveOccurred())
						Eventually(func() string {
							_, err := c.BGPFilter().Get(ctx, name2, options.GetOptions{})
							if err != nil {
								return err.Error()
							}
							return ""
						}, 5*time.Second, 200*time.Millisecond).Should(ContainSubstring("resource does not exist: BGPFilter(" + name2 + ") with error:"))
					}

					if config.Spec.DatastoreType == apiconfig.Kubernetes {
						By("Attempting to deleting BGPFilter (name2)")
						dres, outError = c.BGPFilter().Delete(ctx, name2, options.DeleteOptions{})
						Expect(outError).NotTo(HaveOccurred())
						Expect(dres).To(MatchResource(apiv3.KindBGPFilter, testutils.ExpectNoNamespace, name2, spec2))
					}

					By("Attempting to delete BGPFilter (name2) again")
					_, outError = c.BGPFilter().Delete(ctx, name2, options.DeleteOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(ContainSubstring("resource does not exist: BGPFilter(" + name2 + ") with error:"))

					By("Listing all BGPFilter and expecting no items")
					outList, outError = c.BGPFilter().List(ctx, options.ListOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(outList.Items).To(HaveLen(0))

					By("Getting BGPFilter (name2) and expecting an error")
					_, outError = c.BGPFilter().Get(ctx, name2, options.GetOptions{})
					Expect(outError).To(HaveOccurred())
					Expect(outError.Error()).To(ContainSubstring("resource does not exist: BGPFilter(" + name2 + ") with error:"))
				},

				Entry("BGPFilter 1,2", name1, name2),
			)

			Describe("BGPFilter watch functionality", func() {
				It("should handle watch events for different resource versions and event types", func() {
					c, err := clientv3.New(config)
					Expect(err).NotTo(HaveOccurred())

					be, err := backend.NewClient(config)
					Expect(err).NotTo(HaveOccurred())
					be.Clean()

					By("Listing BGPFilter with the latest resource version and checking for two results with name1 and name2")
					outList, outError := c.BGPFilter().List(ctx, options.ListOptions{})
					Expect(outError).NotTo(HaveOccurred())
					Expect(outList.Items).To(HaveLen(0))
					rev0 := outList.ResourceVersion

					By("Configuring a BGPFilter name1 and storing the response")
					outRes1, err := c.BGPFilter().Create(
						ctx,
						&apiv3.BGPFilter{
							ObjectMeta: metav1.ObjectMeta{Name: name1},
							Spec:       spec1,
						},
						options.SetOptions{},
					)
					Expect(err).NotTo(HaveOccurred())
					rev1 := outRes1.ResourceVersion

					By("Configuring a BGPFilter name2/spec2 and storing the response")
					outRes2, err := c.BGPFilter().Create(
						ctx,
						&apiv3.BGPFilter{
							ObjectMeta: metav1.ObjectMeta{Name: name2},
							Spec:       spec2,
						},
						options.SetOptions{},
					)
					Expect(err).NotTo(HaveOccurred())

					By("Starting a watcher from revision rev1 - this should skip the first creation")
					w, err := c.BGPFilter().Watch(ctx, options.ListOptions{ResourceVersion: rev1})
					Expect(err).NotTo(HaveOccurred())
					testWatcher1 := testutils.NewTestResourceWatch(config.Spec.DatastoreType, w)
					defer testWatcher1.Stop()

					By("Deleting res1")
					_, err = c.BGPFilter().Delete(ctx, name1, options.DeleteOptions{})
					Expect(err).NotTo(HaveOccurred())

					By("Checking for two events, create res2 and delete re1")
					testWatcher1.ExpectEvents(apiv3.KindBGPFilter, []watch.Event{
						{
							Type:   watch.Added,
							Object: outRes2,
						},
						{
							Type:     watch.Deleted,
							Previous: outRes1,
						},
					})
					testWatcher1.Stop()

					By("Starting a watcher from rev0 - this should get all events")
					w, err = c.BGPFilter().Watch(ctx, options.ListOptions{ResourceVersion: rev0})
					Expect(err).NotTo(HaveOccurred())
					testWatcher2 := testutils.NewTestResourceWatch(config.Spec.DatastoreType, w)
					defer testWatcher2.Stop()

					By("Modifying res2")
					outRes3, err := c.BGPFilter().Update(
						ctx,
						&apiv3.BGPFilter{
							ObjectMeta: outRes2.ObjectMeta,
							Spec:       spec1,
						},
						options.SetOptions{},
					)
					Expect(err).NotTo(HaveOccurred())
					testWatcher2.ExpectEvents(apiv3.KindBGPFilter, []watch.Event{
						{
							Type:   watch.Added,
							Object: outRes1,
						},
						{
							Type:   watch.Added,
							Object: outRes2,
						},
						{
							Type:     watch.Deleted,
							Previous: outRes1,
						},
						{
							Type:     watch.Modified,
							Previous: outRes2,
							Object:   outRes3,
						},
					})
					testWatcher2.Stop()

					// Only etcdv3 supports watching a specific instance of a resource.
					if config.Spec.DatastoreType == apiconfig.EtcdV3 {
						By("Starting a watcher from rev0 watching name1 - this should get all events for name1")
						w, err = c.BGPFilter().Watch(ctx, options.ListOptions{Name: name1, ResourceVersion: rev0})
						Expect(err).NotTo(HaveOccurred())
						testWatcher2_1 := testutils.NewTestResourceWatch(config.Spec.DatastoreType, w)
						defer testWatcher2_1.Stop()
						testWatcher2_1.ExpectEvents(apiv3.KindBGPFilter, []watch.Event{
							{
								Type:   watch.Added,
								Object: outRes1,
							},
							{
								Type:     watch.Deleted,
								Previous: outRes1,
							},
						})
						testWatcher2_1.Stop()
					}

					By("Starting a watcher not specifying a rev - expect the current snapshot")
					w, err = c.BGPFilter().Watch(ctx, options.ListOptions{})
					Expect(err).NotTo(HaveOccurred())
					testWatcher3 := testutils.NewTestResourceWatch(config.Spec.DatastoreType, w)
					defer testWatcher3.Stop()
					testWatcher3.ExpectEvents(apiv3.KindBGPFilter, []watch.Event{
						{
							Type:   watch.Added,
							Object: outRes3,
						},
					})
					testWatcher3.Stop()

					By("Configuring BGPFilter name1/spec1 again and storing the response")
					outRes1, err = c.BGPFilter().Create(
						ctx,
						&apiv3.BGPFilter{
							ObjectMeta: metav1.ObjectMeta{Name: name1},
							Spec:       spec1,
						},
						options.SetOptions{},
					)

					By("Starting a watcher not specifying a rev - expect the current snapshot")
					w, err = c.BGPFilter().Watch(ctx, options.ListOptions{})
					Expect(err).NotTo(HaveOccurred())
					testWatcher4 := testutils.NewTestResourceWatch(config.Spec.DatastoreType, w)
					defer testWatcher4.Stop()
					testWatcher4.ExpectEventsAnyOrder(apiv3.KindBGPFilter, []watch.Event{
						{
							Type:   watch.Added,
							Object: outRes1,
						},
						{
							Type:   watch.Added,
							Object: outRes3,
						},
					})

					By("Cleaning the datastore and expecting deletion events for each configured resource (tests prefix deletes results in individual events for each key)")
					be.Clean()
					testWatcher4.ExpectEventsAnyOrder(apiv3.KindBGPFilter, []watch.Event{
						{
							Type:     watch.Deleted,
							Previous: outRes1,
						},
						{
							Type:     watch.Deleted,
							Previous: outRes3,
						},
					})
					testWatcher4.Stop()
				})
			})
		})
	}
})
