// Copyright Istio Authors
//
// 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 xds_test

import (
	"fmt"
	"strings"
	"testing"
	"time"

	udpa "github.com/cncf/xds/go/udpa/type/v1"
	clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	upstream "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"istio.io/api/label"
	meshconfig "istio.io/api/mesh/v1alpha1"
	networking "istio.io/api/networking/v1alpha3"
	networkingclient "istio.io/client-go/pkg/apis/networking/v1"
	"istio.io/istio/pilot/pkg/features"
	"istio.io/istio/pilot/pkg/model"
	v3 "istio.io/istio/pilot/pkg/xds/v3"
	"istio.io/istio/pilot/test/xds"
	"istio.io/istio/pilot/test/xdstest"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/mesh"
	"istio.io/istio/pkg/kube/kclient/clienttest"
	"istio.io/istio/pkg/slices"
	"istio.io/istio/pkg/test"
	"istio.io/istio/pkg/test/util/assert"
	"istio.io/istio/pkg/test/util/retry"
	"istio.io/istio/pkg/util/sets"
	"istio.io/istio/pkg/wellknown"
)

var (
	waypointSvc = `apiVersion: v1
kind: Service
metadata:
  labels:
    gateway.istio.io/managed: istio.io-mesh-controller
    gateway.networking.k8s.io/gateway-name: waypoint
    istio.io/gateway-name: waypoint
  name: waypoint
  namespace: default
spec:
  clusterIP: 3.0.0.0
  ports:
  - appProtocol: hbone
    name: mesh
    port: 15008
  selector:
    gateway.networking.k8s.io/gateway-name: waypoint
`
	waypointInstance = `apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: waypoint-a
  namespace: default
spec:
  address: 3.0.0.1
  labels:
    gateway.networking.k8s.io/gateway-name: waypoint
`
	waypointGateway = `apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: waypoint
  namespace: default
spec:
  gatewayClassName: waypoint
  listeners:
    - name: mesh
      port: 15008
      protocol: HBONE
status:
  addresses:
  - type: Hostname
    value: waypoint.default.svc.cluster.local
`
	appServiceEntry = `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: app
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
    istio.io/ingress-use-waypoint: "true"
spec:
  hosts: [app.com]
  addresses: [1.2.3.4]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  workloadSelector:
    labels:
      app: app
`
	appWorkloadEntry = `apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: app-a
  namespace: default
  labels:
    app: app
  annotations:
    ambient.istio.io/redirection: enabled
spec:
  address: 1.1.1.1
`
	appPod = `apiVersion: v1
kind: Pod
metadata:
  name: app-b
  namespace: default
  labels:
    app: app
  annotations:
    ambient.istio.io/redirection: enabled
spec: {}
status:
  conditions:
  - status: "True"
    type: Ready
  podIP: 1.1.1.2
  podIPs:
  - ip: 1.1.1.2
  - ip: 2001:20::2
`
	appPodNoMesh = `apiVersion: v1
kind: Pod
metadata:
  name: app-c
  namespace: default
  labels:
    app: app
spec: {}
status:
  conditions:
  - status: "True"
    type: Ready
  podIP: 1.1.1.3
  podIPs:
  - ip: 1.1.1.3
  - ip: 2001:20::3
`
)

func TestWaypointNetworkFilters(t *testing.T) {
	// Define two SE with various protocol declarations, we will check how sniffing and TLS inspection are enabled.

	appServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: app
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
spec:
  hosts: [app.com]
  ports:
  - number: 80
    name: http # HTTP, but will have another TCP port overlapping in another service
    protocol: HTTP
  - number: 81
    name: http-only # HTTP
    protocol: HTTP
  - number: 90 # Sniffed, on its own
    name: auto
    protocol: ""
  - number: 443
    name: https
    protocol: TLS
`
	app2ServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: app2
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
spec:
  hosts: [app2.com]
  ports:
  - number: 80
    name: tcp # conflicts with other HTTP port
    protocol: TCP
  - number: 91
    name: tcp-only # TCP
    protocol: HTTP
  - number: 443
    name: https
    protocol: TCP
`

	d, proxy := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		waypointInstance,
		appServiceEntry, app2ServiceEntry)

	l := xdstest.ExtractListener("main_internal", d.Listeners(proxy))
	filters := xdstest.ExtractListenerFilters(l)

	httpInspectorDisabled := filters[wellknown.HTTPInspector].GetFilterDisabled()
	hasSniffing := func(port int, expect bool) {
		t.Helper()
		assert.Equal(t, xdstest.EvaluateListenerFilterPredicates(httpInspectorDisabled, port), !expect)
	}
	hasSniffing(80, true)   // HTTP and TCP on same port
	hasSniffing(81, false)  // HTTP
	hasSniffing(91, false)  // TCP
	hasSniffing(90, true)   // Unspecified
	hasSniffing(443, false) // TLS and TCP on the same port - HTTP inspector not needed

	tlsInspectorDisabled := filters[wellknown.TLSInspector].GetFilterDisabled()
	hasTLSInspector := func(port int, expect bool) {
		t.Helper()
		assert.Equal(t, xdstest.EvaluateListenerFilterPredicates(tlsInspectorDisabled, port), !expect)
	}
	hasTLSInspector(80, false)
	hasTLSInspector(81, false)
	hasTLSInspector(91, false)
	hasTLSInspector(90, false)
	hasTLSInspector(443, true)
}

