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

import (
	"fmt"
	"reflect"
	"regexp"
	"sort"
	"sync"
	"testing"
	"time"

	"github.com/golang/protobuf/ptypes/wrappers"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	. "github.com/onsi/gomega"
	"go.uber.org/atomic"
	"google.golang.org/protobuf/testing/protocmp"
	"google.golang.org/protobuf/types/known/durationpb"
	"google.golang.org/protobuf/types/known/structpb"
	"k8s.io/apimachinery/pkg/types"

	extensions "istio.io/api/extensions/v1alpha1"
	meshconfig "istio.io/api/mesh/v1alpha1"
	networking "istio.io/api/networking/v1alpha3"
	securityBeta "istio.io/api/security/v1beta1"
	selectorpb "istio.io/api/type/v1beta1"
	"istio.io/istio/pilot/pkg/features"
	istionetworking "istio.io/istio/pilot/pkg/networking"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
	"istio.io/istio/pkg/config/host"
	"istio.io/istio/pkg/config/labels"
	"istio.io/istio/pkg/config/mesh"
	"istio.io/istio/pkg/config/schema/collections"
	"istio.io/istio/pkg/config/schema/gvk"
	"istio.io/istio/pkg/config/schema/kind"
	"istio.io/istio/pkg/config/visibility"
	"istio.io/istio/pkg/test"
	"istio.io/istio/pkg/test/util/assert"
	"istio.io/istio/pkg/util/sets"
)

func TestMergeUpdateRequest(t *testing.T) {
	push0 := &PushContext{}
	// trivially different push contexts just for testing
	push1 := &PushContext{ProxyStatus: make(map[string]map[string]ProxyPushStatus)}

	var t0 time.Time
	t1 := t0.Add(time.Minute)

	cases := []struct {
		name   string
		left   *PushRequest
		right  *PushRequest
		merged PushRequest
	}{
		{
			"left nil",
			nil,
			&PushRequest{Full: true},
			PushRequest{Full: true},
		},
		{
			"right nil",
			&PushRequest{Full: true},
			nil,
			PushRequest{Full: true},
		},
		{
			"simple merge",
			&PushRequest{
				Full:  true,
				Push:  push0,
				Start: t0,
				ConfigsUpdated: sets.Set[ConfigKey]{
					{Kind: kind.Kind(1), Namespace: "ns1"}: {},
				},
				Reason: []TriggerReason{ServiceUpdate, ServiceUpdate},
			},
			&PushRequest{
				Full:  false,
				Push:  push1,
				Start: t1,
				ConfigsUpdated: sets.Set[ConfigKey]{
					{Kind: kind.Kind(2), Namespace: "ns2"}: {},
				},
				Reason: []TriggerReason{EndpointUpdate},
			},
			PushRequest{
				Full:  true,
				Push:  push1,
				Start: t0,
				ConfigsUpdated: sets.Set[ConfigKey]{
					{Kind: kind.Kind(1), Namespace: "ns1"}: {},
					{Kind: kind.Kind(2), Namespace: "ns2"}: {},
				},
				Reason: []TriggerReason{ServiceUpdate, ServiceUpdate, EndpointUpdate},
			},
		},
		{
			"skip config type merge: one empty",
			&PushRequest{Full: true, ConfigsUpdated: nil},
			&PushRequest{Full: true, ConfigsUpdated: sets.Set[ConfigKey]{{
				Kind: kind.Kind(2),
			}: {}}},
			PushRequest{Full: true, ConfigsUpdated: nil, Reason: nil},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.left.CopyMerge(tt.right)
			if !reflect.DeepEqual(&tt.merged, got) {
				t.Fatalf("expected %v, got %v", &tt.merged, got)
			}
			got = tt.left.Merge(tt.right)
			if !reflect.DeepEqual(&tt.merged, got) {
				t.Fatalf("expected %v, got %v", &tt.merged, got)
			}
		})
	}
}

func TestConcurrentMerge(t *testing.T) {
	reqA := &PushRequest{Reason: make([]TriggerReason, 0, 100)}
	reqB := &PushRequest{Reason: []TriggerReason{ServiceUpdate, ProxyUpdate}}
	for i := 0; i < 50; i++ {
		go func() {
			reqA.CopyMerge(reqB)
		}()
	}
	if len(reqA.Reason) != 0 {
		t.Fatalf("reqA modified: %v", reqA.Reason)
	}
	if len(reqB.Reason) != 2 {
		t.Fatalf("reqB modified: %v", reqB.Reason)
	}
}

func TestEnvoyFilters(t *testing.T) {
	proxyVersionRegex := regexp.MustCompile(`1\.4.*`)
	envoyFilters := []*EnvoyFilterWrapper{
		{
			Name:             "ef1",
			workloadSelector: map[string]string{"app": "v1"},
			Patches: map[networking.EnvoyFilter_ApplyTo][]*EnvoyFilterConfigPatchWrapper{
				networking.EnvoyFilter_LISTENER: {
					{
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{
								ProxyVersion: "1\\.4.*",
							},
						},
						ProxyVersionRegex: proxyVersionRegex,
					},
				},
			},
		},
		{
			Name:             "ef2",
			workloadSelector: map[string]string{"app": "v1"},
			Patches: map[networking.EnvoyFilter_ApplyTo][]*EnvoyFilterConfigPatchWrapper{
				networking.EnvoyFilter_CLUSTER: {
					{
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{
								ProxyVersion: `1\\.4.*`,
							},
						},
						ProxyVersionRegex: proxyVersionRegex,
					},
				},
			},
		},
	}

	push := &PushContext{
		Mesh: &meshconfig.MeshConfig{
			RootNamespace: "istio-system",
		},
		envoyFiltersByNamespace: map[string][]*EnvoyFilterWrapper{
			"istio-system": envoyFilters,
			"test-ns":      envoyFilters,
		},
	}

	if !push.HasEnvoyFilters("ef1", "test-ns") {
		t.Errorf("Check presence of EnvoyFilter ef1 at test-ns got false, want true")
	}
	if push.HasEnvoyFilters("ef3", "test-ns") {
		t.Errorf("Check presence of EnvoyFilter ef3 at test-ns got true, want false")
	}

	cases := []struct {
		name                    string
		proxy                   *Proxy
		expectedListenerPatches int
		expectedClusterPatches  int
	}{
		{
			name: "proxy matches two envoyfilters",
			proxy: &Proxy{
				Labels:          map[string]string{"app": "v1"},
				Metadata:        &NodeMetadata{IstioVersion: "1.4.0", Labels: map[string]string{"app": "v1"}},
				ConfigNamespace: "test-ns",
			},
			expectedListenerPatches: 2,
			expectedClusterPatches:  2,
		},
		{
			name: "proxy in root namespace matches an envoyfilter",
			proxy: &Proxy{
				Labels:          map[string]string{"app": "v1"},
				Metadata:        &NodeMetadata{IstioVersion: "1.4.0", Labels: map[string]string{"app": "v1"}},
				ConfigNamespace: "istio-system",
			},
			expectedListenerPatches: 1,
			expectedClusterPatches:  1,
		},

		{
			name: "proxy matches no envoyfilter",
			proxy: &Proxy{
				Labels:          map[string]string{"app": "v2"},
				Metadata:        &NodeMetadata{IstioVersion: "1.4.0", Labels: map[string]string{"app": "v2"}},
				ConfigNamespace: "test-ns",
			},
			expectedListenerPatches: 0,
			expectedClusterPatches:  0,
		},
		{
			name: "proxy matches envoyfilter in root ns",
			proxy: &Proxy{
				Labels:          map[string]string{"app": "v1"},
				Metadata:        &NodeMetadata{IstioVersion: "1.4.0", Labels: map[string]string{"app": "v1"}},
				ConfigNamespace: "test-n2",
			},
			expectedListenerPatches: 1,
			expectedClusterPatches:  1,
		},
		{
			name: "proxy version matches no envoyfilters",
			proxy: &Proxy{
				Labels:          map[string]string{"app": "v1"},
				Metadata:        &NodeMetadata{IstioVersion: "1.3.0", Labels: map[string]string{"app": "v1"}},
				ConfigNamespace: "test-ns",
			},
			expectedListenerPatches: 0,
			expectedClusterPatches:  0,
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			filter := push.EnvoyFilters(tt.proxy)
			if filter == nil {
				if tt.expectedClusterPatches != 0 || tt.expectedListenerPatches != 0 {
					t.Errorf("Got no envoy filter")
				}
				return
			}
			if len(filter.Patches[networking.EnvoyFilter_CLUSTER]) != tt.expectedClusterPatches {
				t.Errorf("Expect %d envoy filter cluster patches, but got %d", tt.expectedClusterPatches, len(filter.Patches[networking.EnvoyFilter_CLUSTER]))
			}
			if len(filter.Patches[networking.EnvoyFilter_LISTENER]) != tt.expectedListenerPatches {
				t.Errorf("Expect %d envoy filter listener patches, but got %d", tt.expectedListenerPatches, len(filter.Patches[networking.EnvoyFilter_LISTENER]))
			}
		})
	}
}

