// 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 core_test

import (
	"testing"

	auth "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"

	meshconfig "istio.io/api/mesh/v1alpha1"
	networking "istio.io/api/networking/v1alpha3"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/simulation"
	"istio.io/istio/pilot/test/xds"
	"istio.io/istio/pilot/test/xdstest"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/mesh"
	"istio.io/istio/pkg/config/schema/gvk"
	"istio.io/istio/pkg/env"
	"istio.io/istio/pkg/test/util/tmpl"
)

func TestDisablePortTranslation(t *testing.T) {
	virtualServices := `
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: a
spec:
  hosts:
  - "example.com"
  gateways:
  - gateway80
  - gateway8081
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: a
        port:
          number: 80
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: b
spec:
  hosts:
  - "example.com"
  gateways:
  - gateway80
  - gateway8081
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: b
        port:
          number: 8081`
	runGatewayTest(t,
		simulationTest{
			name: "multiple target port, target port first",
			config: createGateway("gateway80", "", `
port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"
`) + createGateway("gateway8081", "", `
port:
  number: 8081
  name: http
  protocol: HTTP
hosts:
- "example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
  labels:
    experimental.istio.io/disable-gateway-port-translation: "true"
spec:
  hosts: ["a.example.com"]
  ports:
  - number: 80
    targetPort: 8081
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
    labels:
      istio: ingressgateway
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance-2
  namespace: istio-system
spec:
  hosts: ["b.example.com"]
  ports:
  - number: 80
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
    labels:
      istio: ingressgateway
---
` + virtualServices,
			calls: []simulation.Expect{
				{
					Name: "target port 1",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "example.com:80",
					},
				},
				{
					Name: "target port 2",
					Call: simulation.Call{
						Port:       8081,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8081",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8081",
						VirtualHostMatched: "example.com:8081",
					},
				},
			},
		},
		simulationTest{
			name: "multiple target port, service port first",
			config: createGateway("gateway80", "", `
port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"
`) + createGateway("gateway8081", "", `
port:
  number: 8081
  name: http
  protocol: HTTP
hosts:
- "example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
  labels:
    experimental.istio.io/disable-gateway-port-translation: "true"
spec:
  hosts: ["b.example.com"]
  ports:
  - number: 80
    targetPort: 8081
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
    labels:
      istio: ingressgateway
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance-2
  namespace: istio-system
spec:
  hosts: ["a.example.com"]
  ports:
  - number: 80
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
    labels:
      istio: ingressgateway
---
` + virtualServices,
			calls: []simulation.Expect{
				{
					Name: "target port 1",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "example.com:80",
					},
				},
				{
					Name: "target port 2",
					Call: simulation.Call{
						Port:       8081,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8081",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8081",
						VirtualHostMatched: "example.com:8081",
					},
				},
			},
		})
}

func TestHTTPGateway(t *testing.T) {
	httpServer := `port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "foo.bar"`
	simpleRoute := `apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: vs
spec:
  hosts:
  - "example.com"
  gateways:
  - gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: b
---
`
	runGatewayTest(t,
		simulationTest{
			name:   "no virtual services",
			config: createGateway("", "", httpServer),
			calls: []simulation.Expect{
				{
					// Expect listener, but no routing
					Name: "defined port",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "foo.bar",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						Error:              simulation.ErrNoRoute,
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						VirtualHostMatched: "blackhole:80",
					},
				},
				{
					// There will be no listener
					Name: "undefined port",
					Call: simulation.Call{
						Port:       81,
						HostHeader: "foo.bar",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						Error: simulation.ErrNoListener,
					},
				},
			},
		},
		simulationTest{
			name: "simple http and virtual service",
			config: createGateway("gateway", "", httpServer) + `
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
  - "*"
  gateways:
  - gateway
  http:
  - match:
    - uri:
        exact: /productpage
    route:
    - destination:
        host: productpage
        port:
          number: 9080
`,
			calls: []simulation.Expect{
				{
					Name: "uri mismatch",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "foo.bar",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						// We didn't match the URI
						Error:              simulation.ErrNoRoute,
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						VirtualHostMatched: "foo.bar:80",
					},
				},
				{
					Name: "host mismatch",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "bad.bar",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						// We didn't match the host
						Error:              simulation.ErrNoVirtualHost,
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
					},
				},
				{
					Name: "match",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "foo.bar",
						Path:       "/productpage",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_80",
						VirtualHostMatched: "foo.bar:80",
						ClusterMatched:     "outbound|9080||productpage.default",
					},
				},
			},
		},
		simulationTest{
			name: "virtual service merging",
			config: createGateway("gateway", "", `port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "*.example.com"`) + `
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: a
spec:
  hosts:
  - "a.example.com"
  gateways:
  - gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: a
        port:
          number: 80
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: b
spec:
  hosts:
  - "b.example.com"
  gateways:
  - gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: b
        port:
          number: 80
`,
			calls: []simulation.Expect{
				{
					Name: "a",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "a.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{ClusterMatched: "outbound|80||a.default"},
				},
				{
					Name: "b",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "b.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{ClusterMatched: "outbound|80||b.default"},
				},
				{
					Name: "undefined hostname",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "c.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{Error: simulation.ErrNoVirtualHost},
				},
			},
		},
		simulationTest{
			name: "httpsRedirect without routes",
			config: createGateway("gateway", "", `port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"
tls:
  httpsRedirect: true`),
			calls: []simulation.Expect{
				{
					Name: "request",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						Error:              simulation.ErrTLSRedirect,
						ListenerMatched:    "0.0.0.0_80",
						VirtualHostMatched: "example.com:80",
						RouteConfigMatched: "http.80",
						StrictMatch:        true,
					},
				},
			},
		},
		simulationTest{
			// This should be the same as without, we elide the routes anyways since they always
			// redirect
			name: "httpsRedirect with routes",
			config: createGateway("gateway", "", `port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"
tls:
  httpsRedirect: true`) + simpleRoute,
			calls: []simulation.Expect{
				{
					Name: "request",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						Error:              simulation.ErrTLSRedirect,
						ListenerMatched:    "0.0.0.0_80",
						VirtualHostMatched: "example.com:80",
						RouteConfigMatched: "http.80",
						StrictMatch:        true,
					},
				},
			},
		},
		simulationTest{
			// A common example of when this would occur is when a user defines their Gateway with
			// httpsRedirect, then cert-manager creates an Ingress which requires sending HTTP
			// traffic Unfortunately, Envoy doesn't have a good way to do HTTPS redirect per-route.
			name: "mixed httpsRedirect with routes",
			config: createGateway("gateway", "", `port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"
tls:
  httpsRedirect: true`) + createGateway("gateway2", "", `port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"`) + simpleRoute,
			calls: []simulation.Expect{
				{
					Name: "request",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						Error:              simulation.ErrTLSRedirect,
						ListenerMatched:    "0.0.0.0_80",
						VirtualHostMatched: "example.com:80",
						RouteConfigMatched: "http.80",
						StrictMatch:        true,
					},
				},
			},
		},
		simulationTest{
			name: "httpsRedirect on https",
			config: createGateway("gateway", "", `port:
  number: 443
  name: https
  protocol: HTTPS
hosts:
- "example.com"
tls:
  httpsRedirect: true
  mode: SIMPLE
  credentialName: test`) + simpleRoute,
			calls: []simulation.Expect{
				{
					Name: "request",
					Call: simulation.Call{
						Port:       443,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
						TLS:        simulation.TLS,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_443",
						VirtualHostMatched: "example.com:443",
						RouteConfigMatched: "https.443.https.gateway.default",
						ClusterMatched:     "outbound|443||b.default",
						StrictMatch:        true,
					},
				},
			},
		},
	)
}