func TestWaypointTLSInspectorWithOnlyTLSPorts(t *testing.T) {
	// Test that tls_inspector is added when only TLS ports exist.
	// This reproduces a bug where tls_inspector was only added when there
	// were non-TLS ports to exclude from inspection.

	tlsServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: tls-only
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
spec:
  hosts: ["secure.example.com"]
  addresses: ["5.5.5.5"]
  ports:
  - number: 443
    name: tls
    protocol: TLS
  resolution: STATIC`

	d, proxy := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		waypointInstance,
		tlsServiceEntry)

	l := xdstest.ExtractListener("main_internal", d.Listeners(proxy))
	filters := xdstest.ExtractListenerFilters(l)

	// tls_inspector must be present even with only TLS ports
	tlsInspector := filters[wellknown.TLSInspector]
	assert.Equal(t, tlsInspector != nil, true)
}

func TestWaypointEndpoints(t *testing.T) {
	d, proxy := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		appPod, appPodNoMesh,
		waypointInstance, appWorkloadEntry,
		appServiceEntry)

	eps := slices.Sort(xdstest.ExtractEndpoints(d.Endpoints(proxy)[1]))
	assert.Equal(t, eps, []string{
		// No tunnel, should get dual IPs
		"1.1.1.3:80,[2001:20::3]:80",
		"connect_originate;1.1.1.1:80",
		// Tunnel doesn't support multiple IPs
		"connect_originate;1.1.1.2:80",
	})
}

func TestWaypointTelemetry(t *testing.T) {
	telemetry := `apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
  name: logs
  namespace: default
spec:
  targetRefs:
  - kind: ServiceEntry
    name: app
    group: networking.istio.io
  accessLogging:
  - providers:
    - name: envoy
  metrics:
  - providers:
    - name: prometheus
  tracing:
  - providers:
    - name: otel
`
	notSelectedAppServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: not-app
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
spec:
  hosts: [not-app.com]
  addresses: [2.3.4.5]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  workloadSelector:
    labels:
      app: app`
	d, proxy := setupWaypointTest(t,
		waypointGateway, waypointSvc, waypointInstance,
		appServiceEntry, notSelectedAppServiceEntry,
		telemetry)

	l := xdstest.ExtractListener("main_internal", d.Listeners(proxy))
	app := xdstest.ExtractHTTPConnectionManager(t,
		xdstest.ExtractFilterChain(model.BuildSubsetKey(model.TrafficDirectionInboundVIP, "http", "app.com", 80), l))
	notApp := xdstest.ExtractHTTPConnectionManager(t,
		xdstest.ExtractFilterChain(model.BuildSubsetKey(model.TrafficDirectionInboundVIP, "http", "not-app.com", 80), l))

	// The selected service should get all 3 types
	assert.Equal(t, app.AccessLog != nil, true)
	assert.Equal(t, sets.New(slices.Map(app.HttpFilters, (*hcm.HttpFilter).GetName)...).Contains("istio.stats"), true)
	assert.Equal(t, app.Tracing.Provider != nil, true)

	// Unselected service should get none
	assert.Equal(t, notApp.AccessLog == nil, true)
	assert.Equal(t, sets.New(slices.Map(notApp.HttpFilters, (*hcm.HttpFilter).GetName)...).Contains("istio.stats"), false)
	assert.Equal(t, notApp.Tracing.Provider == nil, true)
}

