// Copyright 2020-2026 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0

package e2e

import (
	"context"
	"fmt"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/client-go/kubernetes"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	capsuleapi "github.com/projectcapsule/capsule/pkg/api"
	"github.com/projectcapsule/capsule/pkg/api/meta"
	"github.com/projectcapsule/capsule/pkg/api/rbac"
	"github.com/projectcapsule/capsule/pkg/api/rules"
	"github.com/projectcapsule/capsule/pkg/api/runtime"
	"github.com/projectcapsule/capsule/pkg/runtime/events"
)

var _ = Describe("enforcing service namespace rules", Ordered, Label("tenant", "rules", "enforce", "services"), func() {
	const ownerName = "e2e-rules-services"

	var (
		tnt         *capsulev1beta2.Tenant
		tenantRules []*rules.NamespaceRuleBodyTenant
	)

	externalNameByExpression := func(expression string) runtime.ExpressionMatch {
		return runtime.ExpressionMatch{
			ExpressionRegex: runtime.ExpressionRegex{
				Expression: expression,
			},
		}
	}

	externalNameByNegatedExpression := func(expression string) runtime.ExpressionMatch {
		return runtime.ExpressionMatch{
			ExpressionRegex: runtime.ExpressionRegex{
				Expression: expression,
				Negate:     true,
			},
		}
	}

	externalNameByExact := func(exact ...string) runtime.ExpressionMatch {
		return runtime.ExpressionMatch{
			Exact: exact,
		}
	}

	externalNameByMatch := func(exact []string, expression string) runtime.ExpressionMatch {
		return runtime.ExpressionMatch{
			ExpressionRegex: runtime.ExpressionRegex{
				Expression: expression,
			},
			Exact: exact,
		}
	}

	serviceTypeRule := func(action rules.ActionType, serviceTypes ...rules.ServiceType) *rules.NamespaceRuleBodyTenant {
		return &rules.NamespaceRuleBodyTenant{
			NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
				Enforce: &rules.NamespaceRuleEnforceBody{
					Action: action,
					Services: rules.NamespaceRuleEnforceServicesBody{
						Types: serviceTypes,
					},
				},
			},
		}
	}

	loadBalancerCIDRRule := func(action rules.ActionType, cidrs ...string) *rules.NamespaceRuleBodyTenant {
		return &rules.NamespaceRuleBodyTenant{
			NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
				Enforce: &rules.NamespaceRuleEnforceBody{
					Action: action,
					Services: rules.NamespaceRuleEnforceServicesBody{
						LoadBalancers: &rules.ServiceLoadBalancerRule{
							CIDRs: cidrs,
						},
					},
				},
			},
		}
	}

	externalIPCIDRRule := func(action rules.ActionType, cidrs ...string) *rules.NamespaceRuleBodyTenant {
		return &rules.NamespaceRuleBodyTenant{
			NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
				Enforce: &rules.NamespaceRuleEnforceBody{
					Action: action,
					Services: rules.NamespaceRuleEnforceServicesBody{
						ExternalIPs: &rules.ServiceExternalIPRule{
							CIDRs: cidrs,
						},
					},
				},
			},
		}
	}

	externalNameRule := func(action rules.ActionType, hostnames ...runtime.ExpressionMatch) *rules.NamespaceRuleBodyTenant {
		return &rules.NamespaceRuleBodyTenant{
			NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
				Enforce: &rules.NamespaceRuleEnforceBody{
					Action: action,
					Services: rules.NamespaceRuleEnforceServicesBody{
						ExternalNames: &rules.ServiceExternalNameRule{
							Hostnames: hostnames,
						},
					},
				},
			},
		}
	}

	nodePortRule := func(action rules.ActionType, ports ...rules.ServiceNodePortRange) *rules.NamespaceRuleBodyTenant {
		return &rules.NamespaceRuleBodyTenant{
			NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
				Enforce: &rules.NamespaceRuleEnforceBody{
					Action: action,
					Services: rules.NamespaceRuleEnforceServicesBody{
						NodePorts: &rules.ServiceNodePortRule{
							Ports: ports,
						},
					},
				},
			},
		}
	}

	selectedRule := func(
		selector map[string]string,
		rule *rules.NamespaceRuleBodyTenant,
	) *rules.NamespaceRuleBodyTenant {
		rule.NamespaceSelector = &metav1.LabelSelector{
			MatchLabels: selector,
		}

		return rule
	}

	baseTenantRules := func() []*rules.NamespaceRuleBodyTenant {
		return []*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeClusterIP,
				rules.ServiceTypeNodePort,
				rules.ServiceTypeLoadBalancer,
				rules.ServiceTypeExternalName,
			),
			loadBalancerCIDRRule(
				rules.ActionTypeAllow,
				"10.0.0.2/32",
				"10.0.1.0/24",
			),
			externalNameRule(
				rules.ActionTypeAllow,
				externalNameByExact("internal.git.com"),
				externalNameByExpression(".*\\.example\\.com"),
				externalNameByMatch(
					[]string{"combined.internal.git.com"},
					"combined\\..*\\.example\\.com",
				),
			),
			nodePortRule(
				rules.ActionTypeAllow,
				rules.ServiceNodePortRange{
					From: 30000,
					To:   30100,
				},
				rules.ServiceNodePortRange{
					From: 30500,
					To:   30500,
				},
			),
			nodePortRule(
				rules.ActionTypeDeny,
				rules.ServiceNodePortRange{
					From: 30090,
					To:   30090,
				},
			),
			loadBalancerCIDRRule(
				rules.ActionTypeDeny,
				"10.0.66.0/24",
			),
			externalNameRule(
				rules.ActionTypeAudit,
				externalNameByExpression("audit\\..*"),
			),
			selectedRule(
				map[string]string{
					"environment": "prod",
				},
				loadBalancerCIDRRule(
					rules.ActionTypeAllow,
					"10.0.171.0/24",
				),
			),
			selectedRule(
				map[string]string{
					"external-policy": "restricted",
				},
				externalNameRule(
					rules.ActionTypeDeny,
					externalNameByExact("blocked.example.com"),
				),
			),
			selectedRule(
				map[string]string{
					"negate": "true",
				},
				externalNameRule(
					rules.ActionTypeDeny,
					externalNameByNegatedExpression("trusted\\..*"),
				),
			),
			selectedRule(
				map[string]string{
					"negate": "true",
				},
				externalNameRule(
					rules.ActionTypeAllow,
					externalNameByExpression("trusted\\..*"),
				),
			),
		}
	}

	newTenant := func() *capsulev1beta2.Tenant {
		return &capsulev1beta2.Tenant{
			ObjectMeta: metav1.ObjectMeta{
				Name: "e2e-rule-services",
				Labels: map[string]string{
					"env": "e2e",
				},
			},
			Spec: capsulev1beta2.TenantSpec{
				Owners: rbac.OwnerListSpec{
					{
						CoreOwnerSpec: rbac.CoreOwnerSpec{
							UserSpec: rbac.UserSpec{
								Name: ownerName,
								Kind: "User",
							},
						},
					},
				},
				ServiceOptions: &capsuleapi.ServiceOptions{
					AllowedServices: &capsuleapi.AllowedServices{
						ExternalName: ptr.To(true),
						LoadBalancer: ptr.To(true),
						NodePort:     ptr.To(true),
					},
				},
				Rules: tenantRules,
			},
		}
	}

	type expectedServiceStatusRule struct {
		action              rules.ActionType
		types               []rules.ServiceType
		loadBalancerCIDRs   []string
		externalIPs         bool
		externalIPCIDRs     []string
		nodePortRanges      []rules.ServiceNodePortRange
		externalExpressions []string
		externalExact       [][]string
		externalNegated     []bool
	}

	expectNamespaceStatusRules := func(nsName string, want []expectedServiceStatusRule) {
		Eventually(func(g Gomega) {
			nsStatus := &capsulev1beta2.RuleStatus{}

			g.Expect(k8sClient.Get(
				context.Background(),
				client.ObjectKey{
					Name:      meta.NameForManagedRuleStatus(),
					Namespace: nsName,
				},
				nsStatus,
			)).To(Succeed())

			g.Expect(nsStatus.Status.Rules).To(HaveLen(len(want)))

			for i, expected := range want {
				got := nsStatus.Status.Rules[i]

				g.Expect(got).NotTo(BeNil())
				g.Expect(got.Enforce).NotTo(BeNil())
				g.Expect(got.Enforce.Action).To(Equal(expected.action))

				if len(expected.types) == 0 {
					g.Expect(got.Enforce.Services.Types).To(BeEmpty())
				} else {
					g.Expect(got.Enforce.Services.Types).To(Equal(expected.types))
				}

				if len(expected.loadBalancerCIDRs) == 0 {
					if got.Enforce.Services.LoadBalancers != nil {
						g.Expect(got.Enforce.Services.LoadBalancers.CIDRs).To(BeEmpty())
					}
				} else {
					g.Expect(got.Enforce.Services.LoadBalancers).NotTo(BeNil())
					g.Expect(got.Enforce.Services.LoadBalancers.CIDRs).To(Equal(expected.loadBalancerCIDRs))
				}

				if expected.externalIPs {
					g.Expect(got.Enforce.Services.ExternalIPs).NotTo(BeNil())
					g.Expect(got.Enforce.Services.ExternalIPs.CIDRs).To(Equal(expected.externalIPCIDRs))
				} else {
					g.Expect(got.Enforce.Services.ExternalIPs).To(BeNil())
				}

				if len(expected.nodePortRanges) == 0 {
					if got.Enforce.Services.NodePorts != nil {
						g.Expect(got.Enforce.Services.NodePorts.Ports).To(BeEmpty())
					}
				} else {
					g.Expect(got.Enforce.Services.NodePorts).NotTo(BeNil())
					g.Expect(got.Enforce.Services.NodePorts.Ports).To(Equal(expected.nodePortRanges))
				}

				wantHostnames := len(expected.externalExpressions)
				if len(expected.externalExact) > wantHostnames {
					wantHostnames = len(expected.externalExact)
				}

				if wantHostnames == 0 {
					if got.Enforce.Services.ExternalNames != nil {
						g.Expect(got.Enforce.Services.ExternalNames.Hostnames).To(BeEmpty())
					}

					continue
				}

				g.Expect(got.Enforce.Services.ExternalNames).NotTo(BeNil())
				g.Expect(got.Enforce.Services.ExternalNames.Hostnames).To(HaveLen(wantHostnames))

				for j := 0; j < wantHostnames; j++ {
					match := got.Enforce.Services.ExternalNames.Hostnames[j]

					if len(expected.externalExpressions) > j {
						g.Expect(match.Expression).To(Equal(expected.externalExpressions[j]))
					} else {
						g.Expect(match.Expression).To(BeEmpty())
					}

					if len(expected.externalExact) > j {
						g.Expect(match.Exact).To(Equal(expected.externalExact[j]))
					} else {
						g.Expect(match.Exact).To(BeEmpty())
					}

					if len(expected.externalNegated) > j {
						g.Expect(match.Negate).To(Equal(expected.externalNegated[j]))
					} else {
						g.Expect(match.Negate).To(BeFalse())
					}
				}
			}
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	}

	baseStatusRules := func() []expectedServiceStatusRule {
		return []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeClusterIP,
					rules.ServiceTypeNodePort,
					rules.ServiceTypeLoadBalancer,
					rules.ServiceTypeExternalName,
				},
			},
			{
				action: rules.ActionTypeAllow,
				loadBalancerCIDRs: []string{
					"10.0.0.2/32",
					"10.0.1.0/24",
				},
			},
			{
				action: rules.ActionTypeAllow,
				externalExpressions: []string{
					"",
					".*\\.example\\.com",
					"combined\\..*\\.example\\.com",
				},
				externalExact: [][]string{
					{
						"internal.git.com",
					},
					nil,
					{
						"combined.internal.git.com",
					},
				},
			},
			{
				action: rules.ActionTypeAllow,
				nodePortRanges: []rules.ServiceNodePortRange{
					{
						From: 30000,
						To:   30100,
					},
					{
						From: 30500,
						To:   30500,
					},
				},
			},
			{
				action: rules.ActionTypeDeny,
				nodePortRanges: []rules.ServiceNodePortRange{
					{
						From: 30090,
						To:   30090,
					},
				},
			},
			{
				action: rules.ActionTypeDeny,
				loadBalancerCIDRs: []string{
					"10.0.66.0/24",
				},
			},
			{
				action: rules.ActionTypeAudit,
				externalExpressions: []string{
					"audit\\..*",
				},
			},
		}
	}

	updateTenantRules := func(next []*rules.NamespaceRuleBodyTenant) {
		UpdateTenantEventually(tnt, func(current *capsulev1beta2.Tenant) {
			current.Spec.Rules = next
		})

		tnt.Spec.Rules = next
	}

	createNamespace := func(labels map[string]string) *corev1.Namespace {
		if labels == nil {
			labels = map[string]string{}
		}

		labels[meta.TenantLabel] = tnt.GetName()

		ns := NewNamespace("", labels)

		NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
		NamespaceIsPartOfTenant(tnt, ns).Should(Succeed())

		return ns
	}

	createServiceAndExpectDenied := func(cs kubernetes.Interface, nsName string, svc *corev1.Service, substrings ...string) {
		base := svc.DeepCopy()
		baseName := base.Name
		if baseName == "" {
			baseName = "svc"
		}

		Eventually(func() error {
			candidate := base.DeepCopy()
			candidate.Name = fmt.Sprintf("%s-%d", baseName, time.Now().UnixNano()%1e6)

			_, err := cs.CoreV1().Services(nsName).Create(context.Background(), candidate, metav1.CreateOptions{})
			if err == nil {
				_ = cs.CoreV1().Services(nsName).Delete(context.Background(), candidate.Name, metav1.DeleteOptions{})

				return fmt.Errorf("expected service create to be denied, but it succeeded")
			}

			if apierrors.IsAlreadyExists(err) {
				return fmt.Errorf("unexpected AlreadyExists: %v", err)
			}

			msg := err.Error()
			for _, substring := range substrings {
				if !strings.Contains(msg, substring) {
					return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
				}
			}

			return nil
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	}

	createServiceAndExpectAllowed := func(cs kubernetes.Interface, nsName string, svc *corev1.Service) {
		EventuallyCreation(func() error {
			_, err := cs.CoreV1().Services(nsName).Create(context.Background(), svc, metav1.CreateOptions{})

			return err
		}).Should(Succeed())
	}

	updateServiceAndExpectDenied := func(
		cs kubernetes.Interface,
		nsName string,
		svcName string,
		mutate func(*corev1.Service),
		substrings ...string,
	) {
		Eventually(func() error {
			svc, err := cs.CoreV1().Services(nsName).Get(context.Background(), svcName, metav1.GetOptions{})
			if err != nil {
				return err
			}

			mutate(svc)

			_, err = cs.CoreV1().Services(nsName).Update(context.Background(), svc, metav1.UpdateOptions{})
			if err == nil {
				return fmt.Errorf("expected service update to be denied, but it succeeded")
			}

			msg := err.Error()
			for _, substring := range substrings {
				if !strings.Contains(msg, substring) {
					return fmt.Errorf("expected error to contain %q, got: %s", substring, msg)
				}
			}

			return nil
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	}

	expectAuditEvent := func(
		cs kubernetes.Interface,
		namespace string,
		serviceName string,
		substrings ...string,
	) {
		Eventually(func() error {
			evt, err := cs.CoreV1().Events(namespace).List(context.Background(), metav1.ListOptions{})
			if err != nil {
				return err
			}

			for _, e := range evt.Items {
				if e.Reason != events.ReasonNamespaceRuleAudit {
					continue
				}

				if e.InvolvedObject.Kind != "Service" {
					continue
				}

				eventServiceName := e.InvolvedObject.Name
				if eventServiceName != serviceName && !strings.HasPrefix(eventServiceName, serviceName+"-") {
					continue
				}

				message := e.Message

				matched := true
				for _, substring := range substrings {
					if !strings.Contains(message, substring) {
						matched = false

						break
					}
				}

				if matched {
					return nil
				}
			}

			return fmt.Errorf(
				"expected audit event for service %q containing %q",
				serviceName,
				substrings,
			)
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	}

	servicePort := func() corev1.ServicePort {
		return corev1.ServicePort{
			Name:       "http",
			Protocol:   corev1.ProtocolTCP,
			Port:       8080,
			TargetPort: intstr.FromInt(8080),
		}
	}

	clusterIPService := func(name string) *corev1.Service {
		return &corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
			Spec: corev1.ServiceSpec{
				Type: corev1.ServiceTypeClusterIP,
				Ports: []corev1.ServicePort{
					servicePort(),
				},
			},
		}
	}

	externalIPService := func(name string, externalIPs ...string) *corev1.Service {
		svc := clusterIPService(name)
		svc.Spec.ExternalIPs = externalIPs

		return svc
	}

	nodePortService := func(name string, nodePort int32) *corev1.Service {
		port := servicePort()
		port.NodePort = nodePort

		return &corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
			Spec: corev1.ServiceSpec{
				Type: corev1.ServiceTypeNodePort,
				Ports: []corev1.ServicePort{
					port,
				},
			},
		}
	}

	nodePortServiceWithoutExplicitNodePort := func(name string) *corev1.Service {
		return &corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
			Spec: corev1.ServiceSpec{
				Type: corev1.ServiceTypeNodePort,
				Ports: []corev1.ServicePort{
					servicePort(),
				},
			},
		}
	}

	createServiceAndExpectDeniedOneOf := func(
		cs kubernetes.Interface,
		nsName string,
		svc *corev1.Service,
		alternatives ...[]string,
	) {
		base := svc.DeepCopy()
		baseName := base.Name
		if baseName == "" {
			baseName = "svc"
		}

		Eventually(func() error {
			candidate := base.DeepCopy()
			candidate.Name = fmt.Sprintf("%s-%d", baseName, time.Now().UnixNano()%1e6)

			_, err := cs.CoreV1().Services(nsName).Create(context.Background(), candidate, metav1.CreateOptions{})
			if err == nil {
				_ = cs.CoreV1().Services(nsName).Delete(context.Background(), candidate.Name, metav1.DeleteOptions{})

				return fmt.Errorf("expected service create to be denied, but it succeeded")
			}

			if apierrors.IsAlreadyExists(err) {
				return fmt.Errorf("unexpected AlreadyExists: %v", err)
			}

			msg := err.Error()

			for _, alternative := range alternatives {
				matches := true

				for _, substring := range alternative {
					if !strings.Contains(msg, substring) {
						matches = false

						break
					}
				}

				if matches {
					return nil
				}
			}

			return fmt.Errorf(
				"expected error to match one of %v, got: %s",
				alternatives,
				msg,
			)
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	}

	loadBalancerService := func(
		name string,
		loadBalancerIP string,
		sourceRanges []string,
		allocateLoadBalancerNodePorts *bool,
	) *corev1.Service {
		return &corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
			Spec: corev1.ServiceSpec{
				Type:                          corev1.ServiceTypeLoadBalancer,
				LoadBalancerIP:                loadBalancerIP,
				LoadBalancerSourceRanges:      sourceRanges,
				AllocateLoadBalancerNodePorts: allocateLoadBalancerNodePorts,
				Ports: []corev1.ServicePort{
					servicePort(),
				},
			},
		}
	}

	externalNameService := func(name string, externalName string) *corev1.Service {
		return &corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
			Spec: corev1.ServiceSpec{
				Type:         corev1.ServiceTypeExternalName,
				ExternalName: externalName,
				Ports: []corev1.ServicePort{
					servicePort(),
				},
			},
		}
	}

	BeforeEach(func() {
		tenantRules = baseTenantRules()
	})

	JustBeforeEach(func() {
		tnt = newTenant()

		EventuallyCreation(func() error {
			tnt.ResourceVersion = ""

			return k8sClient.Create(context.TODO(), tnt)
		}).Should(Succeed())

		TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)
	})

	JustAfterEach(func() {
		EventuallyDeletion(tnt)
	})

	It("requires or validates LoadBalancer nodePorts when allocation is enabled and nodePort ranges are configured", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDeniedOneOf(
			cs,
			ns.Name,
			loadBalancerService("lb-auto-node-port-denied", "10.0.0.2", nil, nil),
			[]string{
				"requires explicit spec.ports[*].nodePort",
				"nodePort ranges are enforced by namespace rule",
			},
			[]string{
				"nodePort",
				"not allowed by namespace rule",
				"Allowed ranges",
				"30000-30100",
				"30500",
			},
		)
	})

	It("stores matching tenant service rules as independent status rule blocks", func() {
		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.GetName(), baseStatusRules())
	})

	It("stores namespace-selector matched service rules as additional independent status rule blocks", func() {
		ns := createNamespace(map[string]string{
			"environment": "prod",
		})

		want := baseStatusRules()
		want = append(want, expectedServiceStatusRule{
			action: rules.ActionTypeAllow,
			loadBalancerCIDRs: []string{
				"10.0.171.0/24",
			},
		})

		expectNamespaceStatusRules(ns.GetName(), want)
	})

	It("stores namespace-selector matched negated service rules as independent status rule blocks", func() {
		ns := createNamespace(map[string]string{
			"negate": "true",
		})

		want := baseStatusRules()
		want = append(want,
			expectedServiceStatusRule{
				action: rules.ActionTypeDeny,
				externalExpressions: []string{
					"trusted\\..*",
				},
				externalNegated: []bool{
					true,
				},
			},
			expectedServiceStatusRule{
				action: rules.ActionTypeAllow,
				externalExpressions: []string{
					"trusted\\..*",
				},
			},
		)

		expectNamespaceStatusRules(ns.GetName(), want)
	})

	It("allows a listed ClusterIP service type", func() {
		updateTenantRules([]*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeClusterIP,
			),
		})

		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeClusterIP,
				},
			},
		})

		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(cs, ns.Name, clusterIPService("cluster-ip-allowed"))
	})

	It("denies a service type missing from services.types and reports allowed service types", func() {
		updateTenantRules([]*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeClusterIP,
			),
		})

		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeClusterIP,
				},
			},
		})

		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, externalNameService("external-name-type-denied", "internal.git.com"),
			"service type",
			"ExternalName",
			"not allowed",
			"Allowed service types",
			"ClusterIP",
		)
	})

	It("allows only Service external IPs contained in configured CIDRs", func() {
		updateTenantRules([]*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeClusterIP,
			),
			externalIPCIDRRule(
				rules.ActionTypeAllow,
				"10.20.0.0/16",
				"192.168.1.2",
			),
		})

		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeClusterIP,
				},
			},
			{
				action:      rules.ActionTypeAllow,
				externalIPs: true,
				externalIPCIDRs: []string{
					"10.20.0.0/16",
					"192.168.1.2",
				},
			},
		})

		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(cs, ns.Name, clusterIPService("external-ip-omitted-allowed"))

		allowed := externalIPService("external-ip-cidr-allowed", "10.20.1.44")
		createServiceAndExpectAllowed(cs, ns.Name, allowed)
		createServiceAndExpectAllowed(cs, ns.Name, externalIPService("external-ip-host-allowed", "192.168.1.2"))

		createServiceAndExpectDenied(cs, ns.Name, externalIPService("external-ip-denied", "8.8.8.8"),
			"external IP",
			"8.8.8.8",
			"spec.externalIPs[0]",
			"not allowed",
			"Allowed CIDRs",
			"10.20.0.0/16",
			"192.168.1.2",
		)

		updateServiceAndExpectDenied(
			cs,
			ns.Name,
			allowed.Name,
			func(svc *corev1.Service) {
				svc.Spec.ExternalIPs = []string{"8.8.4.4"}
			},
			"external IP",
			"8.8.4.4",
			"spec.externalIPs[0]",
			"not allowed",
		)
	})

	It("denies every specified Service external IP when a deny rule has empty CIDRs", func() {
		updateTenantRules([]*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeClusterIP,
			),
			externalIPCIDRRule(rules.ActionTypeDeny),
		})

		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeClusterIP,
				},
			},
			{
				action:      rules.ActionTypeDeny,
				externalIPs: true,
			},
		})

		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(cs, ns.Name, clusterIPService("external-ip-empty-deny-omitted"))
		createServiceAndExpectDenied(cs, ns.Name, externalIPService("external-ip-empty-denied", "10.20.1.44"),
			"external IP",
			"10.20.1.44",
			"denied",
			"all external IPs",
		)
	})

	It("allows exact, regex, and combined ExternalName hostname matchers", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(cs, ns.Name, externalNameService("external-exact-allowed", "internal.git.com"))
		createServiceAndExpectAllowed(cs, ns.Name, externalNameService("external-regex-allowed", "api.example.com"))
		createServiceAndExpectAllowed(cs, ns.Name, externalNameService("external-combined-exact-allowed", "combined.internal.git.com"))
		createServiceAndExpectAllowed(cs, ns.Name, externalNameService("external-combined-regex-allowed", "combined.api.example.com"))
	})

	It("denies non-matching ExternalName hostnames and reports allowed hostname rules", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, externalNameService("external-denied", "api.bad.com"),
			"externalName hostname",
			"api.bad.com",
			"spec.externalName",
			"not allowed",
			"Allowed hostnames",
			"exact: internal.git.com",
			"exp: .*\\.example\\.com",
		)
	})

	It("audits a matching ExternalName but still denies when no allow rule matches", func() {
		updateTenantRules([]*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeExternalName,
			),
			externalNameRule(
				rules.ActionTypeAudit,
				externalNameByExpression("audit\\..*"),
			),
			externalNameRule(
				rules.ActionTypeAllow,
				externalNameByExpression("allowed\\..*"),
			),
		})

		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeExternalName,
				},
			},
			{
				action: rules.ActionTypeAudit,
				externalExpressions: []string{
					"audit\\..*",
				},
			},
			{
				action: rules.ActionTypeAllow,
				externalExpressions: []string{
					"allowed\\..*",
				},
			},
		})

		svc := externalNameService("external-audit-denied", "audit.internal")

		createServiceAndExpectDenied(clusterAdminClient(), ns.Name, svc,
			"externalName hostname",
			"audit.internal",
			"not allowed",
			"Allowed hostnames",
			"allowed\\..*",
		)

		expectAuditEvent(clusterAdminClient(), ns.Name, svc.Name,
			"matched audit",
			"audit\\..*",
		)
	})

	It("denies a later selected ExternalName deny rule after an earlier allow rule matched", func() {
		ns := createNamespace(map[string]string{
			"external-policy": "restricted",
		})
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, externalNameService("external-selected-denied", "blocked.example.com"),
			"externalName hostname",
			"blocked.example.com",
			"denied",
			"exact: blocked.example.com",
		)
	})

	It("applies namespace-selector matched negated ExternalName rules after base rules", func() {
		ns := createNamespace(map[string]string{
			"negate": "true",
		})
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, externalNameService("external-negated-denied", "api.example.com"),
			"externalName hostname",
			"api.example.com",
			"denied",
			"exp: trusted\\..*",
		)

		createServiceAndExpectAllowed(cs, ns.Name, externalNameService("external-negated-allowed", "trusted.api"))
	})

	It("allows LoadBalancer IPs and source ranges contained in configured CIDRs", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(cs, ns.Name, loadBalancerService("lb-ip-allowed", "10.0.0.2", nil, ptr.To(false)))
		createServiceAndExpectAllowed(cs, ns.Name, loadBalancerService("lb-ip-range-allowed", "10.0.1.44", nil, ptr.To(false)))
		createServiceAndExpectAllowed(cs, ns.Name, loadBalancerService("lb-source-range-allowed", "", []string{"10.0.1.0/25"}, ptr.To(false)))
	})

	It("denies LoadBalancer IPs outside configured CIDRs and reports allowed CIDRs", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, loadBalancerService("lb-ip-denied", "10.0.171.239", nil, ptr.To(false)),
			"loadBalancer CIDR",
			"10.0.171.239",
			"spec.loadBalancerIP",
			"not allowed",
			"Allowed CIDRs",
			"10.0.0.2/32",
			"10.0.1.0/24",
		)
	})

	It("denies LoadBalancer source ranges not fully contained in configured CIDRs", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, loadBalancerService("lb-source-range-denied", "", []string{"10.0.0.0/23"}, ptr.To(false)),
			"loadBalancer CIDR",
			"10.0.0.0/23",
			"spec.loadBalancerSourceRanges[0]",
			"Allowed CIDRs",
		)
	})

	It("requires LoadBalancer IP or source ranges when CIDR constraints are configured", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, loadBalancerService("lb-required-value-denied", "", nil, ptr.To(false)),
			"requires spec.loadBalancerIP or spec.loadBalancerSourceRanges",
			"loadBalancer CIDR constraints are enforced by namespace rule",
		)
	})

	It("denies a later LoadBalancer CIDR deny rule after an earlier allow rule matched", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, loadBalancerService("lb-later-deny", "10.0.66.10", nil, ptr.To(false)),
			"loadBalancer CIDR",
			"10.0.66.10",
			"denied",
			"10.0.66.0/24",
		)
	})

	It("allows a later selected LoadBalancer allow rule to override an earlier allow miss", func() {
		ns := createNamespace(map[string]string{
			"environment": "prod",
		})
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(cs, ns.Name, loadBalancerService("lb-prod-selected-allowed", "10.0.171.239", nil, ptr.To(false)))
	})

	It("allows explicit nodePorts inside configured ranges", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(cs, ns.Name, nodePortService("node-port-range-allowed", 30080))
		createServiceAndExpectAllowed(cs, ns.Name, nodePortService("node-port-single-allowed", 30500))
	})

	It("denies explicit nodePorts outside configured ranges and reports allowed ranges", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(cs, ns.Name, nodePortService("node-port-range-denied", 32080),
			"nodePort",
			"32080",
			"not allowed",
			"Allowed ranges",
			"30000-30100",
			"30500",
		)
	})

	It("requires explicit nodePorts when nodePort ranges are configured", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDeniedOneOf(
			cs,
			ns.Name,
			nodePortServiceWithoutExplicitNodePort("node-port-required-denied"),
			[]string{
				"requires explicit spec.ports[*].nodePort",
				"nodePort ranges are enforced by namespace rule",
			},
			[]string{
				"nodePort",
				"not allowed by namespace rule",
				"Allowed ranges",
				"30000-30100",
				"30500",
			},
		)
	})
	It("denies a later nodePort deny rule after an earlier allow rule matched", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDenied(
			cs,
			ns.Name,
			nodePortService("node-port-later-deny", 30090),
			"nodePort",
			"30090",
			"denied",
			"30090",
		)
	})

	It("requires or validates LoadBalancer nodePorts when allocation is enabled and nodePort ranges are configured", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectDeniedOneOf(
			cs,
			ns.Name,
			loadBalancerService("lb-auto-node-port-denied", "10.0.0.2", nil, nil),
			[]string{
				"requires explicit spec.ports[*].nodePort",
				"nodePort ranges are enforced by namespace rule",
			},
			[]string{
				"nodePort",
				"not allowed by namespace rule",
				"Allowed ranges",
				"30000-30100",
				"30500",
			},
		)
	})

	It("does not require LoadBalancer nodePorts when allocation is explicitly disabled", func() {
		ns := createNamespace(nil)
		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(
			cs,
			ns.Name,
			loadBalancerService("lb-node-port-allocation-disabled", "10.0.0.2", nil, ptr.To(false)),
		)
	})

	It("denies an update when the new ExternalName no longer matches the allowed hostname rules", func() {
		updateTenantRules([]*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeExternalName,
			),
			externalNameRule(
				rules.ActionTypeAllow,
				externalNameByExact("internal.git.com"),
			),
		})

		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeExternalName,
				},
			},
			{
				action: rules.ActionTypeAllow,
				externalExact: [][]string{
					{
						"internal.git.com",
					},
				},
			},
		})

		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		svc := externalNameService("external-update-denied", "internal.git.com")
		createServiceAndExpectAllowed(cs, ns.Name, svc)

		updateServiceAndExpectDenied(
			cs,
			ns.Name,
			svc.Name,
			func(svc *corev1.Service) {
				svc.Spec.ExternalName = "api.bad.com"
			},
			"externalName hostname",
			"api.bad.com",
			"not allowed",
			"Allowed hostnames",
		)
	})

	It("allows service creation when no matching service-specific constraint exists for the allowed type", func() {
		updateTenantRules([]*rules.NamespaceRuleBodyTenant{
			serviceTypeRule(
				rules.ActionTypeAllow,
				rules.ServiceTypeClusterIP,
				rules.ServiceTypeLoadBalancer,
			),
		})

		ns := createNamespace(nil)

		expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
			{
				action: rules.ActionTypeAllow,
				types: []rules.ServiceType{
					rules.ServiceTypeClusterIP,
					rules.ServiceTypeLoadBalancer,
				},
			},
		})

		cs := ownerClient(tnt.Spec.Owners[0].UserSpec)

		createServiceAndExpectAllowed(
			cs,
			ns.Name,
			loadBalancerService("lb-no-cidr-rule-allowed", "", nil, ptr.To(false)),
		)
	})
})