func TestGatewayConflicts(t *testing.T) {
	tcpServer := `port:
  number: 80
  name: tcp
  protocol: TCP
hosts:
- "foo.bar"`
	httpServer := `port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "foo.bar"`
	tlsServer := `hosts:
  - ./*
port:
  name: https-ingress
  number: 443
  protocol: HTTPS
tls:
  credentialName: sds-credential
  mode: SIMPLE`
	gatewayCollision := `
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
  - "*"
  gateways:
  - istio-system/gateway
  tcp:
  - route:
    - destination:
        host: productpage
        port:
          number: 9080
`
	runGatewayTest(t,
		simulationTest{
			name: "duplicate cross namespace gateway collision",
			config: createGateway("gateway", "istio-system", tcpServer) +
				createGateway("gateway", "alpha", tcpServer) + // namespace comes before istio-system

				gatewayCollision,
			calls: []simulation.Expect{
				{
					Name: "call",
					Call: simulation.Call{Port: 80, Protocol: simulation.TCP},
					// TODO(https://github.com/istio/istio/issues/21394) This is a bug!
					// Should have identical result to the test below
					Result: simulation.Result{Error: simulation.ErrNoListener},
				},
			},
		},
		simulationTest{
			name: "duplicate cross namespace gateway collision - selected first",
			config: createGateway("gateway", "istio-system", tcpServer) +
				createGateway("gateway", "zeta", tcpServer) + // namespace comes after istio-system
				gatewayCollision,
			calls: []simulation.Expect{
				{
					Name:   "call",
					Call:   simulation.Call{Port: 80, Protocol: simulation.TCP},
					Result: simulation.Result{ListenerMatched: "0.0.0.0_80", ClusterMatched: "outbound|9080||productpage.default"},
				},
			},
		},
		simulationTest{
			name:           "duplicate tls gateway",
			skipValidation: true,
			// Create the same gateway in two namespaces
			config: createGateway("", "istio-system", tlsServer) +
				createGateway("", "default", tlsServer),
			calls: []simulation.Expect{
				{
					// TODO(https://github.com/istio/istio/issues/24638) This is a bug!
					// We should not have multiple matches, envoy will NACK this
					Name:   "call",
					Call:   simulation.Call{Port: 443, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "foo.bar"},
					Result: simulation.Result{Error: simulation.ErrMultipleFilterChain},
				},
			},
		},
		simulationTest{
			name: "duplicate tls virtual service",
			// Create the same virtual service in two namespaces
			config: `
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: ingressgateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*.example.com'
    port:
      name: https
      number: 443
      protocol: HTTPS
    tls:
      mode: PASSTHROUGH
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: vs1
  namespace: default
spec:
  gateways:
  - istio-system/ingressgateway
  hosts:
  - mysite.example.com
  tls:
  - match:
    - port: 443
      sniHosts:
      - mysite.example.com
    route:
    - destination:
        host: mysite.default.svc.cluster.local
        port:
          number: 443
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: vs2
  namespace: default
spec:
  gateways:
  - istio-system/ingressgateway
  hosts:
  - mysite.example.com
  tls:
  - match:
    - port: 443
      sniHosts:
      - mysite.example.com
    route:
    - destination:
        host: mysite.default.svc.cluster.local
        port:
          number: 443
`,
			calls: []simulation.Expect{
				{
					Name: "call",
					Call: simulation.Call{Port: 443, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "mysite.example.com"},
					Result: simulation.Result{
						ListenerMatched: "0.0.0.0_443",
						ClusterMatched:  "outbound|443||mysite.default.svc.cluster.local",
					},
				},
			},
		},
		simulationTest{
			// TODO(https://github.com/istio/istio/issues/27481) this may be a bug. At very least, this should have indication to user
			name: "multiple protocols on a port - tcp first",
			config: createGateway("alpha", "", tcpServer) +
				createGateway("beta", "", httpServer),
			calls: []simulation.Expect{
				{
					Name: "call tcp",
					// TCP takes precedence. Since we have no tcp routes, this will result in no listeners
					Call: simulation.Call{
						Port:     80,
						Protocol: simulation.TCP,
					},
					Result: simulation.Result{
						Error: simulation.ErrNoListener,
					},
				},
			},
		},
		simulationTest{
			// TODO(https://github.com/istio/istio/issues/27481) this may be a bug. At very least, this should have indication to user
			name: "multiple protocols on a port - http first",
			config: createGateway("beta", "", tcpServer) +
				createGateway("alpha", "", httpServer),
			calls: []simulation.Expect{
				{
					// Port define in gateway, but no virtual services
					// Expect a 404
					// HTTP protocol takes precedence
					Name: "call http",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "foo.bar",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						Error:              simulation.ErrNoRoute,
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						VirtualHostMatched: "blackhole:80",
					},
				},
			},
		},
		simulationTest{
			name: "multiple wildcards with virtual service disambiguator",
			config: createGateway("alpha", "", `
hosts:
  - ns-1/*.example.com
port:
  name: http
  number: 80
  protocol: HTTP`) +
				createGateway("beta", "", `
hosts:
  - ns-2/*.example.com
port:
  name: http
  number: 80
  protocol: HTTP`) + `
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: default
  namespace: ns-1
spec:
  hosts:
  - "ns-1.example.com"
  gateways:
  - default/alpha
  http:
  - route:
    - destination:
        host: echo
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: default
  namespace: ns-2
spec:
  hosts:
  - "ns-2.example.com"
  gateways:
  - default/beta
  http:
  - route:
    - destination:
        host: echo
`,
			calls: []simulation.Expect{
				{
					Name: "ns-1",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "ns-1.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						ClusterMatched:     "outbound|80||echo.ns-1",
					},
				},
				{
					Name: "ns-2",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "ns-2.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						ClusterMatched:     "outbound|80||echo.ns-2",
					},
				},
			},
		},
		simulationTest{
			name: "multiple virtual services that should become separate vhosts",
			config: `
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: public-gw
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - foo.example.com
        - bar.example.com
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: foobar-vs
  namespace: default
  creationTimestamp: 2010-01-01T00:00:00Z
spec:
  hosts:
    - foo.example.com
    - bar.example.com
  gateways:
    - istio-system/public-gw
  http:
    - name: foobar
      match:
        - uri:
            prefix: /1
        - uri:
            prefix: /2
        - uri:
            prefix: /3
      route:
        - destination:
            host: echo-foobar
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: foo-vs
  namespace: default
  creationTimestamp: 2015-01-01T00:00:00Z
spec:
  gateways:
    - istio-system/public-gw
  hosts:
    - foo.example.com
  http:
    - name: foo
      match:
        - uri:
            prefix: /foo
      route:
        - destination:
            host: echo-foo
            port:
              number: 80
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: bar-vs
  namespace: default
  creationTimestamp: 2020-01-01T00:00:00Z
spec:
  gateways:
    - istio-system/public-gw
  hosts:
    - bar.example.com
  http:
    - name: bar
      match:
        - uri:
            prefix: /bar
      route:
        - destination:
            host: echo-bar
            port:
              number: 80
`,
			calls: []simulation.Expect{
				{
					Name: "foobar",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "foo.example.com",
						Protocol:   simulation.HTTP,
						Path:       "/1",
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						ClusterMatched:     "outbound|80||echo-foobar.default",
					},
				},
				{
					Name: "foo",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "foo.example.com",
						Protocol:   simulation.HTTP,
						Path:       "/foo",
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						ClusterMatched:     "outbound|80||echo-foo.default",
					},
				},
				{
					Name: "bar",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "bar.example.com",
						Protocol:   simulation.HTTP,
						Path:       "/bar",
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_80",
						RouteConfigMatched: "http.80",
						ClusterMatched:     "outbound|80||echo-bar.default",
					},
				},
			},
		},
	)
}