func TestWaypointRequestAuth(t *testing.T) {
	// jwks mostly from https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.1
	requestAuthn := `apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: jwt-on-waypoint
  namespace: default
spec:
  targetRefs:
  - group: networking.istio.io
    kind: ServiceEntry
    name: app
  jwtRules:
  - issuer: "example.ietf.org"
    jwks: |
          {"keys":
            [
              {"kty":"EC",
              "crv":"P-256",
              "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
              "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
              "use":"enc",
              "kid":"1"}
            ]
          }`
	notSelectedAppServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: not-app
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
spec:
  hosts: [not-app.com]
  addresses: [2.3.4.5]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  workloadSelector:
    labels:
      app: app`
	d, proxy := setupWaypointTest(t,
		waypointGateway, waypointSvc, waypointInstance,
		appServiceEntry, notSelectedAppServiceEntry, requestAuthn)

	l := xdstest.ExtractListener("main_internal", d.Listeners(proxy))

	app := xdstest.ExtractHTTPConnectionManager(t,
		xdstest.ExtractFilterChain(model.BuildSubsetKey(model.TrafficDirectionInboundVIP, "http", "app.com", 80), l))
	assert.Equal(t, sets.New(slices.Map(app.HttpFilters, (*hcm.HttpFilter).GetName)...).Contains("envoy.filters.http.jwt_authn"), true)

	// assert not-app.com has not gotten config from the req authn
	notApp := xdstest.ExtractHTTPConnectionManager(t,
		xdstest.ExtractFilterChain(model.BuildSubsetKey(model.TrafficDirectionInboundVIP, "http", "not-app.com", 80), l))
	assert.Equal(t, sets.New(slices.Map(notApp.HttpFilters, (*hcm.HttpFilter).GetName)...).Contains("envoy.filters.http.jwt_authn"), false)
}

func TestCrossNamespaceWaypointRequestAuth(t *testing.T) {
	// jwks mostly from https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.1
	requestAuthn := `apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: jwt-on-waypoint
  namespace: app
spec:
  targetRefs:
  - group: networking.istio.io
    kind: ServiceEntry
    name: app
  jwtRules:
  - issuer: "example.ietf.org"
    jwks: |
          {"keys":
            [
              {"kty":"EC",
              "crv":"P-256",
              "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
              "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
              "use":"enc",
              "kid":"1"}
            ]
          }`
	waypointSvc := `apiVersion: v1
kind: Service
metadata:
  labels:
    gateway.istio.io/managed: istio.io-mesh-controller
    gateway.networking.k8s.io/gateway-name: waypoint
    istio.io/gateway-name: waypoint
  name: waypoint
  namespace: default
spec:
  clusterIP: 3.0.0.0
  ports:
  - appProtocol: hbone
    name: mesh
    port: 15008
  selector:
    gateway.networking.k8s.io/gateway-name: waypoint`

	waypointInstance := `apiVersion: networking.istio.io/v1
kind: WorkloadEntry
metadata:
  name: waypoint-a
  namespace: default
spec:
  address: 3.0.0.1
  labels:
    gateway.networking.k8s.io/gateway-name: waypoint`

	waypointGateway := `apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: waypoint
  namespace: default
spec:
  gatewayClassName: waypoint
  listeners:
    - name: mesh
      port: 15008
      protocol: HBONE
      allowedRoutes:
        namespaces:
          from: All
status:
  addresses:
  - type: Hostname
    value: waypoint.default.svc.cluster.local`

	appServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: app
  namespace: app
  labels:
    istio.io/use-waypoint: waypoint
    istio.io/use-waypoint-namespace: default
spec:
  hosts: [app.com]
  addresses: [1.2.3.4]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  workloadSelector:
    labels:
      app: app`
	notSelectedAppServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: not-app
  namespace: app
  labels:
    istio.io/use-waypoint: waypoint
    istio.io/use-waypoint-namespace: default
spec:
  hosts: [not-app.com]
  addresses: [2.3.4.5]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  workloadSelector:
    labels:
      app: app`

	d, proxy := setupWaypointTest(t,
		waypointGateway, waypointSvc, waypointInstance,
		appServiceEntry, notSelectedAppServiceEntry, requestAuthn)

	var app, notApp *hcm.HttpConnectionManager
	retry.UntilSuccessOrFail(t, func() error {
		l := xdstest.ExtractListener("main_internal", d.Listeners(proxy))
		if l == nil {
			return fmt.Errorf("listener not found")
		}

		appFc := xdstest.ExtractFilterChain(model.BuildSubsetKey(model.TrafficDirectionInboundVIP, "http", "app.com", 80), l)
		if appFc == nil {
			return fmt.Errorf("app filter chain not found")
		}
		app = xdstest.ExtractHTTPConnectionManager(t, appFc)
		if app == nil {
			return fmt.Errorf("app hcm not found")
		}

		notAppFc := xdstest.ExtractFilterChain(model.BuildSubsetKey(model.TrafficDirectionInboundVIP, "http", "not-app.com", 80), l)
		if notAppFc == nil {
			return fmt.Errorf("notApp filter chain not found")
		}
		notApp = xdstest.ExtractHTTPConnectionManager(t, notAppFc)
		if notApp == nil {
			return fmt.Errorf("notApp hcm not found")
		}
		return nil
	}, retry.Timeout(time.Second*10))

	assert.Equal(t, sets.New(slices.Map(app.HttpFilters, (*hcm.HttpFilter).GetName)...).Contains("envoy.filters.http.jwt_authn"), true)

	// assert not-app.com has not gotten config from the req authn
	assert.Equal(t, sets.New(slices.Map(notApp.HttpFilters, (*hcm.HttpFilter).GetName)...).Contains("envoy.filters.http.jwt_authn"), false)
}

func TestIngressUseWaypoint(t *testing.T) {
	d, _ := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		appPod, appPodNoMesh,
		waypointInstance, appWorkloadEntry,
		appServiceEntry)
	ingressProxy := d.SetupProxy(&model.Proxy{
		Type:            model.Router,
		ConfigNamespace: "default",
		IPAddresses:     []string{"3.0.0.3"}, // Arbitrary
	})

	eps := slices.Sort(xdstest.ExtractLoadAssignments(d.Endpoints(ingressProxy))["outbound|80||app.com"])
	assert.Equal(t, eps, []string{
		// 1.2.3.4: Service VIP
		// 3.0.0.1: Waypoint Pod IP
		"connect_originate;1.2.3.4:80;3.0.0.1:15008",
	})
}

func TestWaypointClusterWithDynamicDNS(t *testing.T) {
	g := NewWithT(t)
	dynamicDNSServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: wildcard-service-entry
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
    istio.io/use-waypoint-namespace: default
spec:
  hosts: ["*.domain.com"]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DYNAMIC_DNS`
	d, p := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		waypointInstance,
		dynamicDNSServiceEntry)

	clusters := xdstest.ExtractClusters(d.Clusters(p))
	cluster := clusters["inbound-vip|80|http|*.domain.com"]
	g.Expect(cluster).NotTo(BeNil())
	g.Expect(cluster.TransportSocket.GetTypedConfig().TypeUrl).
		To(Equal("type.googleapis.com/envoy.extensions.transport_sockets.internal_upstream.v3.InternalUpstreamTransport"))
	clusterType := cluster.ClusterDiscoveryType.(*clusterv3.Cluster_ClusterType)
	g.Expect(clusterType).NotTo(BeNil())
	g.Expect(clusterType.ClusterType.GetTypedConfig().TypeUrl).
		To(Equal("type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig"))
	po := cluster.TypedExtensionProtocolOptions["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"]
	g.Expect(po).To(BeNil())
}