func TestEnvoyFilterOrder(t *testing.T) {
	env := &Environment{}
	store := NewFakeStore()

	ctime := time.Now()

	envoyFilters := []config.Config{
		{
			Meta: config.Meta{Name: "default-priority", Namespace: "testns-1", GroupVersionKind: gvk.EnvoyFilter},
			Spec: &networking.EnvoyFilter{
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "default-priority", Namespace: "testns", GroupVersionKind: gvk.EnvoyFilter},
			Spec: &networking.EnvoyFilter{
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "b-medium-priority", Namespace: "testns-1", GroupVersionKind: gvk.EnvoyFilter, CreationTimestamp: ctime},
			Spec: &networking.EnvoyFilter{
				Priority: 10,
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "a-medium-priority", Namespace: "testns-1", GroupVersionKind: gvk.EnvoyFilter, CreationTimestamp: ctime},
			Spec: &networking.EnvoyFilter{
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "b-medium-priority", Namespace: "testns", GroupVersionKind: gvk.EnvoyFilter, CreationTimestamp: ctime},
			Spec: &networking.EnvoyFilter{
				Priority: 10,
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "a-medium-priority", Namespace: "testns", GroupVersionKind: gvk.EnvoyFilter, CreationTimestamp: ctime},
			Spec: &networking.EnvoyFilter{
				Priority: 10,
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "a-low-priority", Namespace: "testns", GroupVersionKind: gvk.EnvoyFilter, CreationTimestamp: time.Now()},
			Spec: &networking.EnvoyFilter{
				Priority: 20,
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "b-low-priority", Namespace: "testns", GroupVersionKind: gvk.EnvoyFilter, CreationTimestamp: ctime},
			Spec: &networking.EnvoyFilter{
				Priority: 20,
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "high-priority", Namespace: "testns", GroupVersionKind: gvk.EnvoyFilter},
			Spec: &networking.EnvoyFilter{
				Priority: -1,
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
		{
			Meta: config.Meta{Name: "super-high-priority", Namespace: "testns", GroupVersionKind: gvk.EnvoyFilter},
			Spec: &networking.EnvoyFilter{
				Priority: -10,
				ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
					{
						Patch: &networking.EnvoyFilter_Patch{},
						Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
							Proxy: &networking.EnvoyFilter_ProxyMatch{ProxyVersion: `foobar`},
						},
					},
				},
			},
		},
	}

	expectedns := []string{
		"testns/super-high-priority", "testns/high-priority", "testns/default-priority", "testns/a-medium-priority",
		"testns/b-medium-priority", "testns/b-low-priority", "testns/a-low-priority",
	}

	expectedns1 := []string{"testns-1/default-priority", "testns-1/a-medium-priority", "testns-1/b-medium-priority"}

	for _, cfg := range envoyFilters {
		_, _ = store.Create(cfg)
	}
	env.ConfigStore = store
	m := mesh.DefaultMeshConfig()
	env.Watcher = mesh.NewFixedWatcher(m)
	env.Init()

	// Init a new push context
	pc := NewPushContext()
	if err := pc.initEnvoyFilters(env); err != nil {
		t.Fatal(err)
	}
	gotns := make([]string, 0)
	for _, filter := range pc.envoyFiltersByNamespace["testns"] {
		gotns = append(gotns, filter.Keys()...)
	}
	gotns1 := make([]string, 0)
	for _, filter := range pc.envoyFiltersByNamespace["testns-1"] {
		gotns1 = append(gotns1, filter.Keys()...)
	}
	if !reflect.DeepEqual(expectedns, gotns) {
		t.Errorf("Envoy filters are not ordered as expected. expected: %v got: %v", expectedns, gotns)
	}
	if !reflect.DeepEqual(expectedns1, gotns1) {
		t.Errorf("Envoy filters are not ordered as expected. expected: %v got: %v", expectedns1, gotns1)
	}
}

func TestWasmPlugins(t *testing.T) {
	env := &Environment{}
	store := NewFakeStore()

	wasmPlugins := map[string]config.Config{
		"invalid-type": {
			Meta: config.Meta{Name: "invalid-type", Namespace: constants.IstioSystemNamespace, GroupVersionKind: gvk.WasmPlugin},
			Spec: &networking.DestinationRule{},
		},
		"invalid-url": {
			Meta: config.Meta{Name: "invalid-url", Namespace: constants.IstioSystemNamespace, GroupVersionKind: gvk.WasmPlugin},
			Spec: &extensions.WasmPlugin{
				Phase:    extensions.PluginPhase_AUTHN,
				Priority: &wrappers.Int64Value{Value: 5},
				Url:      "notavalid%%Url;",
			},
		},
		"authn-low-prio-all": {
			Meta: config.Meta{Name: "authn-low-prio-all", Namespace: "testns-1", GroupVersionKind: gvk.WasmPlugin},
			Spec: &extensions.WasmPlugin{
				Phase:    extensions.PluginPhase_AUTHN,
				Priority: &wrappers.Int64Value{Value: 10},
				Url:      "file:///etc/istio/filters/authn.wasm",
				PluginConfig: &structpb.Struct{
					Fields: map[string]*structpb.Value{
						"test": {
							Kind: &structpb.Value_StringValue{StringValue: "test"},
						},
					},
				},
				Sha256: "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2",
			},
		},
		"global-authn-low-prio-ingress": {
			Meta: config.Meta{Name: "global-authn-low-prio-ingress", Namespace: constants.IstioSystemNamespace, GroupVersionKind: gvk.WasmPlugin},
			Spec: &extensions.WasmPlugin{
				Phase:    extensions.PluginPhase_AUTHN,
				Priority: &wrappers.Int64Value{Value: 5},
				Selector: &selectorpb.WorkloadSelector{
					MatchLabels: map[string]string{
						"istio": "ingressgateway",
					},
				},
			},
		},
		"authn-med-prio-all": {
			Meta: config.Meta{Name: "authn-med-prio-all", Namespace: "testns-1", GroupVersionKind: gvk.WasmPlugin},
			Spec: &extensions.WasmPlugin{
				Phase:    extensions.PluginPhase_AUTHN,
				Priority: &wrappers.Int64Value{Value: 50},
			},
		},
		"global-authn-high-prio-app": {
			Meta: config.Meta{Name: "global-authn-high-prio-app", Namespace: constants.IstioSystemNamespace, GroupVersionKind: gvk.WasmPlugin},
			Spec: &extensions.WasmPlugin{
				Phase:    extensions.PluginPhase_AUTHN,
				Priority: &wrappers.Int64Value{Value: 1000},
				Selector: &selectorpb.WorkloadSelector{
					MatchLabels: map[string]string{
						"app": "productpage",
					},
				},
				Match: []*extensions.WasmPlugin_TrafficSelector{
					{
						Mode:  selectorpb.WorkloadMode_SERVER,
						Ports: []*selectorpb.PortSelector{{Number: 1234}},
					},
				},
			},
		},
		"global-authz-med-prio-app": {
			Meta: config.Meta{Name: "global-authz-med-prio-app", Namespace: constants.IstioSystemNamespace, GroupVersionKind: gvk.WasmPlugin},
			Spec: &extensions.WasmPlugin{
				Phase:    extensions.PluginPhase_AUTHZ,
				Priority: &wrappers.Int64Value{Value: 50},
				Selector: &selectorpb.WorkloadSelector{
					MatchLabels: map[string]string{
						"app": "productpage",
					},
				},
				Match: []*extensions.WasmPlugin_TrafficSelector{
					{
						Mode:  selectorpb.WorkloadMode_SERVER,
						Ports: []*selectorpb.PortSelector{{Number: 1235}},
					},
				},
			},
		},
		"authz-high-prio-ingress": {
			Meta: config.Meta{Name: "authz-high-prio-ingress", Namespace: "testns-2", GroupVersionKind: gvk.WasmPlugin},
			Spec: &extensions.WasmPlugin{
				Phase:    extensions.PluginPhase_AUTHZ,
				Priority: &wrappers.Int64Value{Value: 1000},
			},
		},
	}

	testCases := []struct {
		name               string
		node               *Proxy
		listenerInfo       WasmPluginListenerInfo
		expectedExtensions map[extensions.PluginPhase][]*WasmPluginWrapper
	}{
		{
			name:               "nil proxy",
			node:               nil,
			listenerInfo:       anyListener,
			expectedExtensions: nil,
		},
		{
			name: "nomatch",
			node: &Proxy{
				ConfigNamespace: "other",
				Metadata:        &NodeMetadata{},
			},
			listenerInfo:       anyListener,
			expectedExtensions: map[extensions.PluginPhase][]*WasmPluginWrapper{},
		},
		{
			name: "ingress",
			node: &Proxy{
				ConfigNamespace: "other",
				Labels: map[string]string{
					"istio": "ingressgateway",
				},
				Metadata: &NodeMetadata{
					Labels: map[string]string{
						"istio": "ingressgateway",
					},
				},
			},
			listenerInfo: anyListener,
			expectedExtensions: map[extensions.PluginPhase][]*WasmPluginWrapper{
				extensions.PluginPhase_AUTHN: {
					convertToWasmPluginWrapper(wasmPlugins["global-authn-low-prio-ingress"]),
				},
			},
		},
		{
			name: "ingress-testns-1",
			node: &Proxy{
				ConfigNamespace: "testns-1",
				Labels: map[string]string{
					"istio": "ingressgateway",
				},
				Metadata: &NodeMetadata{
					Labels: map[string]string{
						"istio": "ingressgateway",
					},
				},
			},
			listenerInfo: anyListener,
			expectedExtensions: map[extensions.PluginPhase][]*WasmPluginWrapper{
				extensions.PluginPhase_AUTHN: {
					convertToWasmPluginWrapper(wasmPlugins["authn-med-prio-all"]),
					convertToWasmPluginWrapper(wasmPlugins["authn-low-prio-all"]),
					convertToWasmPluginWrapper(wasmPlugins["global-authn-low-prio-ingress"]),
				},
			},
		},
		{
			name: "testns-2",
			node: &Proxy{
				ConfigNamespace: "testns-2",
				Labels: map[string]string{
					"app": "productpage",
				},
				Metadata: &NodeMetadata{
					Labels: map[string]string{
						"app": "productpage",
					},
				},
			},
			listenerInfo: anyListener,
			expectedExtensions: map[extensions.PluginPhase][]*WasmPluginWrapper{
				extensions.PluginPhase_AUTHN: {
					convertToWasmPluginWrapper(wasmPlugins["global-authn-high-prio-app"]),
				},
				extensions.PluginPhase_AUTHZ: {
					convertToWasmPluginWrapper(wasmPlugins["authz-high-prio-ingress"]),
					convertToWasmPluginWrapper(wasmPlugins["global-authz-med-prio-app"]),
				},
			},
		},
		{
			// Detailed tests regarding TrafficSelector are in extension_test.go
			// Just test the integrity here.
			// This testcase is identical with "testns-2", but `listenerInfo`` is specified.
			// 1. `global-authn-high-prio-app` matched, because it has a port matching clause with "1234"
			// 2. `authz-high-prio-ingress` matched, because it does not have any `match` clause
			// 3. `global-authz-med-prio-app` not matched, because it has a port matching clause with "1235"
			name: "testns-2-with-port-match",
			node: &Proxy{
				ConfigNamespace: "testns-2",
				Labels: map[string]string{
					"app": "productpage",
				},
				Metadata: &NodeMetadata{
					Labels: map[string]string{
						"app": "productpage",
					},
				},
			},
			listenerInfo: WasmPluginListenerInfo{
				Port:  1234,
				Class: istionetworking.ListenerClassSidecarInbound,
			},
			expectedExtensions: map[extensions.PluginPhase][]*WasmPluginWrapper{
				extensions.PluginPhase_AUTHN: {
					convertToWasmPluginWrapper(wasmPlugins["global-authn-high-prio-app"]),
				},
				extensions.PluginPhase_AUTHZ: {
					convertToWasmPluginWrapper(wasmPlugins["authz-high-prio-ingress"]),
				},
			},
		},
	}

	for _, config := range wasmPlugins {
		store.Create(config)
	}
	env.ConfigStore = store
	m := mesh.DefaultMeshConfig()
	env.Watcher = mesh.NewFixedWatcher(m)
	env.Init()

	// Init a new push context
	pc := NewPushContext()
	pc.Mesh = m
	if err := pc.initWasmPlugins(env); err != nil {
		t.Fatal(err)
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			result := pc.WasmPluginsByListenerInfo(tc.node, tc.listenerInfo)
			if !reflect.DeepEqual(tc.expectedExtensions, result) {
				t.Errorf("WasmPlugins did not match expectations\n\ngot: %v\n\nexpected: %v", result, tc.expectedExtensions)
			}
		})
	}
}

