// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package builder

import (
	_ "embed"
	"fmt"

	"github.com/cilium/cilium/cilium-cli/connectivity/builder/manifests/template"
	"github.com/cilium/cilium/cilium-cli/connectivity/check"
	"github.com/cilium/cilium/cilium-cli/utils/features"
)

var (
	//go:embed manifests/allow-all-egress.yaml
	allowAllEgressPolicyYAML string

	//go:embed manifests/allow-all-ingress.yaml
	allowAllIngressPolicyYAML string

	//go:embed manifests/deny-all-ingress.yaml
	denyAllIngressPolicyYAML string

	//go:embed manifests/deny-cidr.yaml
	denyCIDRPolicyYAML string

	//go:embed manifests/allow-cluster-entity.yaml
	allowClusterEntityPolicyYAML string

	//go:embed manifests/client-egress-only-dns.yaml
	clientEgressOnlyDNSPolicyYAML string

	//go:embed manifests/client-egress-to-fqdns.yaml
	clientEgressToFQDNsPolicyYAML string

	//go:embed manifests/client-egress-to-fqdns-and-http-get.yaml
	clientEgressToFQDNsAndHTTPGetPolicyYAML string

	//go:embed manifests/echo-ingress-from-other-client.yaml
	echoIngressFromOtherClientPolicyYAML string

	//go:embed manifests/client-egress-to-cidr-cp-host-knp.yaml
	clientEgressToCIDRCPHostPolicyYAML string

	//go:embed manifests/client-egress-to-cidr-external.yaml
	clientEgressToCIDRExternalPolicyYAML string

	//go:embed manifests/client-egress-to-cidr-external-knp.yaml
	clientEgressToCIDRExternalPolicyKNPYAML string

	//go:embed manifests/client-egress-to-cidr-k8s.yaml
	clientEgressToCIDRK8sPolicyYAML string

	//go:embed manifests/client-egress-to-cidr-node-knp.yaml
	clientEgressToCIDRNodeKNPYAML string

	//go:embed manifests/client-egress-to-cidr-external-deny.yaml
	clientEgressToCIDRExternalDenyPolicyYAML string

	//go:embed manifests/client-egress-to-cidrgroup-external-deny.yaml
	clientEgressToCIDRGroupExternalDenyPolicyYAML string

	//go:embed manifests/client-egress-to-cidrgroup-external-deny-v2alpha1.yaml
	clientEgressToCIDRGroupExternalDenyPolicyV2Alpha1YAML string

	//go:embed manifests/client-egress-to-cidrgroup-external-deny-label.yaml
	clientEgressToCIDRGroupExternalDenyLabelPolicyYAML string

	//go:embed manifests/client-egress-to-cidrgroup-external-deny-label-v2alpha1.yaml
	clientEgressToCIDRGroupExternalDenyLabelPolicyV2Alpha1YAML string

	//go:embed manifests/client-egress-l7-http.yaml
	clientEgressL7HTTPPolicyYAML string

	//go:embed manifests/client-egress-l7-http-port-range.yaml
	clientEgressL7HTTPPolicyPortRangeYAML string

	//go:embed manifests/client-egress-l7-http-named-port.yaml
	clientEgressL7HTTPNamedPortPolicyYAML string

	//go:embed manifests/client-egress-tls-sni.yaml
	clientEgressTLSSNIPolicyYAML string

	//go:embed manifests/client-egress-tls-sni-wildcard.yaml
	clientEgressTLSSNIWildcardPolicyYAML string

	//go:embed manifests/client-egress-tls-sni-double-wildcard.yaml
	clientEgressTLSSNIDoubleWildcardPolicyYAML string

	//go:embed manifests/client-egress-tls-sni-other.yaml
	clientEgressTLSSNIOtherPolicyYAML string

	//go:embed manifests/client-egress-l7-tls-sni.yaml
	clientEgressL7TLSSNIPolicyYAML string

	//go:embed manifests/client-egress-l7-tls-other-sni.yaml
	clientEgressL7TLSOtherSNIPolicyYAML string

	//go:embed manifests/client-egress-l7-tls.yaml
	clientEgressL7TLSPolicyYAML string

	//go:embed manifests/client-egress-l7-tls-port-range.yaml
	clientEgressL7TLSPolicyPortRangeYAML string

	//go:embed manifests/client-egress-l7-http-matchheader-secret.yaml
	clientEgressL7HTTPMatchheaderSecretYAML string

	//go:embed manifests/client-egress-l7-http-matchheader-secret-port-range.yaml
	clientEgressL7HTTPMatchheaderSecretPortRangeYAML string

	//go:embed manifests/echo-ingress-from-cidr.yaml
	echoIngressFromCIDRYAML string
)

