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

import (
	"fmt"
	"os"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	k8s "sigs.k8s.io/gateway-api/apis/v1alpha2"
	"sigs.k8s.io/yaml"

	"istio.io/istio/pilot/pkg/config/kube/crd"
	credentials "istio.io/istio/pilot/pkg/credentials/kube"
	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pilot/pkg/model/kstatus"
	"istio.io/istio/pilot/pkg/networking/core/v1alpha3"
	"istio.io/istio/pilot/test/util"
	"istio.io/istio/pkg/cluster"
	"istio.io/istio/pkg/config"
	crdvalidation "istio.io/istio/pkg/config/crd"
	"istio.io/istio/pkg/config/schema/gvk"
	"istio.io/istio/pkg/kube"
	"istio.io/istio/pkg/test"
	"istio.io/istio/pkg/test/util/assert"
	"istio.io/istio/pkg/util/sets"
)

var ports = []*model.Port{
	{
		Name:     "http",
		Port:     80,
		Protocol: "HTTP",
	},
	{
		Name:     "tcp",
		Port:     34000,
		Protocol: "TCP",
	},
}

var services = []*model.Service{
	{
		Attributes: model.ServiceAttributes{
			Name:      "istio-ingressgateway",
			Namespace: "istio-system",
			ClusterExternalAddresses: model.AddressMap{
				Addresses: map[cluster.ID][]string{
					"Kubernetes": {"1.2.3.4"},
				},
			},
		},
		Ports:    ports,
		Hostname: "istio-ingressgateway.istio-system.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "istio-system",
		},
		Ports:    ports,
		Hostname: "example.com",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "apple",
		},
		Ports:    ports,
		Hostname: "httpbin-apple.apple.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "banana",
		},
		Ports:    ports,
		Hostname: "httpbin-banana.banana.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-second.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-wildcard.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "foo-svc.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-other.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "example.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "echo.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "echo.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "cert",
		},
		Ports:    ports,
		Hostname: "httpbin.cert.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "service",
		},
		Ports:    ports,
		Hostname: "my-svc.service.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "google.com",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "allowed-1",
		},
		Ports:    ports,
		Hostname: "svc2.allowed-1.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "allowed-2",
		},
		Ports:    ports,
		Hostname: "svc2.allowed-2.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "allowed-1",
		},
		Ports:    ports,
		Hostname: "svc1.allowed-1.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "allowed-2",
		},
		Ports:    ports,
		Hostname: "svc3.allowed-2.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "svc4.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "group-namespace1",
		},
		Ports:    ports,
		Hostname: "httpbin.group-namespace1.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "group-namespace2",
		},
		Ports:    ports,
		Hostname: "httpbin.group-namespace2.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-zero.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "istio-system",
		},
		Ports:    ports,
		Hostname: "httpbin.istio-system.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-mirror.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-foo.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-alt.default.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "istio-system",
		},
		Ports:    ports,
		Hostname: "istiod.istio-system.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "istio-system",
		},
		Ports:    ports,
		Hostname: "istiod.istio-system.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "istio-system",
		},
		Ports:    ports,
		Hostname: "echo.istio-system.svc.domain.suffix",
	},
	{
		Attributes: model.ServiceAttributes{
			Namespace: "default",
		},
		Ports:    ports,
		Hostname: "httpbin-bad.default.svc.domain.suffix",
	},
}