func TestWaypointClusterDNSConnectStrategyHappyEyeballs(t *testing.T) {
	g := NewWithT(t)
	DNSConnectStrategyServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: race-target
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
    istio.io/use-waypoint-namespace: default
  annotations:
    istio.io/connect-strategy: RACE_FIRST_TCP_CONNECT
spec:
  hosts: ["sql.example.com"]
  ports:
  - number: 1433
    name: tcp-sql
    protocol: TCP
  resolution: DNS`
	d, p := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		waypointInstance,
		DNSConnectStrategyServiceEntry)

	clusters := xdstest.ExtractClusters(d.Clusters(p))
	c := clusters["inbound-vip|1433|tcp|sql.example.com"]
	g.Expect(c).NotTo(BeNil(), "expected inbound-vip cluster for sql.example.com")
	g.Expect(c.DnsLookupFamily).To(Equal(clusterv3.Cluster_ALL),
		"connect strategy RACE_FIRST_TCP_CONNECT should set DnsLookupFamily=ALL for happy eyeballs")
	g.Expect(c.GetType()).To(Equal(clusterv3.Cluster_LOGICAL_DNS), "connect strategy RACE_FIRST_TCP_CONNECT should be a LOGICAL_DNS cluster")
}

func TestWaypointClusterWithDynamicDNSWithoutWaypoint(t *testing.T) {
	g := NewWithT(t)
	dynamicDNSServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: wildcard-service-entry
  namespace: default
spec:
  hosts: ["*.domain.com"]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DYNAMIC_DNS`
	d, p := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		waypointInstance,
		dynamicDNSServiceEntry)

	clusters := xdstest.ExtractClusters(d.Clusters(p))
	var clusterNames []string
	for _, c := range clusters {
		clusterNames = append(clusterNames, c.Name)
	}
	g.Expect(len(clusterNames)).To(Equal(4))
	g.Expect(clusterNames).To(ContainElements([]string{
		"connect_originate",
		"outbound|15008||waypoint.default.svc.cluster.local",
		"main_internal",
		"encap",
	}))
}