var (
	clientLabel  = map[string]string{"name": "client"}
	client2Label = map[string]string{"name": "client2"}
)

type testBuilder interface {
	build(ct *check.ConnectivityTest, templates map[string]string)
}

// GetTestSuites returns a slice of functions, that when invoked, result in a
// collection of tests being built. These functions use helper methods to add various
// collections of test builders depending upon the provided parameters.
func GetTestSuites(params check.Parameters) ([]func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error, error) {
	switch {
	case params.Perf:
		if params.PerfParameters.NetQos {
			return []func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error{
				func(connTests []*check.ConnectivityTest, _ func(cts ...*check.ConnectivityTest) error) error {
					return networkQosTests(connTests[0])
				},
			}, nil
		}
		if params.PerfParameters.Bandwidth {
			return []func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error{
				func(connTests []*check.ConnectivityTest, _ func(cts ...*check.ConnectivityTest) error) error {
					return netBandwidthLimitTests(connTests[0])
				},
			}, nil
		}
		return []func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error{
			func(connTests []*check.ConnectivityTest, _ func(cts ...*check.ConnectivityTest) error) error {
				return networkPerformanceTests(connTests[0])
			},
		}, nil
	case params.IncludeConnDisruptTest && params.ConnDisruptTestSetup:
		// Exit early, as --conn-disrupt-test-setup is only needed to deploy pods which
		// will be used by another invocation of "cli connectivity test"
		// with include --include-conn-disrupt-test"
		return []func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error{
			func(connTests []*check.ConnectivityTest, _ func(cts ...*check.ConnectivityTest) error) error {
				return connDisruptTests(connTests[0])
			},
		}, nil
	case params.TestConcurrency > 1:
		return []func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error{
			func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error {
				if connTests[0].Params().IncludeConnDisruptTest {
					if err := connDisruptTests(connTests[0]); err != nil {
						return err
					}
				}
				if err := concurrentTests(connTests); err != nil {
					return err
				}
				return extraTests(connTests...)
			},
			func(connTests []*check.ConnectivityTest, _ func(cts ...*check.ConnectivityTest) error) error {
				if err := sequentialTests(connTests[0]); err != nil {
					return err
				}
				return finalTests(connTests[0])
			},
		}, nil
	default: // fallback to the sequential run
		return []func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error{
			func(connTests []*check.ConnectivityTest, extraTests func(cts ...*check.ConnectivityTest) error) error {
				if connTests[0].Params().IncludeConnDisruptTest {
					if err := connDisruptTests(connTests[0]); err != nil {
						return err
					}
				}
				if err := concurrentTests(connTests); err != nil {
					return err
				}
				if err := sequentialTests(connTests[0]); err != nil {
					return err
				}
				if err := extraTests(connTests...); err != nil {
					return err
				}
				return finalTests(connTests[0])
			},
		}, nil
	}
}

// networkPerformanceTests injects the network performance connectivity tests.
func networkPerformanceTests(ct *check.ConnectivityTest) error {
	tests := []testBuilder{networkPerf{}}
	return injectTests(tests, ct)
}

// networkQosTests injects the network performance connectivity tests.
func networkQosTests(ct *check.ConnectivityTest) error {
	tests := []testBuilder{networkQos{}}
	return injectTests(tests, ct)
}

// netBandwidthLimitTests injects the network performance connectivity tests.
func netBandwidthLimitTests(ct *check.ConnectivityTest) error {
	tests := []testBuilder{networkBandwidthLimit{}}
	return injectTests(tests, ct)
}

// connDisruptTests injects the conn-disrupt connectivity tests.
func connDisruptTests(ct *check.ConnectivityTest) error {
	tests := []testBuilder{
		noInterruptedConnections{},
		noIpsecXfrmErrors{},
	}
	return injectTests(tests, ct)
}