func TestServiceIndex(t *testing.T) {
	g := NewWithT(t)
	env := NewEnvironment()
	env.ConfigStore = NewFakeStore()
	env.ServiceDiscovery = &localServiceDiscovery{
		services: []*Service{
			{
				Hostname: "svc-unset",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Namespace: "test1",
				},
			},
			{
				Hostname: "svc-public",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Namespace: "test1",
					ExportTo:  map[visibility.Instance]bool{visibility.Public: true},
				},
			},
			{
				Hostname: "svc-private",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Namespace: "test1",
					ExportTo:  map[visibility.Instance]bool{visibility.Private: true},
				},
			},
			{
				Hostname: "svc-none",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Namespace: "test1",
					ExportTo:  map[visibility.Instance]bool{visibility.None: true},
				},
			},
			{
				Hostname: "svc-namespace",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Namespace: "test1",
					ExportTo:  map[visibility.Instance]bool{"namespace": true},
				},
			},
		},
		serviceInstances: []*ServiceInstance{{
			Endpoint: &IstioEndpoint{
				Address:      "192.168.1.2",
				EndpointPort: 8000,
				TLSMode:      DisabledTLSModeLabel,
			},
		}},
	}
	m := mesh.DefaultMeshConfig()
	env.Watcher = mesh.NewFixedWatcher(m)
	env.Init()

	// Init a new push context
	pc := NewPushContext()
	if err := pc.InitContext(env, nil, nil); err != nil {
		t.Fatal(err)
	}
	si := pc.ServiceIndex

	// Should have all 5 services
	g.Expect(si.instancesByPort).To(HaveLen(5))
	g.Expect(si.HostnameAndNamespace).To(HaveLen(5))

	// Should just have "namespace"
	g.Expect(si.exportedToNamespace).To(HaveLen(1))
	g.Expect(serviceNames(si.exportedToNamespace["namespace"])).To(Equal([]string{"svc-namespace"}))

	g.Expect(serviceNames(si.public)).To(Equal([]string{"svc-public", "svc-unset"}))

	// Should just have "test1"
	g.Expect(si.privateByNamespace).To(HaveLen(1))
	g.Expect(serviceNames(si.privateByNamespace["test1"])).To(Equal([]string{"svc-private"}))
}

func TestIsServiceVisible(t *testing.T) {
	targetNamespace := "foo"
	cases := []struct {
		name        string
		pushContext *PushContext
		service     *Service
		expect      bool
	}{
		{
			name: "service whose namespace is foo has no exportTo map with global private",
			pushContext: &PushContext{
				exportToDefaults: exportToDefaults{
					service: map[visibility.Instance]bool{
						visibility.Private: true,
					},
				},
			},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "foo",
				},
			},
			expect: true,
		},
		{
			name: "service whose namespace is bar has no exportTo map with global private",
			pushContext: &PushContext{
				exportToDefaults: exportToDefaults{
					service: map[visibility.Instance]bool{
						visibility.Private: true,
					},
				},
			},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
				},
			},
			expect: false,
		},
		{
			name: "service whose namespace is bar has no exportTo map with global public",
			pushContext: &PushContext{
				exportToDefaults: exportToDefaults{
					service: map[visibility.Instance]bool{
						visibility.Public: true,
					},
				},
			},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
				},
			},
			expect: true,
		},
		{
			name:        "service whose namespace is foo has exportTo map with private",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "foo",
					ExportTo: map[visibility.Instance]bool{
						visibility.Private: true,
					},
				},
			},
			expect: true,
		},
		{
			name:        "service whose namespace is bar has exportTo map with private",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
					ExportTo: map[visibility.Instance]bool{
						visibility.Private: true,
					},
				},
			},
			expect: false,
		},
		{
			name:        "service whose namespace is bar has exportTo map with public",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
					ExportTo: map[visibility.Instance]bool{
						visibility.Public: true,
					},
				},
			},
			expect: true,
		},
		{
			name:        "service whose namespace is bar has exportTo map with specific namespace foo",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
					ExportTo: map[visibility.Instance]bool{
						visibility.Instance("foo"): true,
					},
				},
			},
			expect: true,
		},
		{
			name:        "service whose namespace is bar has exportTo map with specific namespace baz",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
					ExportTo: map[visibility.Instance]bool{
						visibility.Instance("baz"): true,
					},
				},
			},
			expect: false,
		},
		{
			name:        "service visible to none",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
					ExportTo: map[visibility.Instance]bool{
						visibility.None: true,
					},
				},
			},
			expect: false,
		},
		{
			name:        "service has both public visibility and none visibility",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
					ExportTo: map[visibility.Instance]bool{
						visibility.Public: true,
						visibility.None:   true,
					},
				},
			},
			expect: true,
		},
		{
			name:        "service has both none visibility and private visibility",
			pushContext: &PushContext{},
			service: &Service{
				Attributes: ServiceAttributes{
					Namespace: "bar",
					ExportTo: map[visibility.Instance]bool{
						visibility.Private: true,
						visibility.None:    true,
					},
				},
			},
			expect: false,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			isVisible := c.pushContext.IsServiceVisible(c.service, targetNamespace)

			g := NewWithT(t)
			g.Expect(isVisible).To(Equal(c.expect))
		})
	}
}

func serviceNames(svcs []*Service) []string {
	var s []string
	for _, ss := range svcs {
		s = append(s, string(ss.Hostname))
	}
	sort.Strings(s)
	return s
}

func TestInitPushContext(t *testing.T) {
	env := NewEnvironment()
	configStore := NewFakeStore()
	_, _ = configStore.Create(config.Config{
		Meta: config.Meta{
			Name:             "rule1",
			Namespace:        "test1",
			GroupVersionKind: gvk.VirtualService,
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{"rule1.com"},
			ExportTo: []string{".", "ns1"},
		},
	})
	_, _ = configStore.Create(config.Config{
		Meta: config.Meta{
			Name:             "rule1",
			Namespace:        "test1",
			GroupVersionKind: gvk.DestinationRule,
		},
		Spec: &networking.DestinationRule{
			ExportTo: []string{".", "ns1"},
		},
	})
	_, _ = configStore.Create(config.Config{
		Meta: config.Meta{
			Name:             "default",
			Namespace:        "istio-system",
			GroupVersionKind: gvk.Sidecar,
		},
		Spec: &networking.Sidecar{
			Egress: []*networking.IstioEgressListener{
				{Hosts: []string{"test1/*"}},
			},
		},
	})

	env.ConfigStore = configStore
	env.ServiceDiscovery = &localServiceDiscovery{
		services: []*Service{
			{
				Hostname: "svc1",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Namespace: "test1",
				},
			},
			{
				Hostname: "svc2",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Namespace: "test1",
					ExportTo:  map[visibility.Instance]bool{visibility.Public: true},
				},
			},
		},
		serviceInstances: []*ServiceInstance{{
			Endpoint: &IstioEndpoint{
				Address:      "192.168.1.2",
				EndpointPort: 8000,
				TLSMode:      DisabledTLSModeLabel,
			},
		}},
	}
	m := mesh.DefaultMeshConfig()
	env.Watcher = mesh.NewFixedWatcher(m)
	env.Init()

	// Init a new push context
	old := NewPushContext()
	if err := old.InitContext(env, nil, nil); err != nil {
		t.Fatal(err)
	}

	// Create a new one, copying from the old one
	// Pass a ConfigsUpdated otherwise we would just copy it directly
	newPush := NewPushContext()
	if err := newPush.InitContext(env, old, &PushRequest{
		ConfigsUpdated: sets.Set[ConfigKey]{
			{Kind: kind.Secret}: {},
		},
	}); err != nil {
		t.Fatal(err)
	}

	// Check to ensure the update is identical to the old one
	// There is probably a better way to do this.
	diff := cmp.Diff(old, newPush,
		// Allow looking into exported fields for parts of push context
		cmp.AllowUnexported(PushContext{}, exportToDefaults{}, serviceIndex{}, virtualServiceIndex{},
			destinationRuleIndex{}, gatewayIndex{}, consolidatedDestRules{}, IstioEgressListenerWrapper{}, SidecarScope{},
			AuthenticationPolicies{}, NetworkManager{}, sidecarIndex{}, Telemetries{}, ProxyConfigs{}, ConsolidatedDestRule{},
			ClusterLocalHosts{}),
		// These are not feasible/worth comparing
		cmpopts.IgnoreTypes(sync.RWMutex{}, localServiceDiscovery{}, FakeStore{}, atomic.Bool{}, sync.Mutex{}),
		cmpopts.IgnoreInterfaces(struct{ mesh.Holder }{}),
		protocmp.Transform(),
	)
	if diff != "" {
		t.Fatalf("Push context had a diff after update: %v", diff)
	}
}