func TestWaypointClusterWithDynamicDNSAndTLSOrigination(t *testing.T) {
	g := NewWithT(t)
	dynamicDNSServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: wildcard-service-entry
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
    istio.io/use-waypoint-namespace: default
spec:
  hosts: ["*.domain.com"]
  ports:
  - number: 80
    name: outbound-tls-1
    protocol: HTTP
  - number: 81
    name: outbound-tls-2
    protocol: HTTP
  resolution: DYNAMIC_DNS`
	// TODO(jaellio): Also set trust bundle
	destinationRule := `apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: domain-tls
  namespace: default
spec:
  host: "*.domain.com"
  trafficPolicy:
    tls:
      mode: SIMPLE`
	d, p := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		waypointInstance,
		dynamicDNSServiceEntry,
		destinationRule)

	clusters := xdstest.ExtractClusters(d.Clusters(p))
	tlsCluster1 := clusters["inbound-vip|80|http|*.domain.com"]
	g.Expect(tlsCluster1).ToNot(BeNil())
	g.Expect(tlsCluster1.TransportSocket.GetTypedConfig().TypeUrl).
		To(Equal("type.googleapis.com/envoy.extensions.transport_sockets.internal_upstream.v3.InternalUpstreamTransport"))
	tlsCluster2 := clusters["inbound-vip|81|http|*.domain.com"]
	g.Expect(tlsCluster2).ToNot(BeNil())
	g.Expect(tlsCluster2.TransportSocket.GetTypedConfig().TypeUrl).
		To(Equal("type.googleapis.com/envoy.extensions.transport_sockets.internal_upstream.v3.InternalUpstreamTransport"))
	// Here we make sure we add validation of SAN for the upstream
	// connections generated by the DFP cluster. That's required by
	// DFP clusters when `allow_insecure_cluster_options` is false.
	po := tlsCluster1.TypedExtensionProtocolOptions["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"]
	g.Expect(po).NotTo(BeNil())
	var httpOptions upstream.HttpProtocolOptions
	err := po.UnmarshalTo(&httpOptions)
	g.Expect(err).To(BeNil())

	g.Expect(httpOptions.UpstreamHttpProtocolOptions.AutoSni).To(BeTrue())
	g.Expect(httpOptions.UpstreamHttpProtocolOptions.AutoSanValidation).To(BeTrue())
}

func TestWaypointFiltersWithDynamicDns(t *testing.T) {
	g := NewWithT(t)
	dynamicDNSServiceEntry := `apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: wildcard-service-entry
  namespace: default
  labels:
    istio.io/use-waypoint: waypoint
    istio.io/use-waypoint-namespace: default
spec:
  hosts: ["*.domain.com"]
  ports:
  - number: 80
    name: http-1
    protocol: HTTP
  - number: 8080
    name: http-2
    protocol: HTTP
  resolution: DYNAMIC_DNS`
	d, p := setupWaypointTest(t,
		waypointGateway,
		waypointSvc,
		waypointInstance,
		dynamicDNSServiceEntry)

	listeners := make(map[string]*listenerv3.Listener)
	for _, l := range d.Listeners(p) {
		listeners[l.Name] = l
	}

	// Making sure the filter chains are there
	mainInternalListener := listeners["main_internal"]
	g.Expect(mainInternalListener).NotTo(BeNil())
	filterChainNames := xdstest.ExtractFilterChainNames(mainInternalListener)
	g.Expect(filterChainNames).To(ContainElements(
		"inbound-vip|80|http|*.domain.com",
		"inbound-vip|8080|http|*.domain.com",
	))

	// Making sure the virtual host matches the wildcarded domain
	// so malicious apps can't exploit the Host header.
	filterChain := xdstest.ExtractFilterChain("inbound-vip|80|http|*.domain.com", mainInternalListener)
	httpConnMngr := xdstest.ExtractHTTPConnectionManager(t, filterChain)
	g.Expect(httpConnMngr).NotTo(BeNil())
	routeConfig := httpConnMngr.GetRouteConfig()
	virtualHosts := xdstest.ExtractVirtualHosts(routeConfig)
	g.Expect(virtualHosts["*.domain.com"]).To(Equal([]string{
		"inbound-vip|80|http|*.domain.com",
	}))

	// Make sure we have the DFP filter in place
	var dfpFilter *hcm.HttpFilter
	for _, f := range httpConnMngr.HttpFilters {
		if f.Name == "envoy.filters.http.dynamic_forward_proxy" {
			dfpFilter = f
		}
	}
	g.Expect(dfpFilter).NotTo(BeNil())
	g.Expect(dfpFilter.GetTypedConfig().TypeUrl).To(Equal("type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig"))
}

func TestWaypointPeerMetadataFilters(t *testing.T) {
	testCases := []struct {
		name                                string
		enableAmbientMultiNetwork           bool
		enableAmbientBaggage                bool
		expectedDownstreamDiscoveryMethods  []string
		expectedDownstreamPropagationMethod string
		expectedUpstreamDiscoveryMethods    []string
		expectedClusterFilters              []string
		expectedConnectFilters              []string
		expectedInnerConnectFilters         []string
	}{
		{
			name:                                "single network ambient uses workload_discovery",
			enableAmbientMultiNetwork:           false,
			enableAmbientBaggage:                false,
			expectedDownstreamDiscoveryMethods:  []string{"workload_discovery"},
			expectedDownstreamPropagationMethod: "",
			expectedUpstreamDiscoveryMethods:    []string{"workload_discovery"},
			expectedClusterFilters:              []string{},
			expectedConnectFilters:              []string{wellknown.TCPProxy},
			expectedInnerConnectFilters:         nil,
		},
		{
			name:                                "ENABLE_AMBIENT_MULTI_NETWORK=true, ENABLE_AMBIENT_BAGGAGE=false",
			enableAmbientMultiNetwork:           true,
			enableAmbientBaggage:                false,
			expectedDownstreamDiscoveryMethods:  []string{"workload_discovery"},
			expectedDownstreamPropagationMethod: "",
			expectedUpstreamDiscoveryMethods:    []string{"workload_discovery"},
			expectedClusterFilters:              []string{},
			expectedConnectFilters:              []string{"envoy.filters.network.tcp_proxy"},
			expectedInnerConnectFilters:         []string{"propagate_endpoint_metadata", wellknown.TCPProxy},
		},
		{
			name:                                "single network ambient uses baggage",
			enableAmbientMultiNetwork:           false,
			enableAmbientBaggage:                true,
			expectedDownstreamDiscoveryMethods:  []string{"baggage", "workload_discovery"},
			expectedDownstreamPropagationMethod: "baggage",
			expectedUpstreamDiscoveryMethods:    []string{"workload_discovery", "upstream_filter_state"},
			expectedClusterFilters:              []string{"upstream_hbone_peer_metadata"},
			expectedConnectFilters:              []string{"upstream_hbone_peer_metadata", wellknown.TCPProxy},
			expectedInnerConnectFilters:         nil,
		},
		{
			name:                                "multi-network ambient uses baggage",
			enableAmbientMultiNetwork:           true,
			enableAmbientBaggage:                true,
			expectedDownstreamDiscoveryMethods:  []string{"baggage", "workload_discovery"},
			expectedDownstreamPropagationMethod: "baggage",
			expectedUpstreamDiscoveryMethods:    []string{"workload_discovery", "upstream_filter_state"},
			expectedClusterFilters:              []string{"upstream_hbone_peer_metadata"},
			expectedConnectFilters:              []string{"upstream_hbone_peer_metadata", wellknown.TCPProxy},
			expectedInnerConnectFilters:         []string{"propagate_endpoint_metadata", "upstream_hbone_peer_metadata", "envoy.filters.network.tcp_proxy"},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			g := NewWithT(t)
			test.SetForTest(t, &features.EnableAmbientMultiNetwork, tc.enableAmbientMultiNetwork)
			test.SetForTest(t, &features.EnableAmbientWaypointMultiNetwork, tc.enableAmbientMultiNetwork)
			test.SetForTest(t, &features.EnableAmbientBaggage, tc.enableAmbientBaggage)

			d, proxy := setupWaypointTest(t,
				waypointGateway,
				waypointSvc,
				waypointInstance,
				appServiceEntry)

			connectTerminateListener := xdstest.ExtractListener("connect_terminate", d.Listeners(proxy))
			g.Expect(connectTerminateListener).NotTo(BeNil())

			connectTerminateHCM := xdstest.ExtractHTTPConnectionManager(t,
				xdstest.ExtractFilterChain("default", connectTerminateListener))
			g.Expect(connectTerminateHCM).NotTo(BeNil(), "connect_terminate HCM should exist")

			// testing downstream first
			{
				var downstreamMetadataFilter *hcm.HttpFilter
				for _, f := range connectTerminateHCM.HttpFilters {
					if f.Name == "waypoint_downstream_peer_metadata" {
						downstreamMetadataFilter = f
						break
					}
				}
				g.Expect(downstreamMetadataFilter).NotTo(BeNil())

				typedStruct := &udpa.TypedStruct{}
				err := downstreamMetadataFilter.GetTypedConfig().UnmarshalTo(typedStruct)
				g.Expect(err).NotTo(HaveOccurred())

				g.Expect(typedStruct.TypeUrl).To(Equal("type.googleapis.com/io.istio.http.peer_metadata.Config"))

				downstreamDiscovery := typedStruct.Value.Fields["downstream_discovery"].GetListValue()
				g.Expect(downstreamDiscovery).NotTo(BeNil())
				g.Expect(len(downstreamDiscovery.Values)).To(BeNumerically("==", len(tc.expectedDownstreamDiscoveryMethods)))

				var discoveryMethods []string
				for _, v := range downstreamDiscovery.Values {
					for key := range v.GetStructValue().GetFields() {
						discoveryMethods = append(discoveryMethods, key)
					}
				}
				for _, expectedDiscoveryMethod := range tc.expectedDownstreamDiscoveryMethods {
					g.Expect(discoveryMethods).To(ContainElement(expectedDiscoveryMethod))
				}

				sharedWithUpstream := typedStruct.Value.Fields["shared_with_upstream"].GetBoolValue()
				g.Expect(sharedWithUpstream).To(BeTrue())

				if tc.expectedDownstreamPropagationMethod != "" {
					downstreamPropagation := typedStruct.Value.Fields["downstream_propagation"].GetListValue()
					g.Expect(downstreamPropagation).NotTo(BeNil())
					g.Expect(len(downstreamPropagation.Values)).To(BeNumerically("==", 1))

					propagationMethod := downstreamPropagation.Values[0].GetStructValue()
					g.Expect(propagationMethod).NotTo(BeNil())
					_, hasExpectedPropagationMethod := propagationMethod.Fields[tc.expectedDownstreamPropagationMethod]
					g.Expect(hasExpectedPropagationMethod).To(BeTrue())
				} else {
					downstreamPropagation := typedStruct.Value.Fields["downstream_propagation"].GetListValue()
					g.Expect(downstreamPropagation).To(BeNil())
				}
			}

			// Now we're going to test upstream
			{
				listeners := make(map[string]*listenerv3.Listener)
				for _, l := range d.Listeners(proxy) {
					listeners[l.Name] = l
				}

				// Making sure the filter chains are there
				mainInternalListener := listeners["main_internal"]
				filterChain := xdstest.ExtractFilterChain("inbound-vip|80|http|app.com", mainInternalListener)
				g.Expect(filterChain).NotTo(BeNil())
				httpConnMngr := xdstest.ExtractHTTPConnectionManager(t, filterChain)
				var upstreamMetadataFilter *hcm.HttpFilter
				for _, f := range httpConnMngr.HttpFilters {
					if f.Name == "waypoint_upstream_peer_metadata" {
						upstreamMetadataFilter = f
						break
					}
				}
				g.Expect(upstreamMetadataFilter).NotTo(BeNil())

				typedStruct := &udpa.TypedStruct{}
				err := upstreamMetadataFilter.GetTypedConfig().UnmarshalTo(typedStruct)
				g.Expect(err).NotTo(HaveOccurred())

				g.Expect(typedStruct.TypeUrl).To(Equal("type.googleapis.com/io.istio.http.peer_metadata.Config"))

				upstreamDiscovery := typedStruct.Value.Fields["upstream_discovery"].GetListValue()
				g.Expect(upstreamDiscovery).NotTo(BeNil())
				g.Expect(len(upstreamDiscovery.Values)).To(BeNumerically("==", len(tc.expectedUpstreamDiscoveryMethods)))

				var discoveryMethods []string
				for _, v := range upstreamDiscovery.Values {
					for key := range v.GetStructValue().GetFields() {
						discoveryMethods = append(discoveryMethods, key)
					}
				}
				for _, expectedDiscoveryMethod := range tc.expectedUpstreamDiscoveryMethods {
					g.Expect(discoveryMethods).To(ContainElement(expectedDiscoveryMethod))
				}
				// No upstream propagation expected

				c := xdstest.ExtractCluster("inbound-vip|80|http|app.com", d.Clusters(proxy))
				g.Expect(c).NotTo(BeNil())
				g.Expect(xdstest.ExtractClusterFilterNames(c.Filters)).To(HaveExactElements(tc.expectedClusterFilters))

				if tc.expectedConnectFilters == nil {
					g.Expect(listeners).NotTo(HaveKey("connect_originate"))
				} else {
					g.Expect(listeners).To(HaveKey("connect_originate"))
					l := listeners["connect_originate"]
					fc := xdstest.ExtractFilterChain("", l)
					g.Expect(fc).NotTo(BeNil())
					fs, _ := xdstest.ExtractFilterNames(t, fc)
					g.Expect(fs).To(HaveExactElements(tc.expectedConnectFilters))
				}

				if tc.expectedInnerConnectFilters == nil {
					g.Expect(listeners).NotTo(HaveKey("inner_connect_originate"))
				} else {
					g.Expect(listeners).To(HaveKey("inner_connect_originate"))
					l := listeners["inner_connect_originate"]
					fc := xdstest.ExtractFilterChain("", l)
					g.Expect(fc).NotTo(BeNil())
					fs, _ := xdstest.ExtractFilterNames(t, fc)
					g.Expect(fs).To(HaveExactElements(tc.expectedInnerConnectFilters))
				}
			}
		})
	}
}

func TestWaypointMultipleVirtualServices(t *testing.T) {
	// Test that multiple VirtualServices targeting the same host at a waypoint
	// have all their routes merged into the inbound VirtualHost.
	vs1 := `apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: vs-route-a
  namespace: default
spec:
  hosts: [app.com]
  http:
  - match:
    - uri:
        prefix: /route-a
    route:
    - destination:
        host: app.com
        port:
          number: 80
`
	vs2 := `apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: vs-route-b
  namespace: default
spec:
  hosts: [app.com]
  http:
  - match:
    - uri:
        prefix: /route-b
    route:
    - destination:
        host: app.com
        port:
          number: 80
`
	d, proxy := setupWaypointTest(t,
		waypointGateway, waypointSvc, waypointInstance,
		appServiceEntry, appWorkloadEntry,
		vs1, vs2)

	l := xdstest.ExtractListener("main_internal", d.Listeners(proxy))
	fc := xdstest.ExtractFilterChain(model.BuildSubsetKey(model.TrafficDirectionInboundVIP, "http", "app.com", 80), l)
	hcmCfg := xdstest.ExtractHTTPConnectionManager(t, fc)
	routeConfig := hcmCfg.GetRouteConfig()

	// Collect all route prefixes across all virtual hosts
	var prefixes []string
	for _, vh := range routeConfig.GetVirtualHosts() {
		for _, r := range vh.Routes {
			if prefix := r.GetMatch().GetPrefix(); prefix != "" {
				prefixes = append(prefixes, prefix)
			}
		}
	}
	// Both VS routes must be present — prior to the fix, only the first VS's routes appeared
	assert.Equal(t, sets.New(prefixes...).Contains("/route-a"), true)
	assert.Equal(t, sets.New(prefixes...).Contains("/route-b"), true)
}

func setupWaypointTest(t *testing.T, configs ...string) (*xds.FakeDiscoveryServer, *model.Proxy) {
	test.SetForTest(t, &features.EnableDualStack, true)
	test.SetForTest(t, &features.EnableIngressWaypointRouting, true)
	c := joinYaml(configs...)
	mc := mesh.DefaultMeshConfig()
	mc.ExtensionProviders = append(mc.ExtensionProviders, &meshconfig.MeshConfig_ExtensionProvider{
		Name: "otel",
		Provider: &meshconfig.MeshConfig_ExtensionProvider_Opentelemetry{
			// Pointing to the waypoint is silly, we just need some valid service to point to and it exists
			Opentelemetry: &meshconfig.MeshConfig_ExtensionProvider_OpenTelemetryTracingProvider{Service: "waypoint.default.svc.cluster.local", Port: 15008},
		},
	})
	// Ambient controller needs objects as kube, so apply to both
	d := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
		ConfigString:           c,
		KubernetesObjectString: c,
		MeshConfig:             mc,
	})
	proxy := d.SetupProxy(&model.Proxy{
		Type:            model.Waypoint,
		ConfigNamespace: "default",
		Labels: map[string]string{
			label.IoK8sNetworkingGatewayGatewayName.Name: "waypoint",
			label.GatewayManaged.Name:                    constants.ManagedGatewayMeshControllerLabel,
		},
		IPAddresses: []string{"3.0.0.1"}, // match the WE
	})
	return d, proxy
}

func joinYaml(s ...string) string {
	return strings.Join(s, "\n---\n")
}

func TestIngressGatewayListenersAndClustersMultiNetwork(t *testing.T) {
	// Ingress gateway service with network label
	ingressGatewaySvc := `apiVersion: v1
kind: Service
metadata:
  name: istio-ingressgateway
  namespace: istio-system
  labels:
    istio: ingressgateway
    topology.istio.io/network: network-1
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 80
    targetPort: 8080
`

	// Gateway resource for ingress
	ingressGateway := `apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: my-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*.example.com"
`

	cases := []struct {
		name            string
		enableHBONESend bool
		expect2HBONE    bool
	}{
		{
			name:            "2HBONE listeners and clusters present when HBONESend enabled",
			enableHBONESend: true,
			expect2HBONE:    true,
		},
		{
			name:            "no 2HBONE listeners and clusters when HBONESend disabled",
			enableHBONESend: false,
			expect2HBONE:    false,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			// Set feature flags before creating discovery server
			test.SetForTest(t, &features.MultiNetworkGatewayAPI, true)
			test.SetForTest(t, &features.EnableAmbientMultiNetwork, true)
			test.SetForTest(t, &features.EnableAmbientIngressMultiNetwork, true)
			test.SetForTest(t, &features.EnableHBONESend, tc.enableHBONESend)

			c := joinYaml(ingressGatewaySvc, ingressGateway)
			mc := mesh.DefaultMeshConfig()
			d := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
				ConfigString:           c,
				KubernetesObjectString: c,
				MeshConfig:             mc,
			})

			// Create an ingress gateway proxy (Router type)
			proxy := d.SetupProxy(&model.Proxy{
				Type:            model.Router,
				ConfigNamespace: "istio-system",
				Labels: map[string]string{
					"istio":                    "ingressgateway",
					label.TopologyNetwork.Name: "network-1",
				},
				IPAddresses: []string{"10.0.0.1"},
				Metadata: &model.NodeMetadata{
					Labels: map[string]string{
						"istio":                    "ingressgateway",
						label.TopologyNetwork.Name: "network-1",
					},
				},
			})

			listeners := d.Listeners(proxy)
			clusters := d.Clusters(proxy)

			listenerNames := []string{}
			for _, l := range listeners {
				listenerNames = append(listenerNames, l.Name)
			}

			clusterNames := []string{}
			for _, c := range clusters {
				clusterNames = append(clusterNames, c.Name)
			}

			expectedListeners := []string{"inner_connect_originate", "outer_connect_originate"}
			expectedClusters := []string{"inner_connect_originate", "outer_connect_originate"}

			g := NewWithT(t)
			if tc.expect2HBONE {
				g.Expect(listenerNames).To(ContainElements(expectedListeners))
				g.Expect(clusterNames).To(ContainElements(expectedClusters))
			} else {
				g.Expect(listenerNames).ToNot(ContainElements(expectedListeners))
				g.Expect(clusterNames).ToNot(ContainElements(expectedClusters))
			}
		})
	}
}

func TestWaypointScopedAddressPush(t *testing.T) {
	d, _ := setupWaypointTest(t,
		waypointGateway, waypointSvc, waypointInstance, appServiceEntry)

	ads := d.ConnectADS().WithType(v3.ListenerType).
		WithID("waypoint~3.0.0.1~waypoint-pod.default~default.svc.cluster.local")
	ads.RequestResponseAck(t, nil)

	// Workload churn unrelated to any waypoint (pods scaling, nodes recycling, ...) only
	// produces Address updates; the waypoint's config doesn't reference it, so its
	// listeners must not be rebuilt.
	clienttest.NewWriter[*networkingclient.WorkloadEntry](t, d.KubeClient()).Create(&networkingclient.WorkloadEntry{
		ObjectMeta: metav1.ObjectMeta{Name: "unrelated", Namespace: "default"},
		Spec:       networking.WorkloadEntry{Address: "9.9.9.9"},
	})
	ads.ExpectNoResponse(t)

	// A service attaching to this waypoint arrives through the same Address update path and
	// must still trigger a listener rebuild.
	clienttest.NewWriter[*networkingclient.ServiceEntry](t, d.KubeClient()).Create(&networkingclient.ServiceEntry{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "app2",
			Namespace: "default",
			Labels:    map[string]string{label.IoIstioUseWaypoint.Name: "waypoint"},
		},
		Spec: networking.ServiceEntry{
			Hosts:      []string{"app2.com"},
			Addresses:  []string{"1.2.3.5"},
			Ports:      []*networking.ServicePort{{Number: 80, Name: "http", Protocol: "HTTP"}},
			Resolution: networking.ServiceEntry_STATIC,
		},
	})
	ads.ExpectResponse(t)
}