func TestIngress(t *testing.T) {
	cfg := `
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{.Name}}
  namespace: default
  creationTimestamp: "{{.Time}}"
  annotations:
    kubernetes.io/ingress.class: istio
spec:
  rules:
  - host: example.com
    http:
      paths:
      - pathType: Prefix
        backend:
          service:
            name: {{.Name}}
            port:
              number: 80
        path: /{{.Name}}
  tls:
  - hosts:
    - example.com
    secretName: ingressgateway-certs
---`
	runGatewayTest(t, simulationTest{
		name: "ingress shared TLS cert conflict - beta first",
		// TODO(https://github.com/istio/istio/issues/24385) this is a bug
		// "alpha" is created after "beta". This triggers a mismatch in the conflict resolution logic in Ingress and VirtualService, leading to unexpected results
		kubeConfig: tmpl.MustEvaluate(cfg, map[string]string{"Name": "alpha", "Time": "2020-01-01T00:00:00Z"}) +
			tmpl.MustEvaluate(cfg, map[string]string{"Name": "beta", "Time": "2010-01-01T00:00:00Z"}),
		calls: []simulation.Expect{
			{
				Name: "http alpha",
				Call: simulation.Call{
					Port:       80,
					HostHeader: "example.com",
					Path:       "/alpha",
					Protocol:   simulation.HTTP,
				},
				Result: simulation.Result{
					ListenerMatched:    "0.0.0.0_80",
					RouteConfigMatched: "http.80",
					ClusterMatched:     "outbound|80||alpha.default.svc.cluster.local",
				},
			},
			{
				Name: "http beta",
				Call: simulation.Call{
					Port:       80,
					HostHeader: "example.com",
					Path:       "/beta",
					Protocol:   simulation.HTTP,
				},
				Result: simulation.Result{
					ListenerMatched:    "0.0.0.0_80",
					RouteConfigMatched: "http.80",
					ClusterMatched:     "outbound|80||beta.default.svc.cluster.local",
				},
			},
			{
				Name: "https alpha",
				Call: simulation.Call{
					Port:       443,
					HostHeader: "example.com",
					Path:       "/alpha",
					Protocol:   simulation.HTTP,
					TLS:        simulation.TLS,
				},
				Result: simulation.Result{
					Error:              simulation.ErrNoRoute,
					ListenerMatched:    "0.0.0.0_443",
					RouteConfigMatched: "https.443.https-443-ingress-alpha-default-0.alpha-istio-autogenerated-k8s-ingress-default.istio-system",
					VirtualHostMatched: "blackhole:443",
				},
			},
			{
				Name: "https beta",
				Call: simulation.Call{
					Port:       443,
					HostHeader: "example.com",
					Path:       "/beta",
					Protocol:   simulation.HTTP,
					TLS:        simulation.TLS,
				},
				Result: simulation.Result{
					Error:              simulation.ErrNoRoute,
					ListenerMatched:    "0.0.0.0_443",
					RouteConfigMatched: "https.443.https-443-ingress-alpha-default-0.alpha-istio-autogenerated-k8s-ingress-default.istio-system",
					VirtualHostMatched: "blackhole:443",
				},
			},
		},
	}, simulationTest{
		name: "ingress shared TLS cert conflict - alpha first",
		// "alpha" is created before "beta". This avoids the bug in the previous test
		kubeConfig: tmpl.MustEvaluate(cfg, map[string]string{"Name": "alpha", "Time": "2010-01-01T00:00:00Z"}) +
			tmpl.MustEvaluate(cfg, map[string]string{"Name": "beta", "Time": "2020-01-01T00:00:00Z"}),
		calls: []simulation.Expect{
			{
				Name: "http alpha",
				Call: simulation.Call{
					Port:       80,
					HostHeader: "example.com",
					Path:       "/alpha",
					Protocol:   simulation.HTTP,
				},
				Result: simulation.Result{
					ListenerMatched:    "0.0.0.0_80",
					RouteConfigMatched: "http.80",
					ClusterMatched:     "outbound|80||alpha.default.svc.cluster.local",
				},
			},
			{
				Name: "http beta",
				Call: simulation.Call{
					Port:       80,
					HostHeader: "example.com",
					Path:       "/beta",
					Protocol:   simulation.HTTP,
				},
				Result: simulation.Result{
					ListenerMatched:    "0.0.0.0_80",
					RouteConfigMatched: "http.80",
					ClusterMatched:     "outbound|80||beta.default.svc.cluster.local",
				},
			},
			{
				Name: "https alpha",
				Call: simulation.Call{
					Port:       443,
					HostHeader: "example.com",
					Path:       "/alpha",
					Protocol:   simulation.HTTP,
					TLS:        simulation.TLS,
				},
				Result: simulation.Result{
					ListenerMatched:    "0.0.0.0_443",
					RouteConfigMatched: "https.443.https-443-ingress-alpha-default-0.alpha-istio-autogenerated-k8s-ingress-default.istio-system",
					ClusterMatched:     "outbound|80||alpha.default.svc.cluster.local",
				},
			},
			{
				Name: "https beta",
				Call: simulation.Call{
					Port:       443,
					HostHeader: "example.com",
					Path:       "/beta",
					Protocol:   simulation.HTTP,
					TLS:        simulation.TLS,
				},
				Result: simulation.Result{
					ListenerMatched:    "0.0.0.0_443",
					RouteConfigMatched: "https.443.https-443-ingress-alpha-default-0.alpha-istio-autogenerated-k8s-ingress-default.istio-system",
					ClusterMatched:     "outbound|80||beta.default.svc.cluster.local",
				},
			},
		},
	})
}