func TestSidecarScope(t *testing.T) {
	ps := NewPushContext()
	env := &Environment{Watcher: mesh.NewFixedWatcher(&meshconfig.MeshConfig{RootNamespace: "istio-system"})}
	ps.Mesh = env.Mesh()
	ps.ServiceIndex.HostnameAndNamespace["svc1.default.cluster.local"] = map[string]*Service{"default": nil}
	ps.ServiceIndex.HostnameAndNamespace["svc2.nosidecar.cluster.local"] = map[string]*Service{"nosidecar": nil}
	ps.ServiceIndex.HostnameAndNamespace["svc3.istio-system.cluster.local"] = map[string]*Service{"istio-system": nil}

	configStore := NewFakeStore()
	sidecarWithWorkloadSelector := &networking.Sidecar{
		WorkloadSelector: &networking.WorkloadSelector{
			Labels: map[string]string{"app": "foo"},
		},
		Egress: []*networking.IstioEgressListener{
			{
				Hosts: []string{"default/*"},
			},
		},
		OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{},
	}
	sidecarWithoutWorkloadSelector := &networking.Sidecar{
		Egress: []*networking.IstioEgressListener{
			{
				Hosts: []string{"default/*"},
			},
		},
		OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{},
	}
	configWithWorkloadSelector := config.Config{
		Meta: config.Meta{
			GroupVersionKind: collections.IstioNetworkingV1Alpha3Sidecars.Resource().GroupVersionKind(),
			Name:             "foo",
			Namespace:        "default",
		},
		Spec: sidecarWithWorkloadSelector,
	}
	rootConfig := config.Config{
		Meta: config.Meta{
			GroupVersionKind: collections.IstioNetworkingV1Alpha3Sidecars.Resource().GroupVersionKind(),
			Name:             "global",
			Namespace:        constants.IstioSystemNamespace,
		},
		Spec: sidecarWithoutWorkloadSelector,
	}
	_, _ = configStore.Create(configWithWorkloadSelector)
	_, _ = configStore.Create(rootConfig)

	env.ConfigStore = configStore
	if err := ps.initSidecarScopes(env); err != nil {
		t.Fatalf("init sidecar scope failed: %v", err)
	}
	cases := []struct {
		proxy    *Proxy
		labels   labels.Instance
		sidecar  string
		describe string
	}{
		{
			proxy:    &Proxy{Type: SidecarProxy, ConfigNamespace: "default"},
			labels:   labels.Instance{"app": "foo"},
			sidecar:  "default/foo",
			describe: "match local sidecar",
		},
		{
			proxy:    &Proxy{Type: SidecarProxy, ConfigNamespace: "default"},
			labels:   labels.Instance{"app": "bar"},
			sidecar:  "default/global",
			describe: "no match local sidecar",
		},
		{
			proxy:    &Proxy{Type: SidecarProxy, ConfigNamespace: "nosidecar"},
			labels:   labels.Instance{"app": "bar"},
			sidecar:  "nosidecar/global",
			describe: "no sidecar",
		},
		{
			proxy:    &Proxy{Type: Router, ConfigNamespace: "istio-system"},
			labels:   labels.Instance{"app": "istio-gateway"},
			sidecar:  "istio-system/default-sidecar",
			describe: "gateway sidecar scope",
		},
	}
	for _, c := range cases {
		t.Run(c.describe, func(t *testing.T) {
			scope := ps.getSidecarScope(c.proxy, c.labels)
			if c.sidecar != scopeToSidecar(scope) {
				t.Errorf("should get sidecar %s but got %s", c.sidecar, scopeToSidecar(scope))
			}
		})
	}
}

func TestRootSidecarScopePropagation(t *testing.T) {
	rootNS := "istio-system"
	defaultNS := "default"
	otherNS := "foo"

	verifyServices := func(sidecarScopeEnabled bool, desc string, ns string, push *PushContext) {
		t.Run(desc, func(t *testing.T) {
			scScope := push.getSidecarScope(&Proxy{Type: SidecarProxy, ConfigNamespace: ns}, nil)
			services := scScope.Services()
			numSvc := 0
			svcList := []string{}
			for _, service := range services {
				svcName := service.Attributes.Name
				svcNS := service.Attributes.Namespace
				if svcNS != ns && svcNS != rootNS {
					numSvc++
				}
				svcList = append(svcList, fmt.Sprintf("%v.%v.cluster.local", svcName, svcNS))
			}
			if sidecarScopeEnabled && numSvc > 0 {
				t.Fatalf("For namespace:%v, should not see services from other ns. Instead got these services: %v", ns, svcList)
			}
		})
	}

	env := NewEnvironment()
	configStore := NewFakeStore()

	m := mesh.DefaultMeshConfig()
	env.Watcher = mesh.NewFixedWatcher(m)

	env.ServiceDiscovery = &localServiceDiscovery{
		services: []*Service{
			{
				Hostname: "svc1",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Name:      "svc1",
					Namespace: defaultNS,
				},
			},
			{
				Hostname: "svc2",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Name:      "svc2",
					Namespace: otherNS,
				},
			},
			{
				Hostname: "svc3",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Name:      "svc3",
					Namespace: otherNS,
				},
			},
			{
				Hostname: "svc4",
				Ports:    allPorts,
				Attributes: ServiceAttributes{
					Name:      "svc4",
					Namespace: rootNS,
				},
			},
		},
	}
	env.Init()
	globalSidecar := &networking.Sidecar{
		Egress: []*networking.IstioEgressListener{
			{
				Hosts: []string{"./*", fmt.Sprintf("%v/*", rootNS)},
			},
		},
		OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{},
	}
	rootConfig := config.Config{
		Meta: config.Meta{
			GroupVersionKind: collections.IstioNetworkingV1Alpha3Sidecars.Resource().GroupVersionKind(),
			Name:             "global",
			Namespace:        constants.IstioSystemNamespace,
		},
		Spec: globalSidecar,
	}

	_, _ = configStore.Create(rootConfig)
	env.ConfigStore = configStore

	testDesc := "Testing root SidecarScope for ns:%v enabled when %v is called."
	when := "createNewContext"
	newPush := NewPushContext()
	newPush.Mesh = env.Mesh()
	newPush.InitContext(env, nil, nil)
	verifyServices(true, fmt.Sprintf(testDesc, otherNS, when), otherNS, newPush)
	verifyServices(true, fmt.Sprintf(testDesc, defaultNS, when), defaultNS, newPush)

	oldPush := newPush
	newPush = NewPushContext()
	newPush.Mesh = env.Mesh()
	svcName := "svc6.foo.cluster.local"
	if err := newPush.InitContext(env, oldPush, &PushRequest{
		ConfigsUpdated: sets.Set[ConfigKey]{
			{Kind: kind.Service, Name: svcName, Namespace: "foo"}: {},
		},
		Reason: nil,
		Full:   true,
	}); err != nil {
		t.Fatal(err)
	}
	when = "updateContext(with no changes)"
	verifyServices(true, fmt.Sprintf(testDesc, otherNS, when), otherNS, newPush)
	verifyServices(true, fmt.Sprintf(testDesc, defaultNS, when), defaultNS, newPush)
}

func TestBestEffortInferServiceMTLSMode(t *testing.T) {
	const partialNS string = "partial"
	const wholeNS string = "whole"
	ps := NewPushContext()
	env := &Environment{Watcher: mesh.NewFixedWatcher(&meshconfig.MeshConfig{RootNamespace: "istio-system"})}
	sd := &localServiceDiscovery{}
	env.ServiceDiscovery = sd
	ps.Mesh = env.Mesh()

	configStore := NewFakeStore()

	// Add beta policies
	_, _ = configStore.Create(*createTestPeerAuthenticationResource("default", wholeNS, time.Now(), nil, securityBeta.PeerAuthentication_MutualTLS_STRICT))
	// workload level beta policy.
	_, _ = configStore.Create(*createTestPeerAuthenticationResource("workload-beta-policy", partialNS, time.Now(), &selectorpb.WorkloadSelector{
		MatchLabels: map[string]string{
			"app":     "httpbin",
			"version": "v1",
		},
	}, securityBeta.PeerAuthentication_MutualTLS_DISABLE))

	env.ConfigStore = configStore
	if err := ps.initAuthnPolicies(env); err != nil {
		t.Fatalf("init authn policies failed: %v", err)
	}

	instancePlainText := &ServiceInstance{
		Endpoint: &IstioEndpoint{
			Address:      "192.168.1.2",
			EndpointPort: 1000000,
			TLSMode:      DisabledTLSModeLabel,
		},
	}

	cases := []struct {
		name              string
		serviceNamespace  string
		servicePort       int
		serviceResolution Resolution
		serviceInstances  []*ServiceInstance
		wanted            MutualTLSMode
	}{
		{
			name:              "from namespace policy",
			serviceNamespace:  wholeNS,
			servicePort:       80,
			serviceResolution: ClientSideLB,
			wanted:            MTLSStrict,
		},
		{
			name:              "from mesh default",
			serviceNamespace:  partialNS,
			servicePort:       80,
			serviceResolution: ClientSideLB,
			wanted:            MTLSPermissive,
		},
		{
			name:              "headless service with no instances found yet",
			serviceNamespace:  partialNS,
			servicePort:       80,
			serviceResolution: Passthrough,
			wanted:            MTLSDisable,
		},
		{
			name:              "headless service with instances",
			serviceNamespace:  partialNS,
			servicePort:       80,
			serviceResolution: Passthrough,
			serviceInstances:  []*ServiceInstance{instancePlainText},
			wanted:            MTLSDisable,
		},
	}

	serviceName := host.Name("some-service")
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			service := &Service{
				Hostname:   host.Name(fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, tc.serviceNamespace)),
				Resolution: tc.serviceResolution,
				Attributes: ServiceAttributes{Namespace: tc.serviceNamespace},
			}
			// Intentionally use the externalService with the same name and namespace for test, though
			// these attributes don't matter.
			externalService := &Service{
				Hostname:     host.Name(fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, tc.serviceNamespace)),
				Resolution:   tc.serviceResolution,
				Attributes:   ServiceAttributes{Namespace: tc.serviceNamespace},
				MeshExternal: true,
			}

			sd.serviceInstances = tc.serviceInstances
			port := &Port{
				Port: tc.servicePort,
			}
			if got := ps.BestEffortInferServiceMTLSMode(nil, service, port); got != tc.wanted {
				t.Fatalf("want %s, but got %s", tc.wanted, got)
			}
			if got := ps.BestEffortInferServiceMTLSMode(nil, externalService, port); got != MTLSUnknown {
				t.Fatalf("MTLS mode for external service should always be %s, but got %s", MTLSUnknown, got)
			}
		})
	}
}

