// Copyright (c) 2025 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 fv_test

import (
	"context"
	"net"
	"regexp"

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

	"github.com/projectcalico/calico/felix/bpf/nat"
	"github.com/projectcalico/calico/felix/fv/infrastructure"
	"github.com/projectcalico/calico/felix/fv/workload"
	"github.com/projectcalico/calico/libcalico-go/lib/apiconfig"
	client "github.com/projectcalico/calico/libcalico-go/lib/clientv3"
)

var (
	_ = describeBPFMultiHomedTests()
)

func describeBPFMultiHomedTests() bool {
	if !BPFMode() {
		return true
	}
	desc := "_BPF_ _BPF-SAFE_ BPF multi-homed tests"
	return infrastructure.DatastoreDescribe(desc, []apiconfig.DatastoreType{apiconfig.Kubernetes}, func(getInfra infrastructure.InfraFactory) {
		var (
			infra           infrastructure.DatastoreInfra
			tc              infrastructure.TopologyContainers
			calicoClient    client.Interface
			Felix           *infrastructure.Felix
			w, eth20, eth30 *workload.Workload
		)

		BeforeEach(func() {
			infra = getInfra()
			opts := infrastructure.DefaultTopologyOptions()
			opts.FelixLogSeverity = "Debug"
			opts.ExtraEnvVars["FELIX_BPFLogLevel"] = "Debug"
			opts.ExtraEnvVars["FELIX_BPFExtToServiceConnmark"] = "0x80"
			tc, calicoClient = infrastructure.StartNNodeTopology(2, opts, infra)
			Felix = tc.Felixes[0]

			infrastructure.AssignIP("workload", "10.65.0.2", Felix.Hostname, calicoClient)
			w = workload.New(Felix, "workload", "default", "10.65.0.2", "8055", "tcp")
			err := w.Start(infra)
			Expect(err).NotTo(HaveOccurred())
			w.ConfigureInInfra(infra)

			ensureBPFProgramsAttached(tc.Felixes[0])

			By("setting up node's fake external iface")
			// We name the iface eth20 since such ifaces are
			// treated by felix as external to the node
			//
			// Using a test-workload creates the namespaces and the
			// interfaces to emulate the host NICs
			eth20 = &workload.Workload{
				Name:          "eth20",
				C:             Felix.Container,
				IP:            "192.168.20.1",
				Ports:         "57005", // 0xdead
				Protocol:      "tcp",
				InterfaceName: "eth20",
				MTU:           1500, // Need to match host MTU or felix will restart.
			}
			err = eth20.Start(infra)
			Expect(err).NotTo(HaveOccurred())

			eth30 = &workload.Workload{
				Name:          "eth30",
				C:             Felix.Container,
				IP:            "192.168.30.1",
				Ports:         "57005", // 0xdead
				Protocol:      "tcp",
				InterfaceName: "eth30",
				MTU:           1500, // Need to match host MTU or felix will restart.
			}
			err = eth30.Start(infra)
			Expect(err).NotTo(HaveOccurred())

			// assign address to eth20 and add route to the .20 network
			// tc.Felixes[1].Exec("ip", "route", "add", eth20Route, "dev", "eth20")
			// This multi-NIC scenario works only if the kernel's RPF check
			// is not strict so we need to override it for the test and must
			// be set properly when product is deployed. We reply on
			// iptables to do require check for us.
			Felix.Exec("sysctl", "-w", "net.ipv4.conf.eth0.rp_filter=2")

			Eventually(func() error {
				return Felix.ExecMayFail("sysctl", "-w", "net.ipv4.conf.eth20.rp_filter=2")
			}, "5s", "300ms").Should(Succeed())

			Eventually(func() error {
				return Felix.ExecMayFail("sysctl", "-w", "net.ipv4.conf.eth30.rp_filter=2")
			}, "5s", "300ms").Should(Succeed())

			_, err = eth20.RunCmd("ip", "route", "add", "blackhole", "10.65.1.0/32")
			Expect(err).NotTo(HaveOccurred())

			_, err = eth30.RunCmd("ip", "addr", "add", "192.168.30.1/24", "dev", "eth0")
			Expect(err).NotTo(HaveOccurred())
			_, err = eth30.RunCmd("ip", "route", "add", "10.65.0.0/24", "via", "192.168.30.30", "dev", "eth0")
			Expect(err).NotTo(HaveOccurred())

			_, err = w.RunCmd("ip", "rule", "del", "from", "all", "lookup", "local", "priority", "0")
			Expect(err).NotTo(HaveOccurred())
			_, err = w.RunCmd("ip", "rule", "add", "from", "all", "lookup", "local", "priority", "2")
			Expect(err).NotTo(HaveOccurred())
			_, err = w.RunCmd("ip", "route", "add", "blackhole", w.IP+"/32")
			Expect(err).NotTo(HaveOccurred())
		})

		It("should allow asymmetric routing", func() {
			var err error

			Felix.Exec("ip", "addr", "add", "192.168.20.20/24", "dev", "lo")
			Felix.Exec("ip", "addr", "add", "192.168.30.30/24", "dev", "eth30")
			Felix.Exec("bash", "-c", "echo 200 container_route >> /etc/iproute2/rt_tables")
			Felix.Exec("ip", "route", "add", "10.65.1.0/24", "dev", "eth20",
				"table", "container_route")
			Felix.Exec("ip", "rule", "add", "from", w.IP, "table", "container_route")
			Felix.Exec("ip", "route", "flush", "cache")
			Felix.Exec("ip", "neigh", "add", "10.65.1.3", "lladdr", "ee:ee:ee:ee:ee:ee", "dev", "eth20")

			Felix.Exec("ip", "route", "add", "192.168.30.1/32", "dev", "eth30")

			dump20 := Felix.AttachTCPDump("eth20")
			dump20.SetLogEnabled(true)
			dump20.AddMatcher("eth20-egress", regexp.MustCompile("10.65.0.2.30444 > 10.65.1.3.30444: UDP"))
			dump20.Start(infra, "-v", "udp", "and", "dst", "host", "10.65.1.3")

			dump30 := Felix.AttachTCPDump("eth30")
			dump30.SetLogEnabled(true)
			dump30.AddMatcher("eth30-ingress", regexp.MustCompile("10.65.1.3.30444 > 10.65.0.2.30444: UDP"))
			dump30.Start(infra, "-v", "udp", "and", "dst", "host", "10.65.0.2")

			By("Sending packet from the workload via eth20")
			_, err = w.RunCmd("pktgen", w.IP, "10.65.1.3", "udp", "--ip-id", "1",
				"--port-src", "30444", "--port-dst", "30444")
			Expect(err).NotTo(HaveOccurred())

			By("Sending reply via eth30")
			_, err = eth30.RunCmd("pktgen", "10.65.1.3", w.IP, "udp", "--ip-id", "2",
				"--port-src", "30444", "--port-dst", "30444")
			Expect(err).NotTo(HaveOccurred())

			By("Sending packet from the workload via eth20")
			_, err = w.RunCmd("pktgen", w.IP, "10.65.1.3", "udp", "--ip-id", "3",
				"--port-src", "30444", "--port-dst", "30444")
			Expect(err).NotTo(HaveOccurred())

			By("Sending reply via eth30")
			_, err = eth30.RunCmd("pktgen", "10.65.1.3", w.IP, "udp", "--ip-id", "4",
				"--port-src", "30444", "--port-dst", "30444")
			Expect(err).NotTo(HaveOccurred())

			Eventually(dump20.MatchCountFn("eth20-egress"), "5s", "330ms").Should(BeNumerically("==", 2))
			Eventually(dump30.MatchCountFn("eth30-ingress"), "5s", "330ms").Should(BeNumerically("==", 2))
		})

		It("should route external traffic marked with connmark correctly", func() {
			var err error

			clusterIP := "10.101.123.1"
			testSvc := k8sService("test-service", clusterIP, w, 30444, 444, 30444, "udp")
			k8sClient := infra.(*infrastructure.K8sDatastoreInfra).K8sClient
			testSvcNamespace := testSvc.Namespace
			_, err = k8sClient.CoreV1().Services(testSvcNamespace).Create(context.Background(), testSvc, metav1.CreateOptions{})
			Expect(err).NotTo(HaveOccurred())
			Eventually(checkSvcEndpoints(k8sClient, testSvc), "10s").Should(Equal(1),
				"Service endpoints didn't get created. Is controller-manager happy?")

			_, err = eth30.RunCmd("ip", "route", "add", clusterIP+"/32", "via", "192.168.30.30", "dev", "eth0")
			Expect(err).NotTo(HaveOccurred())

			Felix.Exec("ip", "addr", "add", "192.168.20.20/24", "dev", "eth20")
			Felix.Exec("ip", "addr", "add", "192.168.30.30/24", "dev", "eth30")

			Felix.Exec("bash", "-c", "echo 200 mark_route >> /etc/iproute2/rt_tables")
			Felix.Exec("ip", "route", "add", "10.65.1.0/24", "dev", "eth20",
				"table", "mark_route")
			Felix.Exec("ip", "rule", "add", "fwmark", "0x80/0x80", "table", "mark_route")
			Felix.Exec("ip", "route", "flush", "cache")
			Felix.Exec("ip", "neigh", "add", "10.65.1.3", "lladdr", "ee:ee:ee:ee:ee:ee", "dev", "eth20")

			dump20 := Felix.AttachTCPDump("eth20")
			dump20.SetLogEnabled(true)
			dump20.AddMatcher("eth20-egress", regexp.MustCompile(clusterIP+".30444 > 10.65.1.3.30444: UDP"))
			dump20.Start(infra, "-v", "udp", "and", "src", "host", clusterIP)

			dump30 := Felix.AttachTCPDump("eth30")
			dump30.SetLogEnabled(true)
			dump30.AddMatcher("eth30-ingress", regexp.MustCompile("10.65.1.3.30444 > "+clusterIP+".30444: UDP"))
			dump30.Start(infra, "-v", "udp", "and", "dst", "host", clusterIP)

			ip := testSvc.Spec.ClusterIP
			natK := nat.NewNATKey(net.ParseIP(ip), 30444, 17)

			Eventually(func() bool {
				natmaps, _, _ := dumpNATMapsAny(4, Felix)
				if _, ok := natmaps[natK]; !ok {
					return false
				}
				return true
			}, "5s").Should(BeTrue(), "service NAT key didn't show up")

			By("Sending request via eth30")
			_, err = eth30.RunCmd("pktgen", "10.65.1.3", clusterIP, "udp", "--ip-id", "1",
				"--port-src", "30444", "--port-dst", "30444")
			Expect(err).NotTo(HaveOccurred())

			By("Sending reply from the workload via eth20")
			_, err = w.RunCmd("pktgen", w.IP, "10.65.1.3", "udp", "--ip-id", "2",
				"--port-src", "444", "--port-dst", "30444")
			Expect(err).NotTo(HaveOccurred())

			Eventually(dump30.MatchCountFn("eth30-ingress"), "5s", "330ms").Should(BeNumerically("==", 1))
			Eventually(dump20.MatchCountFn("eth20-egress"), "5s", "330ms").Should(BeNumerically("==", 1))
		})
	})
}