type simulationTest struct {
	name       string
	config     string
	kubeConfig string
	// skipValidation disables validation of XDS resources. Should be used only when we expect a failure (regression catching)
	skipValidation bool
	calls          []simulation.Expect
}

var debugMode = env.Register("SIMULATION_DEBUG", true, "if enabled, will dump verbose output").Get()

func runGatewayTest(t *testing.T, cases ...simulationTest) {
	for _, tt := range cases {
		proxy := &model.Proxy{
			Labels: map[string]string{"istio": "ingressgateway"},
			Metadata: &model.NodeMetadata{
				Labels:    map[string]string{"istio": "ingressgateway"},
				Namespace: "istio-system",
			},
			Type: model.Router,
		}
		runSimulationTest(t, proxy, xds.FakeOptions{}, tt)
	}
}

func runSimulationTest(t *testing.T, proxy *model.Proxy, o xds.FakeOptions, tt simulationTest) {
	runTest := func(t *testing.T) {
		o.ConfigString = tt.config
		o.KubernetesObjectString = tt.kubeConfig
		s := xds.NewFakeDiscoveryServer(t, o)
		sim := simulation.NewSimulation(t, s, s.SetupProxy(proxy))
		sim.RunExpectations(tt.calls)
		if t.Failed() && debugMode {
			t.Log(xdstest.MapKeys(xdstest.ExtractClusters(sim.Clusters)))
			t.Log(xdstest.ExtractListenerNames(sim.Listeners))
			t.Log(xdstest.DumpList(t, sim.Listeners))
			t.Log(xdstest.DumpList(t, sim.Routes))
			t.Log(tt.config)
			t.Log(tt.kubeConfig)
		}
		t.Run("validate configs", func(t *testing.T) {
			if tt.skipValidation {
				t.Skip()
			}
			xdstest.ValidateClusters(t, sim.Clusters)
			xdstest.ValidateListeners(t, sim.Listeners)
			xdstest.ValidateRouteConfigurations(t, sim.Routes)
		})
	}
	if tt.name != "" {
		t.Run(tt.name, runTest)
	} else {
		runTest(t)
	}
}