var (
	// https://github.com/kubernetes/kubernetes/blob/v1.25.4/staging/src/k8s.io/kubectl/pkg/cmd/create/create_secret_tls_test.go#L31
	rsaCertPEM = `-----BEGIN CERTIFICATE-----
MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ
hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa
rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv
zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW
r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V
-----END CERTIFICATE-----
`
	rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo
k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G
6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N
MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW
SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T
xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi
D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g==
-----END RSA PRIVATE KEY-----
`

	secrets = []runtime.Object{
		&corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "my-cert-http",
				Namespace: "istio-system",
			},
			Data: map[string][]byte{
				"tls.crt": []byte(rsaCertPEM),
				"tls.key": []byte(rsaKeyPEM),
			},
		},
		&corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "cert",
				Namespace: "cert",
			},
			Data: map[string][]byte{
				"tls.crt": []byte(rsaCertPEM),
				"tls.key": []byte(rsaKeyPEM),
			},
		},
		&corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "malformed",
				Namespace: "istio-system",
			},
			Data: map[string][]byte{
				// nolint: lll
				// https://github.com/kubernetes-sigs/gateway-api/blob/d7f71d6b7df7e929ae299948973a693980afc183/conformance/tests/gateway-invalid-tls-certificateref.yaml#L87-L90
				// this certificate is invalid because contains an invalid pem (base64 of "Hello world"),
				// and the certificate and the key are identical
				"tls.crt": []byte("SGVsbG8gd29ybGQK"),
				"tls.key": []byte("SGVsbG8gd29ybGQK"),
			},
		},
	}
)

func TestConvertResources(t *testing.T) {
	validator := crdvalidation.NewIstioValidator(t)
	cases := []struct {
		name string
	}{
		{"http"},
		{"tcp"},
		{"tls"},
		{"mismatch"},
		{"weighted"},
		{"zero"},
		{"mesh"},
		{"invalid"},
		{"multi-gateway"},
		{"delegated"},
		{"route-binding"},
		{"reference-policy-tls"},
		{"reference-policy-service"},
		{"serviceentry"},
		{"eastwest"},
		{"alias"},
		{"mcs"},
		{"route-precedence"},
	}
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			input := readConfig(t, fmt.Sprintf("testdata/%s.yaml", tt.name), validator)
			// Setup a few preconfigured services
			instances := []*model.ServiceInstance{}
			for _, svc := range services {
				instances = append(instances, &model.ServiceInstance{
					Service:     svc,
					ServicePort: ports[0],
					Endpoint:    &model.IstioEndpoint{EndpointPort: 8080},
				}, &model.ServiceInstance{
					Service:     svc,
					ServicePort: ports[1],
					Endpoint:    &model.IstioEndpoint{},
				})
			}
			cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{
				Services:  services,
				Instances: instances,
			})
			kr := splitInput(t, input)
			kr.Context = NewGatewayContext(cg.PushContext())
			output := convertResources(kr)
			output.AllowedReferences = AllowedReferences{} // Not tested here
			output.ReferencedNamespaceKeys = nil           // Not tested here
			output.ResourceReferences = nil                // Not tested here

			// sort virtual services to make the order deterministic
			sort.Slice(output.VirtualService, func(i, j int) bool {
				return output.VirtualService[i].Namespace+"/"+output.VirtualService[i].Name < output.VirtualService[j].Namespace+"/"+output.VirtualService[j].Name
			})
			goldenFile := fmt.Sprintf("testdata/%s.yaml.golden", tt.name)
			res := append(output.Gateway, output.VirtualService...)
			util.CompareContent(t, marshalYaml(t, res), goldenFile)
			golden := splitOutput(readConfig(t, goldenFile, validator))

			// sort virtual services to make the order deterministic
			sort.Slice(golden.VirtualService, func(i, j int) bool {
				return golden.VirtualService[i].Namespace+"/"+golden.VirtualService[i].Name < golden.VirtualService[j].Namespace+"/"+golden.VirtualService[j].Name
			})

			assert.Equal(t, golden, output)

			outputStatus := getStatus(t, kr.GatewayClass, kr.Gateway, kr.HTTPRoute, kr.TLSRoute, kr.TCPRoute)
			goldenStatusFile := fmt.Sprintf("testdata/%s.status.yaml.golden", tt.name)
			if util.Refresh() {
				if err := os.WriteFile(goldenStatusFile, outputStatus, 0o644); err != nil {
					t.Fatal(err)
				}
			}
			goldenStatus, err := os.ReadFile(goldenStatusFile)
			if err != nil {
				t.Fatal(err)
			}
			if diff := cmp.Diff(string(goldenStatus), string(outputStatus)); diff != "" {
				t.Fatalf("Diff:\n%s", diff)
			}
		})
	}
}

func TestReferencePolicy(t *testing.T) {
	validator := crdvalidation.NewIstioValidator(t)
	type res struct {
		name, namespace string
		allowed         bool
	}
	cases := []struct {
		name         string
		config       string
		expectations []res
	}{
		{
			name: "simple",
			config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferenceGrant
metadata:
  name: allow-gateways-to-ref-secrets
  namespace: default
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: istio-system
  to:
  - group: ""
    kind: Secret
`,
			expectations: []res{
				// allow cross namespace
				{"kubernetes-gateway://default/wildcard-example-com-cert", "istio-system", true},
				// denied same namespace. We do not implicitly allow (in this code - higher level code does)
				{"kubernetes-gateway://default/wildcard-example-com-cert", "default", false},
				// denied namespace
				{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
			},
		},
		{
			name: "multiple in one",
			config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferenceGrant
metadata:
  name: allow-gateways-to-ref-secrets
  namespace: default
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: ns-1
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: ns-2
  to:
  - group: ""
    kind: Secret
`,
			expectations: []res{
				{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-1", true},
				{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-2", true},
				{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
			},
		},
		{
			name: "multiple",
			config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferenceGrant
metadata:
  name: ns1
  namespace: default
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: ns-1
  to:
  - group: ""
    kind: Secret
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferenceGrant
metadata:
  name: ns2
  namespace: default
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: ns-2
  to:
  - group: ""
    kind: Secret
`,
			expectations: []res{
				{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-1", true},
				{"kubernetes-gateway://default/wildcard-example-com-cert", "ns-2", true},
				{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
			},
		},
		{
			name: "same namespace",
			config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferenceGrant
metadata:
  name: allow-gateways-to-ref-secrets
  namespace: default
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: default
  to:
  - group: ""
    kind: Secret
`,
			expectations: []res{
				{"kubernetes-gateway://default/wildcard-example-com-cert", "istio-system", false},
				{"kubernetes-gateway://default/wildcard-example-com-cert", "default", true},
				{"kubernetes-gateway://default/wildcard-example-com-cert", "bad", false},
			},
		},
		{
			name: "same name",
			config: `apiVersion: gateway.networking.k8s.io/v1alpha2
kind: ReferenceGrant
metadata:
  name: allow-gateways-to-ref-secrets
  namespace: default
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: default
  to:
  - group: ""
    kind: Secret
    name: public
`,
			expectations: []res{
				{"kubernetes-gateway://default/public", "istio-system", false},
				{"kubernetes-gateway://default/public", "default", true},
				{"kubernetes-gateway://default/private", "default", false},
			},
		},
	}
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			input := readConfigString(t, tt.config, validator)
			cg := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{})
			kr := splitInput(t, input)
			kr.Context = NewGatewayContext(cg.PushContext())
			output := convertResources(kr)
			c := &Controller{
				state: output,
			}
			for _, sc := range tt.expectations {
				t.Run(fmt.Sprintf("%v/%v", sc.name, sc.namespace), func(t *testing.T) {
					got := c.SecretAllowed(sc.name, sc.namespace)
					if got != sc.allowed {
						t.Fatalf("expected allowed=%v, got allowed=%v", sc.allowed, got)
					}
				})
			}
		})
	}
}

func getStatus(t test.Failer, acfgs ...[]config.Config) []byte {
	cfgs := []config.Config{}
	for _, cl := range acfgs {
		cfgs = append(cfgs, cl...)
	}
	for i, c := range cfgs {
		c = c.DeepCopy()
		c.Spec = nil
		c.Labels = nil
		c.Annotations = nil
		if c.Status.(*kstatus.WrappedStatus) != nil {
			c.Status = c.Status.(*kstatus.WrappedStatus).Status
		}
		cfgs[i] = c
	}
	return timestampRegex.ReplaceAll(marshalYaml(t, cfgs), []byte("lastTransitionTime: fake"))
}

var timestampRegex = regexp.MustCompile(`lastTransitionTime:.*`)

func splitOutput(configs []config.Config) OutputResources {
	out := OutputResources{
		Gateway:        []config.Config{},
		VirtualService: []config.Config{},
	}
	for _, c := range configs {
		c.Domain = "domain.suffix"
		switch c.GroupVersionKind {
		case gvk.Gateway:
			out.Gateway = append(out.Gateway, c)
		case gvk.VirtualService:
			out.VirtualService = append(out.VirtualService, c)
		}
	}
	return out
}

func splitInput(t test.Failer, configs []config.Config) KubernetesResources {
	out := KubernetesResources{}
	namespaces := sets.New[string]()
	for _, c := range configs {
		namespaces.Insert(c.Namespace)
		switch c.GroupVersionKind {
		case gvk.GatewayClass:
			out.GatewayClass = append(out.GatewayClass, c)
		case gvk.KubernetesGateway:
			out.Gateway = append(out.Gateway, c)
		case gvk.HTTPRoute:
			out.HTTPRoute = append(out.HTTPRoute, c)
		case gvk.TCPRoute:
			out.TCPRoute = append(out.TCPRoute, c)
		case gvk.TLSRoute:
			out.TLSRoute = append(out.TLSRoute, c)
		case gvk.ReferenceGrant:
			out.ReferenceGrant = append(out.ReferenceGrant, c)
		}
	}
	out.Namespaces = map[string]*corev1.Namespace{}
	for ns := range namespaces {
		out.Namespaces[ns] = &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name: ns,
				Labels: map[string]string{
					"istio.io/test-name-part": strings.Split(ns, "-")[0],
				},
			},
		}
	}

	client := kube.NewFakeClient(secrets...)
	out.Credentials = credentials.NewCredentialsController(client, "")
	client.RunAndWait(test.NewStop(t))

	out.Domain = "domain.suffix"
	return out
}

func readConfig(t testing.TB, filename string, validator *crdvalidation.Validator) []config.Config {
	t.Helper()

	data, err := os.ReadFile(filename)
	if err != nil {
		t.Fatalf("failed to read input yaml file: %v", err)
	}
	return readConfigString(t, string(data), validator)
}

func readConfigString(t testing.TB, data string, validator *crdvalidation.Validator) []config.Config {
	if err := validator.ValidateCustomResourceYAML(data); err != nil {
		t.Error(err)
	}
	c, _, err := crd.ParseInputs(data)
	if err != nil {
		t.Fatalf("failed to parse CRD: %v", err)
	}
	return insertDefaults(c)
}

// insertDefaults sets default values that would be present when reading from Kubernetes but not from
// files
func insertDefaults(cfgs []config.Config) []config.Config {
	res := make([]config.Config, 0, len(cfgs))
	for _, c := range cfgs {
		switch c.GroupVersionKind {
		case gvk.GatewayClass:
			c.Status = kstatus.Wrap(&k8s.GatewayClassStatus{})
		case gvk.KubernetesGateway:
			c.Status = kstatus.Wrap(&k8s.GatewayStatus{})
		case gvk.HTTPRoute:
			c.Status = kstatus.Wrap(&k8s.HTTPRouteStatus{})
		case gvk.TCPRoute:
			c.Status = kstatus.Wrap(&k8s.TCPRouteStatus{})
		case gvk.TLSRoute:
			c.Status = kstatus.Wrap(&k8s.TLSRouteStatus{})
		}
		res = append(res, c)
	}
	return res
}

// Print as YAML
func marshalYaml(t test.Failer, cl []config.Config) []byte {
	t.Helper()
	result := []byte{}
	separator := []byte("---\n")
	for _, config := range cl {
		obj, err := crd.ConvertConfig(config)
		if err != nil {
			t.Fatalf("Could not decode %v: %v", config.Name, err)
		}
		bytes, err := yaml.Marshal(obj)
		if err != nil {
			t.Fatalf("Could not convert %v to YAML: %v", config, err)
		}
		result = append(result, bytes...)
		result = append(result, separator...)
	}
	return result
}

func TestHumanReadableJoin(t *testing.T) {
	tests := []struct {
		input []string
		want  string
	}{
		{[]string{"a"}, "a"},
		{[]string{"a", "b"}, "a and b"},
		{[]string{"a", "b", "c"}, "a, b, and c"},
	}
	for _, tt := range tests {
		t.Run(strings.Join(tt.input, "_"), func(t *testing.T) {
			if got := humanReadableJoin(tt.input); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("got %v, want %v", got, tt.want)
			}
		})
	}
}

func BenchmarkBuildHTTPVirtualServices(b *testing.B) {
	ports := []*model.Port{
		{
			Name:     "http",
			Port:     80,
			Protocol: "HTTP",
		},
		{
			Name:     "tcp",
			Port:     34000,
			Protocol: "TCP",
		},
	}
	ingressSvc := &model.Service{
		Attributes: model.ServiceAttributes{
			Name:      "istio-ingressgateway",
			Namespace: "istio-system",
			ClusterExternalAddresses: model.AddressMap{
				Addresses: map[cluster.ID][]string{
					"Kubernetes": {"1.2.3.4"},
				},
			},
		},
		Ports:    ports,
		Hostname: "istio-ingressgateway.istio-system.svc.domain.suffix",
	}
	altIngressSvc := &model.Service{
		Attributes: model.ServiceAttributes{
			Namespace: "istio-system",
		},
		Ports:    ports,
		Hostname: "example.com",
	}
	cg := v1alpha3.NewConfigGenTest(b, v1alpha3.TestOptions{
		Services: []*model.Service{ingressSvc, altIngressSvc},
		Instances: []*model.ServiceInstance{
			{Service: ingressSvc, ServicePort: ingressSvc.Ports[0], Endpoint: &model.IstioEndpoint{EndpointPort: 8080}},
			{Service: ingressSvc, ServicePort: ingressSvc.Ports[1], Endpoint: &model.IstioEndpoint{}},
			{Service: altIngressSvc, ServicePort: altIngressSvc.Ports[0], Endpoint: &model.IstioEndpoint{}},
			{Service: altIngressSvc, ServicePort: altIngressSvc.Ports[1], Endpoint: &model.IstioEndpoint{}},
		},
	})

	validator := crdvalidation.NewIstioValidator(b)
	input := readConfig(b, "testdata/benchmark-httproute.yaml", validator)
	kr := splitInput(b, input)
	kr.Context = NewGatewayContext(cg.PushContext())
	ctx := ConfigContext{
		KubernetesResources: kr,
		AllowedReferences:   convertReferencePolicies(kr),
	}
	_, gwMap, _ := convertGateways(ctx)
	ctx.GatewayReferences = gwMap

	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		// for gateway routes, build one VS per gateway+host
		gatewayRoutes := make(map[string]map[string]*config.Config)
		// for mesh routes, build one VS per namespace+host
		meshRoutes := make(map[string]map[string]*config.Config)
		for _, obj := range kr.HTTPRoute {
			buildHTTPVirtualServices(ctx, obj, gatewayRoutes, meshRoutes)
		}
	}
}