func scopeToSidecar(scope *SidecarScope) string {
	if scope == nil {
		return ""
	}
	return scope.Namespace + "/" + scope.Name
}

func TestSetDestinationRuleInheritance(t *testing.T) {
	test.SetForTest(t, &features.EnableDestinationRuleInheritance, true)
	ps := NewPushContext()
	ps.Mesh = &meshconfig.MeshConfig{RootNamespace: "istio-system"}
	testhost := "httpbin.org"
	meshDestinationRule := config.Config{
		Meta: config.Meta{
			Name:      "meshRule",
			Namespace: ps.Mesh.RootNamespace,
		},
		Spec: &networking.DestinationRule{
			TrafficPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 1},
						MaxConnections: 111,
					},
				},
				Tls: &networking.ClientTLSSettings{
					Mode:              networking.ClientTLSSettings_MUTUAL,
					ClientCertificate: "/etc/certs/myclientcert.pem",
					PrivateKey:        "/etc/certs/client_private_key.pem",
					CaCertificates:    "/etc/certs/rootcacerts.pem",
				},
			},
		},
	}
	nsDestinationRule := config.Config{
		Meta: config.Meta{
			Name:      "nsRule",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			TrafficPolicy: &networking.TrafficPolicy{
				OutlierDetection: &networking.OutlierDetection{
					ConsecutiveGatewayErrors: &wrappers.UInt32Value{Value: 222},
					Interval:                 &durationpb.Duration{Seconds: 22},
				},
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 2,
					},
				},
			},
		},
	}
	svcDestinationRule := config.Config{
		Meta: config.Meta{
			Name:      "svcRule",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			Host: testhost,
			TrafficPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 33,
					},
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 33},
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					Consecutive_5XxErrors: &wrappers.UInt32Value{Value: 3},
				},
				Tls: &networking.ClientTLSSettings{
					Mode: networking.ClientTLSSettings_SIMPLE,
				},
			},
		},
	}
	destinationRuleNamespace2 := config.Config{
		Meta: config.Meta{
			Name:      "svcRule2",
			Namespace: "test2",
		},
		Spec: &networking.DestinationRule{
			Host: testhost,
			Subsets: []*networking.Subset{
				{
					Name: "subset1",
				},
				{
					Name: "subset2",
				},
			},
		},
	}
	workloadSpecificDrNamespace2 := config.Config{
		Meta: config.Meta{
			Name:      "drRule2",
			Namespace: "test2",
		},
		Spec: &networking.DestinationRule{
			Host: testhost,
			WorkloadSelector: &selectorpb.WorkloadSelector{
				MatchLabels: map[string]string{"app": "app1"},
			},
			TrafficPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 33,
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					Consecutive_5XxErrors: &wrappers.UInt32Value{Value: 3},
				},
			},
		},
	}
	testCases := []struct {
		name               string
		proxyNs            string
		serviceNs          string
		serviceHostname    string
		expectedConfig     string
		expectedSourceRule []types.NamespacedName
		expectedPolicy     *networking.TrafficPolicy
	}{
		{
			name:            "merge mesh+namespace+service DR",
			proxyNs:         "test",
			serviceNs:       "test",
			serviceHostname: testhost,
			expectedConfig:  "svcRule",
			expectedSourceRule: []types.NamespacedName{
				{Namespace: "istio-system", Name: "meshRule"},
				{Namespace: "test", Name: "nsRule"},
				{Namespace: "test", Name: "svcRule"},
			},
			expectedPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 33,
					},
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 33},
						MaxConnections: 111,
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					Consecutive_5XxErrors:    &wrappers.UInt32Value{Value: 3},
					ConsecutiveGatewayErrors: &wrappers.UInt32Value{Value: 222},
					Interval:                 &durationpb.Duration{Seconds: 22},
				},
				Tls: &networking.ClientTLSSettings{
					Mode: networking.ClientTLSSettings_SIMPLE,
				},
			},
		},
		{
			name:            "merge mesh+service DR",
			proxyNs:         "test2",
			serviceNs:       "test2",
			serviceHostname: testhost,
			expectedConfig:  "svcRule2",
			expectedSourceRule: []types.NamespacedName{
				{Namespace: "istio-system", Name: "meshRule"},
				{Namespace: "test2", Name: "svcRule2"},
			},
			expectedPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 1},
						MaxConnections: 111,
					},
				},
				Tls: &networking.ClientTLSSettings{
					Mode:              networking.ClientTLSSettings_MUTUAL,
					ClientCertificate: "/etc/certs/myclientcert.pem",
					PrivateKey:        "/etc/certs/client_private_key.pem",
					CaCertificates:    "/etc/certs/rootcacerts.pem",
				},
			},
		},
		{
			name:            "merge mesh+workloadselector DR",
			proxyNs:         "test2",
			serviceNs:       "test2",
			serviceHostname: testhost,
			expectedConfig:  "drRule2",
			expectedSourceRule: []types.NamespacedName{
				{Namespace: "istio-system", Name: "meshRule"},
				{Namespace: "test2", Name: "drRule2"},
				{Namespace: "test2", Name: "svcRule2"},
			},
			expectedPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 33,
					},
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 1},
						MaxConnections: 111,
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					Consecutive_5XxErrors: &wrappers.UInt32Value{Value: 3},
				},
				Tls: &networking.ClientTLSSettings{
					Mode:              networking.ClientTLSSettings_MUTUAL,
					ClientCertificate: "/etc/certs/myclientcert.pem",
					PrivateKey:        "/etc/certs/client_private_key.pem",
					CaCertificates:    "/etc/certs/rootcacerts.pem",
				},
			},
		},
		{
			name:            "unknown host returns merged mesh+namespace",
			proxyNs:         "test",
			serviceNs:       "test",
			serviceHostname: "unknown.host",
			expectedConfig:  "nsRule",
			expectedSourceRule: []types.NamespacedName{
				{Namespace: "istio-system", Name: "meshRule"},
				{Namespace: "test", Name: "nsRule"},
			},
			expectedPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 2,
					},
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 1},
						MaxConnections: 111,
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					ConsecutiveGatewayErrors: &wrappers.UInt32Value{Value: 222},
					Interval:                 &durationpb.Duration{Seconds: 22},
				},
				Tls: &networking.ClientTLSSettings{
					Mode:              networking.ClientTLSSettings_MUTUAL,
					ClientCertificate: "/etc/certs/myclientcert.pem",
					PrivateKey:        "/etc/certs/client_private_key.pem",
					CaCertificates:    "/etc/certs/rootcacerts.pem",
				},
			},
		},
		{
			name:            "unknown namespace+host returns mesh",
			proxyNs:         "unknown",
			serviceNs:       "unknown",
			serviceHostname: "unknown.host",
			expectedConfig:  "meshRule",
			expectedSourceRule: []types.NamespacedName{
				{Namespace: "istio-system", Name: "meshRule"},
			},
			expectedPolicy: meshDestinationRule.Spec.(*networking.DestinationRule).TrafficPolicy,
		},
	}

	ps.setDestinationRules([]config.Config{meshDestinationRule, nsDestinationRule, svcDestinationRule, destinationRuleNamespace2, workloadSpecificDrNamespace2})

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			mergedConfigList := ps.destinationRule(tt.proxyNs,
				&Service{
					Hostname: host.Name(tt.serviceHostname),
					Attributes: ServiceAttributes{
						Namespace: tt.serviceNs,
					},
				})
			expectedConfigPresent := false
			for _, mergedConfig := range mergedConfigList {
				if mergedConfig.rule.Name == tt.expectedConfig {
					expectedConfigPresent = true
					mergedPolicy := mergedConfig.rule.Spec.(*networking.DestinationRule).TrafficPolicy
					assert.Equal(t, mergedPolicy, tt.expectedPolicy)
					assert.Equal(t, mergedConfig.from, tt.expectedSourceRule)
				}
			}
			if !expectedConfigPresent {
				t.Errorf("case %s failed, merged config should contain most specific config name, wanted %v but missing", tt.name, tt.expectedConfig)
			}
		})
	}
}