func createGateway(name, namespace string, servers ...string) string {
	if name == "" {
		name = "default"
	}
	if namespace == "" {
		namespace = "default"
	}
	return tmpl.MustEvaluate(`apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: "{{.Name}}"
  namespace: "{{.Namespace}}"
spec:
  selector:
    istio: ingressgateway
  servers:
{{- range $i, $p := $.Servers }}
  -
{{$p | trim | indent 4}}
{{- end }}
---
`, struct {
		Name      string
		Namespace string
		Servers   []string
	}{name, namespace, servers})
}

func createGatewayWithServiceSelector(name, service string, servers ...string) string {
	if name == "" {
		name = "default"
	}
	return tmpl.MustEvaluate(`apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: "{{.Name}}"
  namespace: "istio-system"
  annotations:
    internal.istio.io/gateway-service: "{{.Service}}"
spec:
  servers:
{{- range $i, $p := $.Servers }}
  -
{{$p | trim | indent 4}}
{{- end }}
---
`, struct {
		Name    string
		Service string
		Servers []string
	}{name, service, servers})
}

func TestTargetPort(t *testing.T) {
	runGatewayTest(t,
		simulationTest{
			name: "basic",
			config: createGatewayWithServiceSelector("gateway80", "istio-ingressgateway.istio-system.svc.cluster.local", `
port:
 number: 80
 name: http
 protocol: HTTP
hosts:
- "example.com"
`) + createGatewayWithServiceSelector("gateway81", "istio-ingressgateway.istio-system.svc.cluster.local", `
port:
 number: 8080
 name: http
 protocol: HTTP
hosts:
- "example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
 name: service-instance
 namespace: istio-system
spec:
 hosts: ["istio-ingressgateway.istio-system.svc.cluster.local"]
 ports:
 - number: 80
   targetPort: 8080
   name: http
   protocol: HTTP
 resolution: STATIC
 location: MESH_INTERNAL
 endpoints:
 - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
 name: a
spec:
 hosts:
 - "example.com"
 gateways:
 - istio-system/gateway80
 - istio-system/gateway81
 http:
 - match:
   - uri:
       prefix: /
   route:
   - destination:
       host: a
       port:
         number: 80`,
			calls: []simulation.Expect{
				{
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "example.com:80",
					},
				},
			},
		},
		simulationTest{
			name: "multiple target port",
			config: createGatewayWithServiceSelector("gateway80", "ingress1.istio-system.svc.cluster.local", `
port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"
`) + createGatewayWithServiceSelector("gateway81", "ingress2.istio-system.svc.cluster.local", `
port:
  number: 80
  name: http
  protocol: HTTP
hosts:
- "example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
spec:
  hosts: ["ingress1.istio-system.svc.cluster.local"]
  ports:
  - number: 80
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance-2
  namespace: istio-system
spec:
  hosts: ["ingress2.istio-system.svc.cluster.local"]
  ports:
  - number: 80
    targetPort: 8081
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: a
spec:
  hosts:
  - "example.com"
  gateways:
  - istio-system/gateway80
  - istio-system/gateway81
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: a
        port:
          number: 80
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: b
spec:
  hosts:
  - "example.com"
  gateways:
  - istio-system/gateway80
  - istio-system/gateway81
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: b
        port:
          number: 8081`,
			calls: []simulation.Expect{
				{
					Name: "target port 1",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "example.com:80",
					},
				},
				{
					Name: "target port 2",
					Call: simulation.Call{
						Port:       8081,
						HostHeader: "example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8081",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8081",
						VirtualHostMatched: "example.com:80",
					},
				},
			},
		})
}