// concurrentTests injects the all connectivity tests that can be run concurrently.
// Each test should be run in a separate namespace.
func concurrentTests(connTests []*check.ConnectivityTest) error {
	tests := []testBuilder{
		noPolicies{},
		noPoliciesFromOutside{},
		noPoliciesExtra{},
		allowAllExceptWorld{},
		clientIngress{},
		clientIngressKnp{},
		allowAllWithMetricsCheck{},
		allIngressDeny{},
		allIngressDenyFromOutside{},
		allIngressDenyKnp{},
		allEgressDeny{},
		allEgressDenyKnp{},
		allEntitiesDeny{},
		clusterEntity{},
		clusterEntityMultiCluster{},
		hostEntityEgress{},
		hostEntityIngress{},
		echoIngress{},
		echoIngressFromOutside{},
		echoIngressKnp{},
		clientIngressIcmp{},
		clientEgress{},
		clientEgressKnp{},
		clientEgressExpression{},
		clientEgressExpressionKnp{},
		clientWithServiceAccountEgressToEcho{},
		clientEgressToEchoServiceAccount{},
		toEntitiesWorld{},
		toCidrExternal{},
		toCidrExternalKnp{},
		fromCidrHostNetns{},
		echoIngressFromOtherClientDeny{},
		clientIngressFromOtherClientIcmpDeny{},
		clientEgressToEchoDeny{},
		clientIngressToEchoNamedPortDeny{},
		clientEgressToEchoExpressionDeny{},
		clientWithServiceAccountEgressToEchoDeny{},
		clientEgressToEchoServiceAccountDeny{},
		clientEgressToCidrDeny{},
		clientEgressToCidrgroupDeny{},
		clientEgressToCidrgroupDenyByLabel{},
		clientEgressToCidrDenyDefault{},
		clusterMeshEndpointSliceSync{},
		health{},
		northSouthLoadbalancing{},
		podToPodEncryption{},
		podToPodEncryptionV2{},
		nodeToNodeEncryption{},
		egressGateway{},
		egressGatewayMultigateway{},
		egressGatewayExcludedCidrs{},
		egressGatewayWithL7Policy{},
		podToNodeCidrpolicy{},
		northSouthLoadbalancingWithL7Policy{},
		echoIngressL7{},
		echoIngressL7NamedPort{},
		clientEgressL7Method{},
		clientEgressL7{},
		clientEgressL7NamedPort{},
		clientEgressTlsSni{},
		clientEgressL7SetHeader{},
		echoIngressAuthAlwaysFail{},
		echoIngressMutualAuthSpiffe{},
		podToIngressService{},
		outsideToIngressService{},
		l7LB{},
		dnsOnly{},
		toFqdns{},
		toFqdnsWithProxy{},
		podToControlplaneHost{},
		podToK8sOnControlplane{},
		podToControlplaneHostCidr{},
		podToK8sOnControlplaneCidr{},
		policyLocalCluster{},
		localRedirectPolicy{},
		localRedirectPolicyWithNodeDNS{},
		noFragmentation{},
		bgpControlPlane{},
		multicast{},
		strictModeEncryption{},
		ipsecKeyDerivation{},
	}
	return injectTests(tests, connTests...)
}

// sequentialTests injects the all connectivity tests that must be run sequentially.
func sequentialTests(ct *check.ConnectivityTest) error {
	tests := []testBuilder{
		hostFirewallIngress{},
		hostFirewallEgress{},
		clientEgressL7TlsDenyWithoutHeaders{},
		clientEgressL7TlsHeaders{},
	}
	return injectTests(tests, ct)
}

// finalTests injects the all connectivity tests that must be run as the last tests sequentially.
func finalTests(ct *check.ConnectivityTest) error {
	return injectTests([]testBuilder{
		noUnexpectedPacketDrops{},
		checkLogErrors{},
	}, ct)
}