func TestSetDestinationRuleWithWorkloadSelector(t *testing.T) {
	ps := NewPushContext()
	ps.Mesh = &meshconfig.MeshConfig{RootNamespace: "istio-system"}
	testhost := "httpbin.org"
	app1DestinationRule := config.Config{
		Meta: config.Meta{
			Name:      "nsRule1",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{"test2", "."},
			WorkloadSelector: &selectorpb.WorkloadSelector{
				MatchLabels: map[string]string{"app": "app1"},
			},
			TrafficPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 1},
						MaxConnections: 111,
					},
				},
				Tls: &networking.ClientTLSSettings{
					Mode:              networking.ClientTLSSettings_MUTUAL,
					ClientCertificate: "/etc/certs/myclientcert.pem",
					PrivateKey:        "/etc/certs/client_private_key.pem",
					CaCertificates:    "/etc/certs/rootcacerts.pem",
				},
			},
		},
	}
	app2DestinationRule := config.Config{
		Meta: config.Meta{
			Name:      "nsRule2",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{"test2", "."},
			WorkloadSelector: &selectorpb.WorkloadSelector{
				MatchLabels: map[string]string{"app": "app2"},
			},
			TrafficPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 33,
					},
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 33},
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					Consecutive_5XxErrors: &wrappers.UInt32Value{Value: 3},
				},
				Tls: &networking.ClientTLSSettings{
					Mode: networking.ClientTLSSettings_SIMPLE,
				},
			},
		},
	}
	app3DestinationRule := config.Config{
		Meta: config.Meta{
			Name:      "nsRule3",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{"test2", "."},
			WorkloadSelector: &selectorpb.WorkloadSelector{
				MatchLabels: map[string]string{"app": "app2"},
			},
			TrafficPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 33,
					},
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 33},
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					Consecutive_5XxErrors: &wrappers.UInt32Value{Value: 3},
				},
				Tls: &networking.ClientTLSSettings{
					Mode: networking.ClientTLSSettings_SIMPLE,
				},
			},
		},
	}
	namespaceDestinationRule := config.Config{
		Meta: config.Meta{
			Name:      "nsRule4",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{".", "test2"},
			TrafficPolicy: &networking.TrafficPolicy{
				ConnectionPool: &networking.ConnectionPoolSettings{
					Http: &networking.ConnectionPoolSettings_HTTPSettings{
						MaxRetries: 33,
					},
					Tcp: &networking.ConnectionPoolSettings_TCPSettings{
						ConnectTimeout: &durationpb.Duration{Seconds: 33},
					},
				},
				OutlierDetection: &networking.OutlierDetection{
					Consecutive_5XxErrors: &wrappers.UInt32Value{Value: 3},
				},
				Tls: &networking.ClientTLSSettings{
					Mode: networking.ClientTLSSettings_SIMPLE,
				},
			},
		},
	}
	testCases := []struct {
		name            string
		proxyNs         string
		serviceNs       string
		serviceHostname string
		expectedDrCount int
		expectedDrName  []string
	}{
		{
			name:            "return list of DRs for specific host",
			proxyNs:         "test",
			serviceNs:       "test",
			serviceHostname: testhost,
			expectedDrCount: 3,
			expectedDrName:  []string{app1DestinationRule.Meta.Name, app2DestinationRule.Meta.Name, namespaceDestinationRule.Meta.Name},
		},
		{
			name:            "workload specific DR should not be exported",
			proxyNs:         "test2",
			serviceNs:       "test",
			serviceHostname: testhost,
			expectedDrCount: 1,
			expectedDrName:  []string{namespaceDestinationRule.Meta.Name},
		},
		{
			name:            "rules with same workloadselector should be merged",
			proxyNs:         "test",
			serviceNs:       "test",
			serviceHostname: testhost,
			expectedDrCount: 3,
			expectedDrName:  []string{app1DestinationRule.Meta.Name, app2DestinationRule.Meta.Name, namespaceDestinationRule.Meta.Name},
		},
	}

	ps.setDestinationRules([]config.Config{app1DestinationRule, app2DestinationRule, app3DestinationRule, namespaceDestinationRule})

	for _, tt := range testCases {
		drList := ps.destinationRule(tt.proxyNs,
			&Service{
				Hostname: host.Name(tt.serviceHostname),
				Attributes: ServiceAttributes{
					Namespace: tt.serviceNs,
				},
			})
		if len(drList) != tt.expectedDrCount {
			t.Errorf("case %s failed, %d destinationRules for host %v got %v", tt.name, tt.expectedDrCount, tt.serviceHostname, drList)
		}
		for i, dr := range drList {
			if dr.rule.Name != tt.expectedDrName[i] {
				t.Errorf("case %s failed, destinationRuleName expected %v got %v", tt.name, tt.expectedDrName[i], dr.rule.Name)
			}
		}
	}
}

func TestSetDestinationRuleMerging(t *testing.T) {
	ps := NewPushContext()
	ps.exportToDefaults.destinationRule = map[visibility.Instance]bool{visibility.Public: true}
	testhost := "httpbin.org"
	destinationRuleNamespace1 := config.Config{
		Meta: config.Meta{
			Name:      "rule1",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			Host: testhost,
			Subsets: []*networking.Subset{
				{
					Name: "subset1",
				},
				{
					Name: "subset2",
				},
			},
		},
	}
	destinationRuleNamespace2 := config.Config{
		Meta: config.Meta{
			Name:      "rule2",
			Namespace: "test",
		},
		Spec: &networking.DestinationRule{
			Host: testhost,
			Subsets: []*networking.Subset{
				{
					Name: "subset3",
				},
				{
					Name: "subset4",
				},
			},
		},
	}
	expectedDestRules := []types.NamespacedName{
		{Namespace: "test", Name: "rule1"},
		{Namespace: "test", Name: "rule2"},
	}
	ps.setDestinationRules([]config.Config{destinationRuleNamespace1, destinationRuleNamespace2})
	private := ps.destinationRuleIndex.namespaceLocal["test"].specificDestRules[host.Name(testhost)]
	public := ps.destinationRuleIndex.exportedByNamespace["test"].specificDestRules[host.Name(testhost)]
	subsetsLocal := private[0].rule.Spec.(*networking.DestinationRule).Subsets
	subsetsExport := public[0].rule.Spec.(*networking.DestinationRule).Subsets
	assert.Equal(t, private[0].from, expectedDestRules)
	assert.Equal(t, public[0].from, expectedDestRules)
	if len(subsetsLocal) != 4 {
		t.Errorf("want %d, but got %d", 4, len(subsetsLocal))
	}
	if len(subsetsExport) != 4 {
		t.Errorf("want %d, but got %d", 4, len(subsetsExport))
	}
}

func TestSetDestinationRuleWithExportTo(t *testing.T) {
	ps := NewPushContext()
	ps.Mesh = &meshconfig.MeshConfig{RootNamespace: "istio-system"}
	testhost := "httpbin.org"
	appHost := "foo.app.org"
	wildcardHost1 := "*.org"
	wildcardHost2 := "*.app.org"
	destinationRuleNamespace1 := config.Config{
		Meta: config.Meta{
			Name:      "rule1",
			Namespace: "test1",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{".", "ns1"},
			Subsets: []*networking.Subset{
				{
					Name: "subset1",
				},
				{
					Name: "subset2",
				},
			},
		},
	}
	destinationRuleNamespace2 := config.Config{
		Meta: config.Meta{
			Name:      "rule2",
			Namespace: "test2",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{"test2", "ns1", "test1"},
			Subsets: []*networking.Subset{
				{
					Name: "subset3",
				},
				{
					Name: "subset4",
				},
			},
		},
	}
	destinationRuleNamespace3 := config.Config{
		Meta: config.Meta{
			Name:      "rule3",
			Namespace: "test3",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{"test1", "test2", "*"},
			Subsets: []*networking.Subset{
				{
					Name: "subset5",
				},
				{
					Name: "subset6",
				},
			},
		},
	}
	destinationRuleNamespace4Local := config.Config{
		Meta: config.Meta{
			Name:      "rule4-local",
			Namespace: "test4",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{"test4"},
			Subsets: []*networking.Subset{
				{
					Name: "subset15",
				},
				{
					Name: "subset16",
				},
			},
		},
	}
	destinationRuleRootNamespace := config.Config{
		Meta: config.Meta{
			Name:      "rule4",
			Namespace: "istio-system",
		},
		Spec: &networking.DestinationRule{
			Host: testhost,
			Subsets: []*networking.Subset{
				{
					Name: "subset7",
				},
				{
					Name: "subset8",
				},
			},
		},
	}
	destinationRuleRootNamespaceLocal := config.Config{
		Meta: config.Meta{
			Name:      "rule1",
			Namespace: "istio-system",
		},
		Spec: &networking.DestinationRule{
			Host:     testhost,
			ExportTo: []string{"."},
			Subsets: []*networking.Subset{
				{
					Name: "subset9",
				},
				{
					Name: "subset10",
				},
			},
		},
	}
	destinationRuleRootNamespaceLocalWithWildcardHost1 := config.Config{
		Meta: config.Meta{
			Name:      "rule2",
			Namespace: "istio-system",
		},
		Spec: &networking.DestinationRule{
			Host:     wildcardHost1,
			ExportTo: []string{"."},
			Subsets: []*networking.Subset{
				{
					Name: "subset11",
				},
				{
					Name: "subset12",
				},
			},
		},
	}
	destinationRuleRootNamespaceLocalWithWildcardHost2 := config.Config{
		Meta: config.Meta{
			Name:      "rule3",
			Namespace: "istio-system",
		},
		Spec: &networking.DestinationRule{
			Host:     wildcardHost2,
			ExportTo: []string{"."},
			Subsets: []*networking.Subset{
				{
					Name: "subset13",
				},
				{
					Name: "subset14",
				},
			},
		},
	}

	destinationRule5ExportToRootNamespace := config.Config{
		Meta: config.Meta{
			Name:      "rule5",
			Namespace: "test5",
		},
		Spec: &networking.DestinationRule{
			Host:     "api.test.com",
			ExportTo: []string{"istio-system"},
			Subsets: []*networking.Subset{
				{
					Name: "subset5-0",
				},
				{
					Name: "subset5-1",
				},
			},
		},
	}

	destinationRule6ExportToRootNamespace := config.Config{
		Meta: config.Meta{
			Name:      "rule6",
			Namespace: "test6",
		},
		Spec: &networking.DestinationRule{
			Host:     "api.test.com",
			ExportTo: []string{"istio-system"},
			Subsets: []*networking.Subset{
				{
					Name: "subset6-0",
				},
				{
					Name: "subset6-1",
				},
			},
		},
	}
	ps.setDestinationRules([]config.Config{
		destinationRuleNamespace1, destinationRuleNamespace2,
		destinationRuleNamespace3, destinationRuleNamespace4Local,
		destinationRuleRootNamespace, destinationRuleRootNamespaceLocal,
		destinationRuleRootNamespaceLocalWithWildcardHost1, destinationRuleRootNamespaceLocalWithWildcardHost2,
		destinationRule5ExportToRootNamespace, destinationRule6ExportToRootNamespace,
	})
	cases := []struct {
		proxyNs     string
		serviceNs   string
		host        string
		wantSubsets []string
	}{
		{
			proxyNs:     "test1",
			serviceNs:   "test1",
			host:        testhost,
			wantSubsets: []string{"subset1", "subset2"},
		},
		{
			proxyNs:     "test1",
			serviceNs:   "test2",
			host:        testhost,
			wantSubsets: []string{"subset1", "subset2"},
		},
		{
			proxyNs:     "test2",
			serviceNs:   "test1",
			host:        testhost,
			wantSubsets: []string{"subset3", "subset4"},
		},
		{
			proxyNs:     "test3",
			serviceNs:   "test1",
			host:        testhost,
			wantSubsets: []string{"subset5", "subset6"},
		},
		{
			proxyNs:     "test5",
			serviceNs:   "test4",
			host:        testhost,
			wantSubsets: []string{"subset7", "subset8"},
		},
		{
			proxyNs:     "ns1",
			serviceNs:   "test1",
			host:        testhost,
			wantSubsets: []string{"subset1", "subset2"},
		},
		{
			proxyNs:     "ns1",
			serviceNs:   "random",
			host:        testhost,
			wantSubsets: []string{"subset7", "subset8"},
		},
		{
			proxyNs:     "random",
			serviceNs:   "random",
			host:        testhost,
			wantSubsets: []string{"subset7", "subset8"},
		},
		{
			proxyNs:     "test3",
			serviceNs:   "random",
			host:        testhost,
			wantSubsets: []string{"subset5", "subset6"},
		},
		{
			proxyNs:     "istio-system",
			serviceNs:   "random",
			host:        testhost,
			wantSubsets: []string{"subset9", "subset10"},
		},
		{
			proxyNs:     "istio-system",
			serviceNs:   "istio-system",
			host:        testhost,
			wantSubsets: []string{"subset9", "subset10"},
		},
		{
			proxyNs:     "istio-system",
			serviceNs:   "istio-system",
			host:        appHost,
			wantSubsets: []string{"subset13", "subset14"},
		},
		// dr in the svc ns takes effect on proxy in root ns
		{
			proxyNs:     "istio-system",
			serviceNs:   "test5",
			host:        "api.test.com",
			wantSubsets: []string{"subset5-0", "subset5-1"},
		},
		// dr in the svc ns takes effect on proxy in root ns
		{
			proxyNs:     "istio-system",
			serviceNs:   "test6",
			host:        "api.test.com",
			wantSubsets: []string{"subset6-0", "subset6-1"},
		},
		// both svc and dr namespace is not equal to proxy ns, the dr will not take effect on the proxy
		{
			proxyNs:     "istio-system",
			serviceNs:   "test7",
			host:        "api.test.com",
			wantSubsets: nil,
		},
	}
	for _, tt := range cases {
		t.Run(fmt.Sprintf("%s-%s", tt.proxyNs, tt.serviceNs), func(t *testing.T) {
			out := ps.destinationRule(tt.proxyNs,
				&Service{
					Hostname: host.Name(tt.host),
					Attributes: ServiceAttributes{
						Namespace: tt.serviceNs,
					},
				})
			if tt.wantSubsets == nil {
				if len(out) != 0 {
					t.Fatalf("proxy in %s namespace: unexpected dr found %+v", tt.proxyNs, out)
				}
				return
			}

			if len(out) == 0 {
				t.Fatalf("proxy in %s namespace: dest rule is nil, expected subsets %+v", tt.proxyNs, tt.wantSubsets)
			}
			destRuleConfig := out[0]
			destRule := destRuleConfig.rule.Spec.(*networking.DestinationRule)
			var gotSubsets []string
			for _, ss := range destRule.Subsets {
				gotSubsets = append(gotSubsets, ss.Name)
			}
			if !reflect.DeepEqual(gotSubsets, tt.wantSubsets) {
				t.Fatalf("want %+v, got %+v", tt.wantSubsets, gotSubsets)
			}
		})
	}
}