func TestGatewayServices(t *testing.T) {
	runGatewayTest(t,
		simulationTest{
			name: "single service",
			config: createGatewayWithServiceSelector("gateway", "istio-ingressgateway.istio-system.svc.cluster.local", `
port:
  number: 80
  name: http-80
  protocol: HTTP
hosts:
- "80.example.com"
`, `
port:
  number: 82
  name: http-82
  protocol: HTTP
hosts:
- "82.example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
spec:
  hosts: ["istio-ingressgateway.istio-system.svc.cluster.local"]
  ports:
  - number: 80
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: a
spec:
  hosts:
  - "*.example.com"
  gateways:
  - istio-system/gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: a
        port:
          number: 80`,
			calls: []simulation.Expect{
				{
					Name: "port 8080",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "80.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "80.example.com:80",
					},
				},
				{
					Name: "no service match",
					Call: simulation.Call{
						Port:       82,
						HostHeader: "81.example.com",
						Protocol:   simulation.HTTP,
					},
					// For gateway service reference, if there is no matching port we do not setup a listener
					Result: simulation.Result{
						Error: simulation.ErrNoListener,
					},
				},
			},
		},
		simulationTest{
			name: "wrong namespace",
			config: createGatewayWithServiceSelector("gateway", "istio-ingressgateway.not-istio-system.svc.cluster.local", `
port:
  number: 80
  name: http-80
  protocol: HTTP
hosts:
- "80.example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: not-istio-system
spec:
  hosts: ["istio-ingressgateway.not-istio-system.svc.cluster.local"]
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---`,
			calls: []simulation.Expect{
				{
					Name: "port 80",
					Call: simulation.Call{
						Port:       80,
						HostHeader: "80.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						// Service selected is in another namespace, we cannot cross namespace boundary
						Error: simulation.ErrNoListener,
					},
				},
			},
		},
		simulationTest{
			name: "multiple services",
			config: createGatewayWithServiceSelector("gateway", "istio-ingressgateway.istio-system.svc.cluster.local,ingress.com", `
port:
  number: 80
  name: http-80
  protocol: HTTP
hosts:
- "80.example.com"
`, `
port:
  number: 81
  name: http-81
  protocol: HTTP
hosts:
- "81.example.com"
`, `
port:
  number: 82
  name: http-82
  protocol: HTTP
hosts:
- "82.example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
spec:
  hosts: ["istio-ingressgateway.istio-system.svc.cluster.local"]
  ports:
  - number: 80
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance2
  namespace: istio-system
spec:
  hosts: ["ingress.com"]
  ports:
  - number: 81
    targetPort: 8081
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: a
spec:
  hosts:
  - "*.example.com"
  gateways:
  - istio-system/gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: a
        port:
          number: 80`,
			calls: []simulation.Expect{
				{
					Name: "port 8080",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "80.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "80.example.com:80",
					},
				},
				{
					Name: "port 8081",
					Call: simulation.Call{
						Port:       8081,
						HostHeader: "81.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8081",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8081",
						VirtualHostMatched: "81.example.com:81",
					},
				},
				{
					// For gateway service reference, if there is no matching port we do not setup a listener
					Name: "no service match",
					Call: simulation.Call{
						Port:       82,
						HostHeader: "81.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						Error: simulation.ErrNoListener,
					},
				},
			},
		},
		simulationTest{
			name: "multiple overlapping services",
			config: createGatewayWithServiceSelector("gateway", "istio-ingressgateway.istio-system.svc.cluster.local,ingress.com", `
port:
 number: 80
 name: http-80
 protocol: HTTP
hosts:
- "80.example.com"
`, `
port:
 number: 81
 name: http-81
 protocol: HTTP
hosts:
- "81.example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
 name: service-instance
 namespace: istio-system
spec:
 hosts: ["istio-ingressgateway.istio-system.svc.cluster.local"]
 ports:
 - number: 80
   targetPort: 8080
   name: http
   protocol: HTTP
 resolution: STATIC
 location: MESH_INTERNAL
 endpoints:
 - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
 name: service-instance2
 namespace: istio-system
spec:
 hosts: ["ingress.com"]
 ports:
 - number: 81
   targetPort: 8080
   name: http
   protocol: HTTP
 resolution: STATIC
 location: MESH_INTERNAL
 endpoints:
 - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
 name: a-80
spec:
 hosts:
 - "80.example.com"
 gateways:
 - istio-system/gateway
 http:
 - match:
   - uri:
       prefix: /
   route:
   - destination:
       host: a
       port:
         number: 80
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
 name: a-81
spec:
 hosts:
 - "81.example.com"
 gateways:
 - istio-system/gateway
 http:
 - match:
   - uri:
       prefix: /
   route:
   - destination:
       host: a
       port:
         number: 80`,
			calls: []simulation.Expect{
				{
					Name: "port 8080 from 80",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "80.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "80.example.com:80",
					},
				},
				{
					Name: "port 8080 from 81",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "81.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "81.example.com:81",
					},
				},
			},
		},
		simulationTest{
			name: "multiple overlapping services wildcard",
			config: createGatewayWithServiceSelector("gateway", "istio-ingressgateway.istio-system.svc.cluster.local,ingress.com", `
port:
  number: 80
  name: http-80
  protocol: HTTP
hosts:
- "80.example.com"
`, `
port:
  number: 81
  name: http-81
  protocol: HTTP
hosts:
- "81.example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
spec:
  hosts: ["istio-ingressgateway.istio-system.svc.cluster.local"]
  ports:
  - number: 80
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance2
  namespace: istio-system
spec:
  hosts: ["ingress.com"]
  ports:
  - number: 81
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
 name: a-80
spec:
 hosts:
 - "80.example.com"
 gateways:
 - istio-system/gateway
 http:
 - match:
   - uri:
       prefix: /
   route:
   - destination:
       host: a
       port:
         number: 80
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
 name: a-81
spec:
 hosts:
 - "81.example.com"
 gateways:
 - istio-system/gateway
 http:
 - match:
   - uri:
       prefix: /
   route:
   - destination:
       host: a
       port:
         number: 80`,
			calls: []simulation.Expect{
				{
					Name: "port 8080 from 80",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "80.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "80.example.com:80",
					},
				},
				{
					Name: "port 8080 from 81",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "81.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "81.example.com:81",
					},
				},
			},
		},
		simulationTest{
			name: "no match selector",
			config: createGateway("gateway", "istio-system", `
port:
  number: 80
  name: http-80
  protocol: HTTP
hosts:
- "80.example.com"
`, `
port:
  number: 82
  name: http-82
  protocol: HTTP
hosts:
- "82.example.com"
`) + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
spec:
  hosts: ["istio-ingressgateway.istio-system.svc.cluster.local"]
  ports:
  - number: 80
    targetPort: 8080
    name: http
    protocol: HTTP
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: a
spec:
  hosts:
  - "*.example.com"
  gateways:
  - istio-system/gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: a
        port:
          number: 80`,
			calls: []simulation.Expect{
				{
					Name: "port 8080",
					Call: simulation.Call{
						Port:       8080,
						HostHeader: "80.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_8080",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.8080",
						VirtualHostMatched: "80.example.com:80",
					},
				},
				{
					// For gateway selector more, if there is no matching service we use the port as is
					Name: "no service match",
					Call: simulation.Call{
						Port:       82,
						HostHeader: "82.example.com",
						Protocol:   simulation.HTTP,
					},
					Result: simulation.Result{
						ListenerMatched:    "0.0.0.0_82",
						ClusterMatched:     "outbound|80||a.default",
						RouteConfigMatched: "http.82",
						VirtualHostMatched: "82.example.com:82",
					},
				},
			},
		},
		simulationTest{
			name:           "overlapping SNI match",
			skipValidation: true, // TODO: https://github.com/istio/istio/issues/39921
			config: `apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: gw
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - example.com
    port:
      name: example
      number: 443
      protocol: TLS
    tls:
      mode: PASSTHROUGH
  - hosts:
    - '*'
    port:
      name: wildcard-tls
      number: 443
      protocol: HTTPS
    tls:
      mode: PASSTHROUGH
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: example
  namespace: istio-system
spec:
  gateways:
  - gw
  hosts:
  - example.com
  tls:
  - match:
    - sniHosts:
      - example.com
    route:
    - destination:
        host: example
` + `
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: service-instance
  namespace: istio-system
spec:
  hosts: ["istio-ingressgateway.istio-system.svc.cluster.local"]
  ports:
  - number: 443
    targetPort: 443
    name: https
    protocol: HTTPS
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 1.1.1.1`,
			calls: []simulation.Expect{},
		},
	)
}

func TestTLSRouteTermination(t *testing.T) {
	tlsTerminateGateway := `
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*/example.com'
    port:
      name: tls-terminate
      number: 443
      protocol: TLS
    tls:
      mode: SIMPLE
      credentialName: sds-credential
`
	tlsPassthroughGateway := `
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*/example.com'
    port:
      name: tls-passthrough
      number: 443
      protocol: TLS
    tls:
      mode: PASSTHROUGH
`
	// VirtualService configs with internal annotations must be provided as config.Config
	// objects rather than YAML strings, because the YAML parser rejects internal annotations.
	tlsRouteVS := config.Config{
		Meta: config.Meta{
			GroupVersionKind: gvk.VirtualService,
			Name:             "tls-route-vs",
			Namespace:        "default",
			Annotations: map[string]string{
				constants.InternalParentNames:    "TLSRoute/my-tls-route.default",
				constants.InternalRouteSemantics: constants.RouteSemanticsGateway,
			},
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{"example.com"},
			Gateways: []string{"istio-system/gateway"},
			Tls: []*networking.TLSRoute{
				{
					Match: []*networking.TLSMatchAttributes{
						{
							SniHosts: []string{"example.com"},
							Port:     443,
						},
					},
					Route: []*networking.RouteDestination{
						{
							Destination: &networking.Destination{
								Host: "backend.default.svc.cluster.local",
								Port: &networking.PortSelector{Number: 8080},
							},
						},
					},
				},
			},
		},
	}

	proxy := &model.Proxy{
		Labels: map[string]string{"istio": "ingressgateway"},
		Metadata: &model.NodeMetadata{
			Labels:    map[string]string{"istio": "ingressgateway"},
			Namespace: "istio-system",
		},
		Type: model.Router,
	}

	t.Run("tlsroute with tls terminate", func(t *testing.T) {
		o := xds.FakeOptions{
			ConfigString: tlsTerminateGateway,
			Configs:      []config.Config{tlsRouteVS},
		}
		s := xds.NewFakeDiscoveryServer(t, o)
		sim := simulation.NewSimulation(t, s, s.SetupProxy(proxy))
		sim.RunExpectations([]simulation.Expect{
			{
				Name: "tls terminate request",
				Call: simulation.Call{
					Port:       443,
					Protocol:   simulation.TCP,
					TLS:        simulation.TLS,
					HostHeader: "example.com",
				},
				Result: simulation.Result{
					ListenerMatched: "0.0.0.0_443",
					ClusterMatched:  "outbound|8080||backend.default.svc.cluster.local",
				},
			},
		})
	})

	t.Run("tlsroute with tls passthrough", func(t *testing.T) {
		o := xds.FakeOptions{
			ConfigString: tlsPassthroughGateway,
			Configs:      []config.Config{tlsRouteVS},
		}
		s := xds.NewFakeDiscoveryServer(t, o)
		sim := simulation.NewSimulation(t, s, s.SetupProxy(proxy))
		sim.RunExpectations([]simulation.Expect{
			{
				Name: "tls passthrough request",
				Call: simulation.Call{
					Port:       443,
					Protocol:   simulation.TCP,
					TLS:        simulation.TLS,
					HostHeader: "example.com",
				},
				Result: simulation.Result{
					ListenerMatched: "0.0.0.0_443",
					ClusterMatched:  "outbound|8080||backend.default.svc.cluster.local",
				},
			},
		})
	})

	// Mixed mode: both terminate and passthrough on same port with different hostnames
	t.Run("tlsroute mixed mode", func(t *testing.T) {
		mixedGateway := `
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - '*/terminate.example.com'
    port:
      name: tls-terminate
      number: 443
      protocol: TLS
    tls:
      mode: SIMPLE
      credentialName: sds-credential
  - hosts:
    - '*/passthrough.example.com'
    port:
      name: tls-passthrough
      number: 443
      protocol: TLS
    tls:
      mode: PASSTHROUGH
`
		terminateVS := config.Config{
			Meta: config.Meta{
				GroupVersionKind: gvk.VirtualService,
				Name:             "terminate-vs",
				Namespace:        "default",
				Annotations: map[string]string{
					constants.InternalParentNames:    "TLSRoute/terminate-route.default",
					constants.InternalRouteSemantics: constants.RouteSemanticsGateway,
				},
			},
			Spec: &networking.VirtualService{
				Hosts:    []string{"terminate.example.com"},
				Gateways: []string{"istio-system/gateway"},
				Tls: []*networking.TLSRoute{
					{
						Match: []*networking.TLSMatchAttributes{
							{
								SniHosts: []string{"terminate.example.com"},
								Port:     443,
							},
						},
						Route: []*networking.RouteDestination{
							{
								Destination: &networking.Destination{
									Host: "backend-terminate.default.svc.cluster.local",
									Port: &networking.PortSelector{Number: 8080},
								},
							},
						},
					},
				},
			},
		}
		passthroughVS := config.Config{
			Meta: config.Meta{
				GroupVersionKind: gvk.VirtualService,
				Name:             "passthrough-vs",
				Namespace:        "default",
				Annotations: map[string]string{
					constants.InternalParentNames:    "TLSRoute/passthrough-route.default",
					constants.InternalRouteSemantics: constants.RouteSemanticsGateway,
				},
			},
			Spec: &networking.VirtualService{
				Hosts:    []string{"passthrough.example.com"},
				Gateways: []string{"istio-system/gateway"},
				Tls: []*networking.TLSRoute{
					{
						Match: []*networking.TLSMatchAttributes{
							{
								SniHosts: []string{"passthrough.example.com"},
								Port:     443,
							},
						},
						Route: []*networking.RouteDestination{
							{
								Destination: &networking.Destination{
									Host: "backend-passthrough.default.svc.cluster.local",
									Port: &networking.PortSelector{Number: 443},
								},
							},
						},
					},
				},
			},
		}

		o := xds.FakeOptions{
			ConfigString: mixedGateway,
			Configs:      []config.Config{terminateVS, passthroughVS},
		}
		s := xds.NewFakeDiscoveryServer(t, o)
		sim := simulation.NewSimulation(t, s, s.SetupProxy(proxy))
		sim.RunExpectations([]simulation.Expect{
			{
				Name: "terminate route reaches terminate backend",
				Call: simulation.Call{
					Port:       443,
					Protocol:   simulation.TCP,
					TLS:        simulation.TLS,
					HostHeader: "terminate.example.com",
				},
				Result: simulation.Result{
					ListenerMatched: "0.0.0.0_443",
					ClusterMatched:  "outbound|8080||backend-terminate.default.svc.cluster.local",
				},
			},
			{
				Name: "passthrough route reaches passthrough backend",
				Call: simulation.Call{
					Port:       443,
					Protocol:   simulation.TCP,
					TLS:        simulation.TLS,
					HostHeader: "passthrough.example.com",
				},
				Result: simulation.Result{
					ListenerMatched: "0.0.0.0_443",
					ClusterMatched:  "outbound|443||backend-passthrough.default.svc.cluster.local",
				},
			},
		})
	})
}

func TestGatewayExternalSDSProviderSimulation(t *testing.T) {
	mc := mesh.DefaultMeshConfig()
	mc.ExtensionProviders = append(mc.ExtensionProviders, &meshconfig.MeshConfig_ExtensionProvider{
		Name: "my-sds-provider",
		Provider: &meshconfig.MeshConfig_ExtensionProvider_Sds{
			Sds: &meshconfig.MeshConfig_ExtensionProvider_SDSProvider{
				Name:    "my-sds-provider",
				Service: "sds-provider.sds-system.svc.cluster.local",
				Port:    8443,
			},
		},
	})

	configStr := `
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: external-sds-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: "sds://my-credential"
    hosts:
    - "secure.example.com"
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: secure-vs
  namespace: istio-system
spec:
  hosts:
  - "secure.example.com"
  gateways:
  - external-sds-gateway
  http:
  - route:
    - destination:
        host: backend.default.svc.cluster.local
        port:
          number: 80
---
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: sds-provider
  namespace: istio-system
spec:
  hosts:
  - "sds-provider.sds-system.svc.cluster.local"
  ports:
  - number: 8443
    name: grpc
    protocol: GRPC
  resolution: STATIC
  location: MESH_INTERNAL
  endpoints:
  - address: 10.0.0.100
`

	proxy := &model.Proxy{
		Labels: map[string]string{"istio": "ingressgateway"},
		Metadata: &model.NodeMetadata{
			Labels:    map[string]string{"istio": "ingressgateway"},
			Namespace: "istio-system",
		},
		Type: model.Router,
	}

	o := xds.FakeOptions{
		ConfigString: configStr,
		MeshConfig:   mc,
	}

	s := xds.NewFakeDiscoveryServer(t, o)
	sim := simulation.NewSimulation(t, s, s.SetupProxy(proxy))

	// Verify the listener was created
	l := xdstest.ExtractListener("0.0.0.0_443", sim.Listeners)
	if l == nil {
		t.Fatal("expected listener 0.0.0.0_443 to be created")
	}

	if len(l.FilterChains) == 0 {
		t.Fatal("expected at least one filter chain")
	}

	fc := l.FilterChains[0]
	if fc.GetTransportSocket() == nil {
		t.Fatal("expected transport socket for TLS")
	}

	tlsContext := xdstest.UnmarshalAny[auth.DownstreamTlsContext](t, fc.GetTransportSocket().GetTypedConfig())
	sdsConfigs := tlsContext.GetCommonTlsContext().GetTlsCertificateSdsSecretConfigs()
	if len(sdsConfigs) == 0 {
		t.Fatal("expected SDS secret configs")
	}

	sdsConfig := sdsConfigs[0]
	if sdsConfig.GetName() != "my-credential" {
		t.Errorf("expected SDS name %q, got %q", "my-credential", sdsConfig.GetName())
	}

	grpcServices := sdsConfig.GetSdsConfig().GetApiConfigSource().GetGrpcServices()
	if len(grpcServices) == 0 {
		t.Fatal("expected gRPC services in SDS config")
	}

	clusterName := grpcServices[0].GetEnvoyGrpc().GetClusterName()
	expectedCluster := "outbound|8443||sds-provider.sds-system.svc.cluster.local"
	if clusterName != expectedCluster {
		t.Errorf("expected cluster %q, got %q", expectedCluster, clusterName)
	}
}