func renderTemplates(clusterNameLocal, clusterNameRemote string, param check.Parameters) (map[string]string, error) {
	templates := map[string]string{
		"clientEgressToCIDRExternalPolicyYAML":                       clientEgressToCIDRExternalPolicyYAML,
		"clientEgressToCIDRExternalPolicyKNPYAML":                    clientEgressToCIDRExternalPolicyKNPYAML,
		"clientEgressToCIDRNodeKNPYAML":                              clientEgressToCIDRNodeKNPYAML,
		"clientEgressToCIDRExternalDenyPolicyYAML":                   clientEgressToCIDRExternalDenyPolicyYAML,
		"clientEgressToCIDRGroupExternalDenyPolicyYAML":              clientEgressToCIDRGroupExternalDenyPolicyYAML,
		"clientEgressToCIDRGroupExternalDenyPolicyV2Alpha1YAML":      clientEgressToCIDRGroupExternalDenyPolicyV2Alpha1YAML,
		"clientEgressToCIDRGroupExternalDenyLabelPolicyYAML":         clientEgressToCIDRGroupExternalDenyLabelPolicyYAML,
		"clientEgressToCIDRGroupExternalDenyLabelPolicyV2Alpha1YAML": clientEgressToCIDRGroupExternalDenyLabelPolicyV2Alpha1YAML,
		"clientEgressL7HTTPPolicyYAML":                               clientEgressL7HTTPPolicyYAML,
		"clientEgressL7HTTPPolicyPortRangeYAML":                      clientEgressL7HTTPPolicyPortRangeYAML,
		"clientEgressL7HTTPNamedPortPolicyYAML":                      clientEgressL7HTTPNamedPortPolicyYAML,
		"clientEgressToFQDNsPolicyYAML":                              clientEgressToFQDNsPolicyYAML,
		"clientEgressToFQDNsAndHTTPGetPolicyYAML":                    clientEgressToFQDNsAndHTTPGetPolicyYAML,
		"clientEgressTLSSNIPolicyYAML":                               clientEgressTLSSNIPolicyYAML,
		"clientEgressTLSSNIWildcardPolicyYAML":                       clientEgressTLSSNIWildcardPolicyYAML,
		"clientEgressTLSSNIDoubleWildcardPolicyYAML":                 clientEgressTLSSNIDoubleWildcardPolicyYAML,
		"clientEgressTLSSNIOtherPolicyYAML":                          clientEgressTLSSNIOtherPolicyYAML,
		"clientEgressL7TLSSNIPolicyYAML":                             clientEgressL7TLSSNIPolicyYAML,
		"clientEgressL7TLSOtherSNIPolicyYAML":                        clientEgressL7TLSOtherSNIPolicyYAML,
		"clientEgressL7TLSPolicyYAML":                                clientEgressL7TLSPolicyYAML,
		"clientEgressL7TLSPolicyPortRangeYAML":                       clientEgressL7TLSPolicyPortRangeYAML,
		"clientEgressL7HTTPMatchheaderSecretYAML":                    clientEgressL7HTTPMatchheaderSecretYAML,
		"clientEgressL7HTTPMatchheaderSecretPortRangeYAML":           clientEgressL7HTTPMatchheaderSecretPortRangeYAML,
		"clientEgressL7HTTPExternalYAML":                             clientEgressL7HTTPExternalYAML,
		"clientEgressNodeLocalDNSYAML":                               clientEgressNodeLocalDNSYAML,
		"clientEgressOnlyDNSPolicyYAML":                              clientEgressOnlyDNSPolicyYAML,
		"echoIngressFromCIDRYAML":                                    echoIngressFromCIDRYAML,
		"denyCIDRPolicyYAML":                                         denyCIDRPolicyYAML,
	}
	if param.K8sLocalHostTest {
		templates["clientEgressToCIDRCPHostPolicyYAML"] = clientEgressToCIDRCPHostPolicyYAML
		templates["clientEgressToCIDRK8sPolicyKNPYAML"] = clientEgressToCIDRK8sPolicyYAML
	}

	renderedTemplates := map[string]string{}
	for key, temp := range templates {
		val, err := template.Render(temp, struct {
			check.Parameters
			ClusterNameLocal  string
			ClusterNameRemote string
		}{
			Parameters:        param,
			ClusterNameLocal:  clusterNameLocal,
			ClusterNameRemote: clusterNameRemote,
		})
		if err != nil {
			return nil, fmt.Errorf("failed to render template %s: %w", key, err)
		}
		renderedTemplates[key] = val
	}
	return renderedTemplates, nil
}

func injectTests(tests []testBuilder, connTests ...*check.ConnectivityTest) error {
	// templates must be compiled per test namespace due to
	// namespace specific placeholders in the network policies
	templates := make(map[string]map[string]string, len(connTests))
	id := 0
	for i := range tests {
		if _, ok := templates[connTests[id].Params().TestNamespace]; !ok {
			nsTemplates, err := renderTemplates(
				connTests[id].ClusterNameLocal, connTests[id].ClusterNameRemote,
				connTests[id].Params(),
			)
			if err != nil {
				return err
			}
			templates[connTests[id].Params().TestNamespace] = nsTemplates
		}
		tests[i].build(connTests[id], templates[connTests[id].Params().TestNamespace])
		id++
		if id == len(connTests) {
			id = 0
		}
	}
	return nil
}

func newTest(name string, ct *check.ConnectivityTest) *check.Test {
	test := check.NewTest(name, ct.Params().Verbose, ct.Params().Debug)
	return ct.AddTest(test)
}

func withKPRReqForMultiCluster(ct *check.ConnectivityTest, reqs ...features.Requirement) []features.Requirement {
	// Skip the nodeport-related tests in the multicluster scenario if KPR is not
	// enabled, since global nodeport services are not supported in that case.
	if ct.Params().MultiCluster != "" {
		reqs = append(reqs, features.RequireEnabled(features.KPR))
	}
	return reqs
}