func TestVirtualServiceWithExportTo(t *testing.T) {
	ps := NewPushContext()
	env := &Environment{Watcher: mesh.NewFixedWatcher(&meshconfig.MeshConfig{RootNamespace: "zzz"})}
	ps.Mesh = env.Mesh()
	configStore := NewFakeStore()
	gatewayName := "default/gateway"

	rule1 := config.Config{
		Meta: config.Meta{
			Name:             "rule1",
			Namespace:        "test1",
			GroupVersionKind: gvk.VirtualService,
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{"rule1.com"},
			ExportTo: []string{".", "ns1"},
		},
	}
	rule2 := config.Config{
		Meta: config.Meta{
			Name:             "rule2",
			Namespace:        "test2",
			GroupVersionKind: gvk.VirtualService,
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{"rule2.com"},
			ExportTo: []string{"test2", "ns1", "test1"},
		},
	}
	rule2Gw := config.Config{
		Meta: config.Meta{
			Name:             "rule2Gw",
			Namespace:        "test2",
			GroupVersionKind: gvk.VirtualService,
		},
		Spec: &networking.VirtualService{
			Gateways: []string{gatewayName, constants.IstioMeshGateway},
			Hosts:    []string{"rule2gw.com"},
			ExportTo: []string{"test2", "ns1", "test1"},
		},
	}
	rule3 := config.Config{
		Meta: config.Meta{
			Name:             "rule3",
			Namespace:        "test3",
			GroupVersionKind: gvk.VirtualService,
		},
		Spec: &networking.VirtualService{
			Gateways: []string{constants.IstioMeshGateway},
			Hosts:    []string{"rule3.com"},
			ExportTo: []string{"test1", "test2", "*"},
		},
	}
	rule3Gw := config.Config{
		Meta: config.Meta{
			Name:             "rule3Gw",
			Namespace:        "test3",
			GroupVersionKind: gvk.VirtualService,
		},
		Spec: &networking.VirtualService{
			Gateways: []string{gatewayName},
			Hosts:    []string{"rule3gw.com"},
			ExportTo: []string{"test1", "test2", "*"},
		},
	}
	rootNS := config.Config{
		Meta: config.Meta{
			Name:             "zzz",
			Namespace:        "zzz",
			GroupVersionKind: gvk.VirtualService,
		},
		Spec: &networking.VirtualService{
			Hosts: []string{"rootNS.com"},
		},
	}

	for _, c := range []config.Config{rule1, rule2, rule3, rule2Gw, rule3Gw, rootNS} {
		if _, err := configStore.Create(c); err != nil {
			t.Fatalf("could not create %v", c.Name)
		}
	}

	env.ConfigStore = configStore
	ps.initDefaultExportMaps()
	if err := ps.initVirtualServices(env); err != nil {
		t.Fatalf("init virtual services failed: %v", err)
	}

	cases := []struct {
		proxyNs   string
		gateway   string
		wantHosts []string
	}{
		{
			proxyNs:   "test1",
			wantHosts: []string{"rule1.com", "rule2.com", "rule2gw.com", "rule3.com", "rootNS.com"},
			gateway:   constants.IstioMeshGateway,
		},
		{
			proxyNs:   "test2",
			wantHosts: []string{"rule2.com", "rule2gw.com", "rule3.com", "rootNS.com"},
			gateway:   constants.IstioMeshGateway,
		},
		{
			proxyNs:   "ns1",
			wantHosts: []string{"rule1.com", "rule2.com", "rule2gw.com", "rule3.com", "rootNS.com"},
			gateway:   constants.IstioMeshGateway,
		},
		{
			proxyNs:   "random",
			wantHosts: []string{"rule3.com", "rootNS.com"},
			gateway:   constants.IstioMeshGateway,
		},
		{
			proxyNs:   "test1",
			wantHosts: []string{"rule2gw.com", "rule3gw.com"},
			gateway:   gatewayName,
		},
		{
			proxyNs:   "test2",
			wantHosts: []string{"rule2gw.com", "rule3gw.com"},
			gateway:   gatewayName,
		},
		{
			proxyNs:   "ns1",
			wantHosts: []string{"rule2gw.com", "rule3gw.com"},
			gateway:   gatewayName,
		},
		{
			proxyNs:   "random",
			wantHosts: []string{"rule3gw.com"},
			gateway:   gatewayName,
		},
	}
	for _, tt := range cases {
		t.Run(fmt.Sprintf("%s-%s", tt.proxyNs, tt.gateway), func(t *testing.T) {
			rules := ps.VirtualServicesForGateway(tt.proxyNs, tt.gateway)
			gotHosts := make([]string, 0)
			for _, r := range rules {
				vs := r.Spec.(*networking.VirtualService)
				gotHosts = append(gotHosts, vs.Hosts...)
			}
			if !reflect.DeepEqual(gotHosts, tt.wantHosts) {
				t.Errorf("want %+v, got %+v", tt.wantHosts, gotHosts)
			}
		})
	}
}

func TestInitVirtualService(t *testing.T) {
	ps := NewPushContext()
	env := &Environment{Watcher: mesh.NewFixedWatcher(&meshconfig.MeshConfig{RootNamespace: "istio-system"})}
	ps.Mesh = env.Mesh()
	configStore := NewFakeStore()
	gatewayName := "ns1/gateway"

	vs1 := config.Config{
		Meta: config.Meta{
			GroupVersionKind: collections.IstioNetworkingV1Alpha3Virtualservices.Resource().GroupVersionKind(),
			Name:             "vs1",
			Namespace:        "ns1",
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{"*.org"},
			Gateways: []string{"gateway"},
			Http: []*networking.HTTPRoute{
				{
					Match: []*networking.HTTPMatchRequest{
						{
							Uri: &networking.StringMatch{
								MatchType: &networking.StringMatch_Prefix{Prefix: "/productpage"},
							},
						},
						{
							Uri: &networking.StringMatch{
								MatchType: &networking.StringMatch_Exact{Exact: "/login"},
							},
						},
					},
					Delegate: &networking.Delegate{
						Name:      "vs2",
						Namespace: "ns2",
					},
				},
			},
		},
	}
	vs2 := config.Config{
		Meta: config.Meta{
			GroupVersionKind: collections.IstioNetworkingV1Alpha3Virtualservices.Resource().GroupVersionKind(),
			Name:             "vs2",
			Namespace:        "ns2",
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{},
			Gateways: []string{gatewayName},
			Http: []*networking.HTTPRoute{
				{
					Route: []*networking.HTTPRouteDestination{
						{
							Destination: &networking.Destination{
								Host: "test",
								Port: &networking.PortSelector{
									Number: 80,
								},
							},
						},
					},
				},
			},
		},
	}

	for _, c := range []config.Config{vs1, vs2} {
		if _, err := configStore.Create(c); err != nil {
			t.Fatalf("could not create %v", c.Name)
		}
	}

	env.ConfigStore = configStore
	ps.initDefaultExportMaps()
	if err := ps.initVirtualServices(env); err != nil {
		t.Fatalf("init virtual services failed: %v", err)
	}

	t.Run("resolve shortname", func(t *testing.T) {
		rules := ps.VirtualServicesForGateway("ns1", gatewayName)
		if len(rules) != 1 {
			t.Fatalf("wanted 1 virtualservice for gateway %s, actually got %d", gatewayName, len(rules))
		}
		gotHTTPHosts := make([]string, 0)
		for _, r := range rules {
			vs := r.Spec.(*networking.VirtualService)
			for _, route := range vs.GetHttp() {
				for _, dst := range route.Route {
					gotHTTPHosts = append(gotHTTPHosts, dst.Destination.Host)
				}
			}
		}
		if !reflect.DeepEqual(gotHTTPHosts, []string{"test.ns2"}) {
			t.Errorf("got %+v", gotHTTPHosts)
		}
	})
}

func TestServiceWithExportTo(t *testing.T) {
	ps := NewPushContext()
	env := &Environment{Watcher: mesh.NewFixedWatcher(&meshconfig.MeshConfig{RootNamespace: "zzz"})}
	ps.Mesh = env.Mesh()

	svc1 := &Service{
		Hostname: "svc1",
		Attributes: ServiceAttributes{
			Namespace: "test1",
			ExportTo:  map[visibility.Instance]bool{visibility.Private: true, visibility.Instance("ns1"): true},
		},
	}
	svc2 := &Service{
		Hostname: "svc2",
		Attributes: ServiceAttributes{
			Namespace: "test2",
			ExportTo: map[visibility.Instance]bool{
				visibility.Instance("test1"): true,
				visibility.Instance("ns1"):   true,
				visibility.Instance("test2"): true,
			},
		},
	}
	svc3 := &Service{
		Hostname: "svc3",
		Attributes: ServiceAttributes{
			Namespace: "test3",
			ExportTo: map[visibility.Instance]bool{
				visibility.Instance("test1"): true,
				visibility.Public:            true,
				visibility.Instance("test2"): true,
			},
		},
	}
	svc4 := &Service{
		Hostname: "svc4",
		Attributes: ServiceAttributes{
			Namespace: "test4",
		},
	}
	env.ServiceDiscovery = &localServiceDiscovery{
		services: []*Service{svc1, svc2, svc3, svc4},
	}
	ps.initDefaultExportMaps()
	if err := ps.initServiceRegistry(env); err != nil {
		t.Fatalf("init services failed: %v", err)
	}

	cases := []struct {
		proxyNs   string
		wantHosts []string
	}{
		{
			proxyNs:   "test1",
			wantHosts: []string{"svc1", "svc2", "svc3", "svc4"},
		},
		{
			proxyNs:   "test2",
			wantHosts: []string{"svc2", "svc3", "svc4"},
		},
		{
			proxyNs:   "ns1",
			wantHosts: []string{"svc1", "svc2", "svc3", "svc4"},
		},
		{
			proxyNs:   "random",
			wantHosts: []string{"svc3", "svc4"},
		},
	}
	for _, tt := range cases {
		services := ps.servicesExportedToNamespace(tt.proxyNs)
		gotHosts := make([]string, 0)
		for _, r := range services {
			gotHosts = append(gotHosts, string(r.Hostname))
		}
		if !reflect.DeepEqual(gotHosts, tt.wantHosts) {
			t.Errorf("proxy in %s namespace: want %+v, got %+v", tt.proxyNs, tt.wantHosts, gotHosts)
		}
	}
}

func TestGetHostsFromMeshConfig(t *testing.T) {
	ps := NewPushContext()
	env := &Environment{Watcher: mesh.NewFixedWatcher(&meshconfig.MeshConfig{
		RootNamespace: "istio-system",
		ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
			{
				Name: "otel",
				Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyOtelAls{
					EnvoyOtelAls: &meshconfig.MeshConfig_ExtensionProvider_EnvoyOpenTelemetryLogProvider{
						Service: "otel.foo.svc.cluster.local",
						Port:    9811,
					},
				},
			},
		},
		DefaultProviders: &meshconfig.MeshConfig_DefaultProviders{
			AccessLogging: []string{"otel"},
		},
	})}
	ps.Mesh = env.Mesh()
	configStore := NewFakeStore()
	gatewayName := "ns1/gateway"

	vs1 := config.Config{
		Meta: config.Meta{
			GroupVersionKind: collections.IstioNetworkingV1Alpha3Virtualservices.Resource().GroupVersionKind(),
			Name:             "vs1",
			Namespace:        "ns1",
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{"*.org"},
			Gateways: []string{"gateway"},
			Http: []*networking.HTTPRoute{
				{
					Match: []*networking.HTTPMatchRequest{
						{
							Uri: &networking.StringMatch{
								MatchType: &networking.StringMatch_Prefix{Prefix: "/productpage"},
							},
						},
						{
							Uri: &networking.StringMatch{
								MatchType: &networking.StringMatch_Exact{Exact: "/login"},
							},
						},
					},
					Delegate: &networking.Delegate{
						Name:      "vs2",
						Namespace: "ns2",
					},
				},
			},
		},
	}
	vs2 := config.Config{
		Meta: config.Meta{
			GroupVersionKind: collections.IstioNetworkingV1Alpha3Virtualservices.Resource().GroupVersionKind(),
			Name:             "vs2",
			Namespace:        "ns2",
		},
		Spec: &networking.VirtualService{
			Hosts:    []string{},
			Gateways: []string{gatewayName},
			Http: []*networking.HTTPRoute{
				{
					Route: []*networking.HTTPRouteDestination{
						{
							Destination: &networking.Destination{
								Host: "test",
								Port: &networking.PortSelector{
									Number: 80,
								},
							},
						},
					},
				},
			},
		},
	}

	for _, c := range []config.Config{vs1, vs2} {
		if _, err := configStore.Create(c); err != nil {
			t.Fatalf("could not create %v", c.Name)
		}
	}

	env.ConfigStore = configStore
	ps.initTelemetry(env)
	ps.initDefaultExportMaps()
	if err := ps.initVirtualServices(env); err != nil {
		t.Fatalf("init virtual services failed: %v", err)
	}
	got := sets.String{}
	addHostsFromMeshConfig(ps, got)
	assert.Equal(t, []string{"otel.foo.svc.cluster.local"}, sets.SortedList(got))
}

// TestGetHostsFromMeshConfigExhaustiveness exhaustiveness check of `getHostsFromMeshConfig`
// Once some one add a new `Provider` in api, we should update `wellknownProviders` and
// implements of `getHostsFromMeshConfig`
func TestGetHostsFromMeshConfigExhaustiveness(t *testing.T) {
	wellknownProviders := map[string]struct{}{
		"envoy_ext_authz_http": {},
		"envoy_ext_authz_grpc": {},
		"zipkin":               {},
		"lightstep":            {},
		"datadog":              {},
		"opencensus":           {},
		"skywalking":           {},
		"envoy_http_als":       {},
		"envoy_tcp_als":        {},
		"envoy_otel_als":       {},
		"opentelemetry":        {},
	}

	unexpectedProviders := make([]string, 0)
	msg := &meshconfig.MeshConfig_ExtensionProvider{}
	pb := msg.ProtoReflect()
	md := pb.Descriptor()

	of := md.Oneofs().Get(0)
	for i := 0; i < of.Fields().Len(); i++ {
		o := of.Fields().Get(i)
		if o.Message().Fields().ByName("service") != nil {
			n := string(o.Name())
			if _, ok := wellknownProviders[n]; ok {
				delete(wellknownProviders, n)
			} else {
				unexpectedProviders = append(unexpectedProviders, n)
			}
		}
	}

	if len(wellknownProviders) != 0 || len(unexpectedProviders) != 0 {
		t.Errorf("unexpected provider not implemented in getHostsFromMeshConfig")
		t.Fail()
	}
}

var _ ServiceDiscovery = &localServiceDiscovery{}

// MockDiscovery is an in-memory ServiceDiscover with mock services
type localServiceDiscovery struct {
	services         []*Service
	serviceInstances []*ServiceInstance

	NetworkGatewaysHandler
}

var _ ServiceDiscovery = &localServiceDiscovery{}

func (l *localServiceDiscovery) Services() []*Service {
	return l.services
}

func (l *localServiceDiscovery) GetService(host.Name) *Service {
	panic("implement me")
}

func (l *localServiceDiscovery) InstancesByPort(*Service, int) []*ServiceInstance {
	return l.serviceInstances
}

func (l *localServiceDiscovery) GetProxyServiceInstances(*Proxy) []*ServiceInstance {
	panic("implement me")
}

func (l *localServiceDiscovery) GetProxyWorkloadLabels(*Proxy) labels.Instance {
	panic("implement me")
}

func (l *localServiceDiscovery) GetIstioServiceAccounts(*Service) []string {
	return nil
}

func (l *localServiceDiscovery) NetworkGateways() []NetworkGateway {
	// TODO implement fromRegistry logic from kube controller if needed
	return nil
}

func (l *localServiceDiscovery) MCSServices() []MCSServiceInfo {
	return nil
}
