/*
 *
 * Copyright 2021 gRPC 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 xdsresource

import (
	"fmt"
	"net/netip"
	"strings"
	"testing"

	"google.golang.org/grpc/internal/envconfig"
	"google.golang.org/grpc/internal/grpctest"
	"google.golang.org/grpc/internal/pretty"
	"google.golang.org/grpc/internal/testutils"
	"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/testing/protocmp"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/wrapperspb"

	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
)

var cmpOptsIgnoreRawProto = cmp.Options{
	cmpopts.EquateEmpty(),
	cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{}),
	cmpopts.IgnoreFields(ListenerUpdate{}, "Raw"),
	cmpopts.IgnoreFields(RouteConfigUpdate{}, "Raw"),
	protocmp.Transform(),
}

// Tests cases where the filter chain match criteria contains unsupported
// fields, that result in the chain being dropped.
func (s) TestUnmarshalListener_ServerSide_DroppedFilterChains(t *testing.T) {
	tests := []struct {
		desc     string
		resource *anypb.Any
		lis      *v3listenerpb.Listener
		wantErr  string
	}{
		{
			desc: "unsupported destination port field",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{DestinationPort: &wrapperspb.UInt32Value{Value: 666}},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `Dropping filter chain "test-filter-chain" since it contains unsupported destination_port match field`,
		},
		{
			desc: "unsupported server names field",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{ServerNames: []string{"example-server"}},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `Dropping filter chain "test-filter-chain" since it contains unsupported server_names match field`,
		},
		{
			desc: "unsupported transport protocol field",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{TransportProtocol: "tls"},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `Dropping filter chain "test-filter-chain" since it contains unsupported value for transport_protocol match field`,
		},
		{
			desc: "unsupported application protocol field",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{ApplicationProtocols: []string{"h2"}},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `Dropping filter chain "test-filter-chain" since it contains unsupported application_protocols match field`,
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			grpctest.ExpectWarning(test.wantErr)
			resource := listenerProtoToAny(t, test.lis)
			if _, _, err := unmarshalListenerResource(resource, nil); err == nil {
				t.Errorf("unmarshalListenerResource(%s) succeeded when expected to fail", pretty.ToJSON(resource))
			}
		})
	}
}

// Tests cases where the filter chain match criteria contains invalid
// information, that result in unmarshaling failure.
func (s) TestUnmarshalListener_ServerSide_FilterChains_FailureCases(t *testing.T) {
	tests := []struct {
		desc    string
		lis     *v3listenerpb.Listener
		wantErr string
	}{
		{
			desc: "bad dest address prefix",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{{AddressPrefix: "a.b.c.d"}}},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `failed to parse destination prefix ranges: invalid address: "a.b.c.d"`,
		},
		{
			desc: "bad dest prefix length",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("10.1.1.0", 50)}},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `failed to parse destination prefix ranges: length 50 is invalid for "10.1.1.0" (max 32)`,
		},
		{
			desc: "bad source address prefix",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePrefixRanges: []*v3corepb.CidrRange{{AddressPrefix: "a.b.c.d"}}},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `failed to parse source prefix ranges: invalid address: "a.b.c.d"`,
		},
		{
			desc: "bad source prefix length",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("10.1.1.0", 50)}},
						Name:             "test-filter-chain",
					},
				},
			},
			wantErr: `failed to parse source prefix ranges: length 50 is invalid for "10.1.1.0" (max 32)`,
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			if _, _, err := unmarshalListenerResource(resource, nil); err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Errorf("unmarshalListenerResource(%s) failed with error: %v, want: %v", pretty.ToJSON(resource), err, test.wantErr)
			}
		})
	}
}

// Tests cases where there are multiple filter chains and they have overlapping
// match rules.
func (s) TestUnmarshalListener_ServerSide_OverlappingMatchingRules(t *testing.T) {
	tests := []struct {
		desc string
		lis  *v3listenerpb.Listener
	}{
		{
			desc: "matching destination prefixes with no other matchers",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16), cidrRangeFromAddressAndPrefixLen("10.0.0.0", 0)},
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.2.2", 16)},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
		},
		{
			desc: "matching source type",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourceType: v3listenerpb.FilterChainMatch_ANY},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourceType: v3listenerpb.FilterChainMatch_EXTERNAL},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourceType: v3listenerpb.FilterChainMatch_EXTERNAL},
						Filters:          emptyValidNetworkFilters(t),
					},
				},
			},
		},
		{
			desc: "matching source prefixes",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16), cidrRangeFromAddressAndPrefixLen("10.0.0.0", 0)},
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.2.2", 16)},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
		},
		{
			desc: "matching source ports",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePorts: []uint32{1, 2, 3, 4, 5}},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePorts: []uint32{5, 6, 7}},
						Filters:          emptyValidNetworkFilters(t),
					},
				},
			},
		},
	}

	const wantErr = "multiple filter chains with overlapping matching rules are defined"
	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			if _, _, err := unmarshalListenerResource(resource, nil); err == nil || !strings.Contains(err.Error(), wantErr) {
				t.Errorf("unmarshalListenerResource(%s) failed with error: %v, want: %v", pretty.ToJSON(resource), err, wantErr)
			}
		})
	}
}

// Tests cases where the security configuration in the filter chain is invalid.
func (s) TestUnmarshalListener_ServerSide_BadSecurityConfig(t *testing.T) {
	tests := []struct {
		desc                      string
		lis                       *v3listenerpb.Listener
		wantErr                   string
		enableSystemRootCertsFlag bool
	}{
		{
			desc: "no filter chains",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
			},
			wantErr: "no supported filter chains and no default filter chain",
		},
		{
			desc: "unexpected transport socket name",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{Name: "unsupported-transport-socket-name"},
						Filters:         emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "transport_socket field has unexpected name",
		},
		{
			desc: "unexpected transport socket URL",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: fmt.Sprintf("transport_socket missing typed_config or wrong type_url: \"%s\"", testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{}).TypeUrl),
		},
		{
			desc: "badly marshaled transport socket",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: &anypb.Any{
									TypeUrl: version.V3DownstreamTLSContextURL,
									Value:   []byte{1, 2, 3, 4},
								},
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "failed to unmarshal DownstreamTlsContext in LDS response",
		},
		{
			desc: "missing CommonTlsContext",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "DownstreamTlsContext in LDS response does not contain a CommonTlsContext",
		},
		{
			desc: "require_sni-set-to-true-in-downstreamTlsContext",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									RequireSni: &wrapperspb.BoolValue{Value: true},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "require_sni field set to true in DownstreamTlsContext message",
		},
		{
			desc: "unsupported-ocsp_staple_policy-in-downstreamTlsContext",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									OcspStaplePolicy: v3tlspb.DownstreamTlsContext_STRICT_STAPLING,
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "ocsp_staple_policy field set to unsupported value in DownstreamTlsContext message",
		},
		{
			desc: "unsupported validation context in transport socket",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextSdsSecretConfig{
											ValidationContextSdsSecretConfig: &v3tlspb.SdsSecretConfig{
												Name: "foo-sds-secret",
											},
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "validation context contains unexpected type",
		},
		{
			desc: "unsupported match_subject_alt_names field in transport socket",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextSdsSecretConfig{
											ValidationContextSdsSecretConfig: &v3tlspb.SdsSecretConfig{
												Name: "foo-sds-secret",
											},
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "validation context contains unexpected type",
		},
		{
			desc: "no root certificate provider with require_client_cert",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
											InstanceName:    "identityPluginInstance",
											CertificateName: "identityCertName",
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "security configuration on the server-side does not contain root certificate provider instance name, but require_client_cert field is set",
		},
		{
			desc: "no_identity_certificate_provider",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									CommonTlsContext: &v3tlspb.CommonTlsContext{},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "security configuration on the server-side does not contain identity certificate provider instance name",
		},
		{
			desc:                      "system root certificate field set on server",
			enableSystemRootCertsFlag: true,
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
											InstanceName:    "identityPluginInstance",
											CertificateName: "identityCertName",
										},
										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContext{
											ValidationContext: &v3tlspb.CertificateValidationContext{
												SystemRootCerts: &v3tlspb.CertificateValidationContext_SystemRootCerts{},
											},
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "expected field ca_certificate_provider_instance is missing and unexpected field system_root_certs is set",
		},
		{
			desc: "system root certificate field set on server, env var disabled",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
											InstanceName:    "identityPluginInstance",
											CertificateName: "identityCertName",
										},
										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContext{
											ValidationContext: &v3tlspb.CertificateValidationContext{
												SystemRootCerts: &v3tlspb.CertificateValidationContext_SystemRootCerts{},
											},
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
			},
			wantErr: "expected field ca_certificate_provider_instance is missing",
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			testutils.SetEnvConfig(t, &envconfig.XDSSystemRootCertsEnabled, test.enableSystemRootCertsFlag)
			resource := listenerProtoToAny(t, test.lis)
			if _, _, err := unmarshalListenerResource(resource, nil); err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Errorf("unmarshalListenerResource(%s) failed with error: %v, want: %v", pretty.ToJSON(resource), err, test.wantErr)
			}
		})
	}
}

// Tests cases where the route configuration specified in the HTTP Connection
// Manager within the filter chain is valid.
func (s) TestUnmarshalListener_ServerSide_GoodRouteUpdate(t *testing.T) {
	tests := []struct {
		name         string
		lis          *v3listenerpb.Listener
		wantListener ListenerUpdate
	}{
		{
			name: "one_route_config_name",
			lis: &v3listenerpb.Listener{
				Name:    "listerner-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
											Rds: &v3httppb.Rds{
												ConfigSource: &v3corepb.ConfigSource{
													ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
												},
												RouteConfigName: "route-1",
											},
										},
										HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
										Rds: &v3httppb.Rds{
											ConfigSource: &v3corepb.ConfigSource{
												ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
											},
											RouteConfigName: "route-1",
										},
									},
									HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
								}),
							},
						},
					},
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												RouteConfigName: "route-1",
												HTTPFilters:     makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							RouteConfigName: "route-1",
							HTTPFilters:     makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			name: "inline_route_config",
			lis: &v3listenerpb.Listener{
				Name:    "listerner-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
											RouteConfig: routeConfig,
										},
										HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
										RouteConfig: routeConfig,
									},
									HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
								}),
							},
						},
					},
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			name: "two_route_config_names",
			lis: &v3listenerpb.Listener{
				Name:    "listerner-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
											Rds: &v3httppb.Rds{
												ConfigSource: &v3corepb.ConfigSource{
													ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
												},
												RouteConfigName: "route-1",
											},
										},
										HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
										Rds: &v3httppb.Rds{
											ConfigSource: &v3corepb.ConfigSource{
												ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
											},
											RouteConfigName: "route-2",
										},
									},
									HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
								}),
							},
						},
					},
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												RouteConfigName: "route-1",
												HTTPFilters:     makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							RouteConfigName: "route-2",
							HTTPFilters:     makeRouterFilterList(t),
						},
					},
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			_, gotListener, err := unmarshalListenerResource(resource, nil)
			if err != nil {
				t.Fatalf("unmarshalListenerResource(%s) failed with error: %v", pretty.ToJSON(resource), err)
			}
			if diff := cmp.Diff(test.wantListener, gotListener, cmpOptsIgnoreRawProto); diff != "" {
				t.Errorf("unmarshalListenerResource(%s), got unexpected update, diff (-want, +got):\n%s", pretty.ToJSON(resource), diff)
			}
		})
	}
}

// Tests cases where the route configuration specified in the HTTP Connection
// Manager within the filter chain is invalid.
func (s) TestUnmarshalListener_ServerSide_BadRouteUpdate(t *testing.T) {
	tests := []struct {
		name    string
		lis     *v3listenerpb.Listener
		wantErr string
	}{
		{
			name: "missing-route-specifier",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{

									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
								}),
							},
						},
					},
				},
			},
			wantErr: "no RouteSpecifier",
		},
		{
			name: "not-ads",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{

									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
											Rds: &v3httppb.Rds{
												RouteConfigName: "route-1",
											},
										},
										HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{
										Rds: &v3httppb.Rds{
											RouteConfigName: "route-1",
										},
									},
									HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter},
								}),
							},
						},
					},
				},
			},
			wantErr: "ConfigSource is not ADS",
		},
		{
			name: "unsupported-route-specifier",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
										HttpFilters:    []*v3httppb.HttpFilter{emptyRouterFilter},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{},
									HttpFilters:    []*v3httppb.HttpFilter{emptyRouterFilter},
								}),
							},
						},
					},
				},
			},
			wantErr: "unsupported type",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			if _, _, err := unmarshalListenerResource(resource, nil); err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Errorf("unmarshalListenerResource(%s) failed with error: %v, want: %v", pretty.ToJSON(resource), err, test.wantErr)
			}
		})
	}
}

// Tests cases where the HTTP Filters in the filter chain are invalid.
func (s) TestUnmarshalListener_ServerSide_BadHTTPFilters(t *testing.T) {
	tests := []struct {
		name    string
		lis     *v3listenerpb.Listener
		wantErr string
	}{
		{
			name: "client side HTTP filter",
			lis: &v3listenerpb.Listener{
				Name:    "grpc/server?xds.resource.listening_address=0.0.0.0:9999",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										HttpFilters: []*v3httppb.HttpFilter{
											{
												Name:       "clientOnlyCustomFilter",
												ConfigType: &v3httppb.HttpFilter_TypedConfig{TypedConfig: clientOnlyCustomFilterConfig},
											},
										},
									}),
								},
							},
						},
					},
				},
			},
			wantErr: "invalid server side HTTP Filters",
		},
		{
			name: "one valid then one invalid HTTP filter",
			lis: &v3listenerpb.Listener{
				Name:    "grpc/server?xds.resource.listening_address=0.0.0.0:9999",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										HttpFilters: []*v3httppb.HttpFilter{
											validServerSideHTTPFilter1,
											{
												Name:       "clientOnlyCustomFilter",
												ConfigType: &v3httppb.HttpFilter_TypedConfig{TypedConfig: clientOnlyCustomFilterConfig},
											},
										},
									}),
								},
							},
						},
					},
				},
			},
			wantErr: "invalid server side HTTP Filters",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			if _, _, err := unmarshalListenerResource(resource, nil); err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Errorf("unmarshalListenerResource(%s) failed with error: %v, want: %v", pretty.ToJSON(resource), err, test.wantErr)
			}
		})
	}
}

// Tests cases where the HTTP Filters in the filter chain are valid.
func (s) TestUnmarshalListener_ServerSide_GoodHTTPFilters(t *testing.T) {
	tests := []struct {
		name         string
		lis          *v3listenerpb.Listener
		wantListener ListenerUpdate
	}{
		{
			name: "singular valid http filter",
			lis: &v3listenerpb.Listener{
				Name:    "listener-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										HttpFilters: []*v3httppb.HttpFilter{
											validServerSideHTTPFilter1,
											emptyRouterFilter,
										},
										RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
											RouteConfig: routeConfig,
										},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									HttpFilters: []*v3httppb.HttpFilter{
										validServerSideHTTPFilter1,
										emptyRouterFilter,
									},
									RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
										RouteConfig: routeConfig,
									},
								}),
							},
						},
					},
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters: []HTTPFilter{
													{
														Name:   "serverOnlyCustomFilter",
														Filter: serverOnlyHTTPFilter{},
														Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
													},
													makeRouterFilter(t),
												},
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters: []HTTPFilter{
								{
									Name:   "serverOnlyCustomFilter",
									Filter: serverOnlyHTTPFilter{},
									Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
								},
								makeRouterFilter(t),
							},
						},
					},
				},
			},
		},
		{
			name: "two valid http filters",
			lis: &v3listenerpb.Listener{
				Name:    "listener-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										HttpFilters: []*v3httppb.HttpFilter{
											validServerSideHTTPFilter1,
											validServerSideHTTPFilter2,
											emptyRouterFilter,
										},
										RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
											RouteConfig: routeConfig,
										},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									HttpFilters: []*v3httppb.HttpFilter{
										validServerSideHTTPFilter1,
										validServerSideHTTPFilter2,
										emptyRouterFilter,
									},
									RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
										RouteConfig: routeConfig,
									},
								}),
							},
						},
					},
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters: []HTTPFilter{
													{
														Name:   "serverOnlyCustomFilter",
														Filter: serverOnlyHTTPFilter{},
														Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
													},
													{
														Name:   "serverOnlyCustomFilter2",
														Filter: serverOnlyHTTPFilter{},
														Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
													},
													makeRouterFilter(t),
												},
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters: []HTTPFilter{
								{
									Name:   "serverOnlyCustomFilter",
									Filter: serverOnlyHTTPFilter{},
									Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
								},
								{
									Name:   "serverOnlyCustomFilter2",
									Filter: serverOnlyHTTPFilter{},
									Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
								},
								makeRouterFilter(t),
							},
						},
					},
				},
			},
		},
		// In the case of two HTTP Connection Manager's being present, the
		// second HTTP Connection Manager should be validated, but ignored.
		{
			name: "two hcms",
			lis: &v3listenerpb.Listener{
				Name:    "listener-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name: "filter-chain-1",
						Filters: []*v3listenerpb.Filter{
							{
								Name: "hcm",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										HttpFilters: []*v3httppb.HttpFilter{
											validServerSideHTTPFilter1,
											validServerSideHTTPFilter2,
											emptyRouterFilter,
										},
										RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
											RouteConfig: routeConfig,
										},
									}),
								},
							},
							{
								Name: "hcm2",
								ConfigType: &v3listenerpb.Filter_TypedConfig{
									TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
										HttpFilters: []*v3httppb.HttpFilter{
											validServerSideHTTPFilter1,
											emptyRouterFilter,
										},
										RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
											RouteConfig: routeConfig,
										},
									}),
								},
							},
						},
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: []*v3listenerpb.Filter{
						{
							Name: "hcm",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									HttpFilters: []*v3httppb.HttpFilter{
										validServerSideHTTPFilter1,
										validServerSideHTTPFilter2,
										emptyRouterFilter,
									},
									RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
										RouteConfig: routeConfig,
									},
								}),
							},
						},
						{
							Name: "hcm2",
							ConfigType: &v3listenerpb.Filter_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
									HttpFilters: []*v3httppb.HttpFilter{
										validServerSideHTTPFilter1,
										emptyRouterFilter,
									},
									RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
										RouteConfig: routeConfig,
									},
								}),
							},
						},
					},
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters: []HTTPFilter{
													{
														Name:   "serverOnlyCustomFilter",
														Filter: serverOnlyHTTPFilter{},
														Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
													},
													{
														Name:   "serverOnlyCustomFilter2",
														Filter: serverOnlyHTTPFilter{},
														Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
													},
													makeRouterFilter(t),
												},
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters: []HTTPFilter{
								{
									Name:   "serverOnlyCustomFilter",
									Filter: serverOnlyHTTPFilter{},
									Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
								},
								{
									Name:   "serverOnlyCustomFilter2",
									Filter: serverOnlyHTTPFilter{},
									Config: filterConfig{Cfg: serverOnlyCustomFilterConfig},
								},
								makeRouterFilter(t),
							},
						},
					},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			_, gotListener, err := unmarshalListenerResource(resource, nil)
			if err != nil {
				t.Fatalf("unmarshalListenerResource(%s) failed with error: %v", pretty.ToJSON(resource), err)
			}
			if diff := cmp.Diff(test.wantListener, gotListener, cmpOptsIgnoreRawProto); diff != "" {
				t.Errorf("unmarshalListenerResource(%s), got unexpected update, diff (-want, +got):\n%s", pretty.ToJSON(resource), diff)
			}
		})
	}
}

// Tests cases where the security configuration in the filter chain contains
// valid data.
func (s) TestUnmarshalListener_ServerSide_GoodSecurityConfig(t *testing.T) {
	tests := []struct {
		desc                      string
		lis                       *v3listenerpb.Listener
		wantListener              ListenerUpdate
		enableSystemRootCertsFlag bool
	}{
		{
			desc: "empty transport socket",
			lis: &v3listenerpb.Listener{
				Name:    "listerner-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name:    "filter-chain-1",
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Filters: emptyValidNetworkFilters(t),
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "no validation context",
			lis: &v3listenerpb.Listener{
				Name:    "listerner-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
											InstanceName:    "identityPluginInstance",
											CertificateName: "identityCertName",
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					TransportSocket: &v3corepb.TransportSocket{
						Name: "envoy.transport_sockets.tls",
						ConfigType: &v3corepb.TransportSocket_TypedConfig{
							TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
								CommonTlsContext: &v3tlspb.CommonTlsContext{
									TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
										InstanceName:    "defaultIdentityPluginInstance",
										CertificateName: "defaultIdentityCertName",
									},
								},
							}),
						},
					},
					Filters: emptyValidNetworkFilters(t),
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											SecurityCfg: &SecurityConfig{
												IdentityInstanceName: "identityPluginInstance",
												IdentityCertName:     "identityCertName",
											},
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						SecurityCfg: &SecurityConfig{
							IdentityInstanceName: "defaultIdentityPluginInstance",
							IdentityCertName:     "defaultIdentityCertName",
						},
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "validation context with certificate provider",
			lis: &v3listenerpb.Listener{
				Name:    "listerner-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
											InstanceName:    "identityPluginInstance",
											CertificateName: "identityCertName",
										},
										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
											ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
												InstanceName:    "rootPluginInstance",
												CertificateName: "rootCertName",
											},
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Name: "default-filter-chain-1",
					TransportSocket: &v3corepb.TransportSocket{
						Name: "envoy.transport_sockets.tls",
						ConfigType: &v3corepb.TransportSocket_TypedConfig{
							TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
								RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
								CommonTlsContext: &v3tlspb.CommonTlsContext{
									TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
										InstanceName:    "defaultIdentityPluginInstance",
										CertificateName: "defaultIdentityCertName",
									},
									ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
										ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
											InstanceName:    "defaultRootPluginInstance",
											CertificateName: "defaultRootCertName",
										},
									},
								},
							}),
						},
					},
					Filters: emptyValidNetworkFilters(t),
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											SecurityCfg: &SecurityConfig{
												RootInstanceName:     "rootPluginInstance",
												RootCertName:         "rootCertName",
												IdentityInstanceName: "identityPluginInstance",
												IdentityCertName:     "identityCertName",
												RequireClientCert:    true,
											},
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						SecurityCfg: &SecurityConfig{
							RootInstanceName:     "defaultRootPluginInstance",
							RootCertName:         "defaultRootCertName",
							IdentityInstanceName: "defaultIdentityPluginInstance",
							IdentityCertName:     "defaultIdentityCertName",
							RequireClientCert:    true,
						},
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc:                      "validation context with certificate provider and system root certs",
			enableSystemRootCertsFlag: true,
			lis: &v3listenerpb.Listener{
				Name:    "listerner-1",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						TransportSocket: &v3corepb.TransportSocket{
							Name: "envoy.transport_sockets.tls",
							ConfigType: &v3corepb.TransportSocket_TypedConfig{
								TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
									RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
									CommonTlsContext: &v3tlspb.CommonTlsContext{
										TlsCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
											InstanceName:    "identityPluginInstance",
											CertificateName: "identityCertName",
										},
										ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContext{
											ValidationContext: &v3tlspb.CertificateValidationContext{
												CaCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
													InstanceName:    "rootPluginInstance",
													CertificateName: "rootCertName",
												},
												// SystemRootCerts will be ignored
												// when
												// CaCertificateProviderInstance is
												// set.
												SystemRootCerts: &v3tlspb.CertificateValidationContext_SystemRootCerts{},
											},
										},
									},
								}),
							},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{
					Name: "default-filter-chain-1",
					TransportSocket: &v3corepb.TransportSocket{
						Name: "envoy.transport_sockets.tls",
						ConfigType: &v3corepb.TransportSocket_TypedConfig{
							TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
								RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
								CommonTlsContext: &v3tlspb.CommonTlsContext{
									TlsCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
										InstanceName:    "defaultIdentityPluginInstance",
										CertificateName: "defaultIdentityCertName",
									},
									ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContext{
										ValidationContext: &v3tlspb.CertificateValidationContext{
											CaCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
												InstanceName:    "defaultRootPluginInstance",
												CertificateName: "defaultRootCertName",
											},
											// SystemRootCerts will be ignored
											// when
											// CaCertificateProviderInstance is
											// set.
											SystemRootCerts: &v3tlspb.CertificateValidationContext_SystemRootCerts{},
										},
									},
								},
							}),
						},
					},
					Filters: emptyValidNetworkFilters(t),
				},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											SecurityCfg: &SecurityConfig{
												RootInstanceName:     "rootPluginInstance",
												RootCertName:         "rootCertName",
												IdentityInstanceName: "identityPluginInstance",
												IdentityCertName:     "identityCertName",
												RequireClientCert:    true,
											},
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						SecurityCfg: &SecurityConfig{
							RootInstanceName:     "defaultRootPluginInstance",
							RootCertName:         "defaultRootCertName",
							IdentityInstanceName: "defaultIdentityPluginInstance",
							IdentityCertName:     "defaultIdentityCertName",
							RequireClientCert:    true,
						},
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			testutils.SetEnvConfig(t, &envconfig.XDSSystemRootCertsEnabled, test.enableSystemRootCertsFlag)
			resource := listenerProtoToAny(t, test.lis)
			_, gotListener, err := unmarshalListenerResource(resource, nil)
			if err != nil {
				t.Fatalf("unmarshalListenerResource(%s) failed with error: %v", pretty.ToJSON(resource), err)
			}
			if diff := cmp.Diff(test.wantListener, gotListener, cmpOptsIgnoreRawProto); diff != "" {
				t.Errorf("unmarshalListenerResource(%s), got unexpected update, diff (-want, +got):\n%s", pretty.ToJSON(resource), diff)
			}
		})
	}
}

// TestNewFilterChainImpl_Success_UnsupportedMatchFields verifies cases where
// there are multiple filter chains, and one of them is valid while the other
// contains unsupported match fields. These configurations should lead to
// success at config validation time and the filter chains which contains
// unsupported match fields will be skipped at lookup time.
func (s) TestUnmarshalListener_ServerSide_Success_UnsupportedMatchFields(t *testing.T) {
	tests := []struct {
		desc         string
		lis          *v3listenerpb.Listener
		wantListener ListenerUpdate
	}{
		{
			desc: "unsupported destination port",
			lis: &v3listenerpb.Listener{
				Name:    "listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name:    "good-chain",
						Filters: emptyValidNetworkFilters(t),
					},
					{
						Name: "unsupported-destination-port",
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:    []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							DestinationPort: &wrapperspb.UInt32Value{Value: 666},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "unsupported server names",
			lis: &v3listenerpb.Listener{
				Name:    "listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name:    "good-chain",
						Filters: emptyValidNetworkFilters(t),
					},
					{
						Name: "unsupported-server-names",
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							ServerNames:  []string{"example-server"},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "unsupported transport protocol",
			lis: &v3listenerpb.Listener{
				Name:    "listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name:    "good-chain",
						Filters: emptyValidNetworkFilters(t),
					},
					{
						Name: "unsupported-transport-protocol",
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:      []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							TransportProtocol: "tls",
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "unsupported application protocol",
			lis: &v3listenerpb.Listener{
				Name:    "listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						Name:    "good-chain",
						Filters: emptyValidNetworkFilters(t),
					},
					{
						Name: "unsupported-application-protocol",
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:         []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							ApplicationProtocols: []string{"h2"},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{{
							SourceTypeArr: [3]SourcePrefixes{{
								Entries: []SourcePrefixEntry{{
									PortMap: map[int]NetworkFilterChainConfig{
										0: {
											HTTPConnMgr: &HTTPConnectionManagerConfig{
												InlineRouteConfig: inlineRouteConfig,
												HTTPFilters:       makeRouterFilterList(t),
											},
										},
									}},
								},
							}},
						}},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			_, gotListener, err := unmarshalListenerResource(resource, nil)
			if err != nil {
				t.Fatalf("unmarshalListenerResource(%s) failed with error: %v", pretty.ToJSON(resource), err)
			}
			if diff := cmp.Diff(test.wantListener, gotListener, cmpOptsIgnoreRawProto); diff != "" {
				t.Errorf("unmarshalListenerResource(%s), got unexpected update, diff (-want, +got):\n%s", pretty.ToJSON(resource), diff)
			}
		})
	}
}

// TestNewFilterChainImpl_Success_AllCombinations verifies different
// combinations of the supported match criteria.
func (s) TestUnmarshalListener_ServerSide_Success_AllCombinations(t *testing.T) {
	tests := []struct {
		desc         string
		lis          *v3listenerpb.Listener
		wantListener ListenerUpdate
	}{
		{
			desc: "multiple destination prefixes",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						// Unspecified destination prefix.
						FilterChainMatch: &v3listenerpb.FilterChainMatch{},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						// v4 wildcard destination prefix.
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("0.0.0.0", 0)},
							SourceType:   v3listenerpb.FilterChainMatch_EXTERNAL,
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						// v6 wildcard destination prefix.
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("::", 0)},
							SourceType:   v3listenerpb.FilterChainMatch_EXTERNAL,
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)}},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("10.0.0.0", 8)}},
						Filters:          emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{
							{
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										PortMap: map[int]NetworkFilterChainConfig{
											0: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
							{
								Prefix: prefixFromCIDR("0.0.0.0/0"),
								SourceTypeArr: [3]SourcePrefixes{
									{},
									{},
									{
										Entries: []SourcePrefixEntry{{
											PortMap: map[int]NetworkFilterChainConfig{
												0: {
													HTTPConnMgr: &HTTPConnectionManagerConfig{
														InlineRouteConfig: inlineRouteConfig,
														HTTPFilters:       makeRouterFilterList(t),
													},
												},
											}},
										},
									},
								},
							},
							{
								Prefix: prefixFromCIDR("::/0"),
								SourceTypeArr: [3]SourcePrefixes{
									{},
									{},
									{
										Entries: []SourcePrefixEntry{{
											PortMap: map[int]NetworkFilterChainConfig{
												0: {
													HTTPConnMgr: &HTTPConnectionManagerConfig{
														InlineRouteConfig: inlineRouteConfig,
														HTTPFilters:       makeRouterFilterList(t),
													},
												},
											}},
										},
									},
								},
							},
							{
								Prefix: prefixFromCIDR("192.168.1.1/16"),
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										PortMap: map[int]NetworkFilterChainConfig{
											0: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
							{
								Prefix: prefixFromCIDR("10.0.0.0/8"),
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										PortMap: map[int]NetworkFilterChainConfig{
											0: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
						},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "multiple source types",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							SourceType:   v3listenerpb.FilterChainMatch_EXTERNAL,
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{
							{
								SourceTypeArr: [3]SourcePrefixes{
									{},
									{
										Entries: []SourcePrefixEntry{{
											PortMap: map[int]NetworkFilterChainConfig{
												0: {
													HTTPConnMgr: &HTTPConnectionManagerConfig{
														InlineRouteConfig: inlineRouteConfig,
														HTTPFilters:       makeRouterFilterList(t),
													},
												},
											}},
										},
									},
									{},
								},
							},
							{
								Prefix: prefixFromCIDR("192.168.1.1/16"),
								SourceTypeArr: [3]SourcePrefixes{
									{},
									{},
									{
										Entries: []SourcePrefixEntry{{
											PortMap: map[int]NetworkFilterChainConfig{
												0: {
													HTTPConnMgr: &HTTPConnectionManagerConfig{
														InlineRouteConfig: inlineRouteConfig,
														HTTPFilters:       makeRouterFilterList(t),
													},
												},
											}},
										},
									},
								},
							},
						},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "multiple source prefixes",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("10.0.0.0", 8)}},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:       []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{
							{
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										Prefix: prefixFromCIDR("10.0.0.0/8"),
										PortMap: map[int]NetworkFilterChainConfig{
											0: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
							{
								Prefix: prefixFromCIDR("192.168.1.1/16"),
								SourceTypeArr: [3]SourcePrefixes{
									{
										Entries: []SourcePrefixEntry{
											{
												Prefix: prefixFromCIDR("192.168.1.1/16"),
												PortMap: map[int]NetworkFilterChainConfig{
													0: {
														HTTPConnMgr: &HTTPConnectionManagerConfig{
															InlineRouteConfig: inlineRouteConfig,
															HTTPFilters:       makeRouterFilterList(t),
														},
													},
												},
											},
										},
									},
								},
							},
						},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "multiple source ports",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{SourcePorts: []uint32{1, 2, 3}},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:       []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)},
							SourceType:         v3listenerpb.FilterChainMatch_EXTERNAL,
							SourcePorts:        []uint32{1, 2, 3},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{
							{
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										PortMap: map[int]NetworkFilterChainConfig{
											1: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
											2: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
											3: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
							{
								Prefix: prefixFromCIDR("192.168.1.1/16"),
								SourceTypeArr: [3]SourcePrefixes{
									{},
									{},
									{
										Entries: []SourcePrefixEntry{
											{
												Prefix: prefixFromCIDR("192.168.1.1/16"),
												PortMap: map[int]NetworkFilterChainConfig{
													1: {
														HTTPConnMgr: &HTTPConnectionManagerConfig{
															InlineRouteConfig: inlineRouteConfig,
															HTTPFilters:       makeRouterFilterList(t),
														},
													},
													2: {
														HTTPConnMgr: &HTTPConnectionManagerConfig{
															InlineRouteConfig: inlineRouteConfig,
															HTTPFilters:       makeRouterFilterList(t),
														},
													},
													3: {
														HTTPConnMgr: &HTTPConnectionManagerConfig{
															InlineRouteConfig: inlineRouteConfig,
															HTTPFilters:       makeRouterFilterList(t),
														},
													},
												},
											},
										},
									},
								},
							},
						},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
		{
			desc: "some chains have unsupported fields",
			lis: &v3listenerpb.Listener{
				Name:    "test-listener",
				Address: localSocketAddress,
				FilterChains: []*v3listenerpb.FilterChain{
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.1.1", 16)}},
						Filters:          emptyValidNetworkFilters(t),
					},
					{
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:      []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("10.0.0.0", 8)},
							TransportProtocol: "raw_buffer",
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						// This chain will be dropped in favor of the above
						// filter chain because they both have the same
						// destination prefix, but this one has an empty
						// transport protocol while the above chain has the more
						// preferred "raw_buffer".
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:       []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("10.0.0.0", 8)},
							TransportProtocol:  "",
							SourceType:         v3listenerpb.FilterChainMatch_EXTERNAL,
							SourcePrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("10.0.0.0", 16)},
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						// This chain will be dropped for unsupported server
						// names.
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges: []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.100.1", 32)},
							ServerNames:  []string{"foo", "bar"},
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						// This chain will be dropped for unsupported transport
						// protocol.
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:      []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.100.2", 32)},
							TransportProtocol: "not-raw-buffer",
						},
						Filters: emptyValidNetworkFilters(t),
					},
					{
						// This chain will be dropped for unsupported
						// application protocol.
						FilterChainMatch: &v3listenerpb.FilterChainMatch{
							PrefixRanges:         []*v3corepb.CidrRange{cidrRangeFromAddressAndPrefixLen("192.168.100.3", 32)},
							ApplicationProtocols: []string{"h2"},
						},
						Filters: emptyValidNetworkFilters(t),
					},
				},
				DefaultFilterChain: &v3listenerpb.FilterChain{Filters: emptyValidNetworkFilters(t)},
			},
			wantListener: ListenerUpdate{
				TCPListener: &InboundListenerConfig{
					Address: "0.0.0.0",
					Port:    "9999",
					FilterChains: NetworkFilterChainMap{
						DstPrefixes: []DestinationPrefixEntry{
							{
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										PortMap: map[int]NetworkFilterChainConfig{
											0: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
							{
								Prefix: prefixFromCIDR("192.168.1.1/16"),
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										PortMap: map[int]NetworkFilterChainConfig{
											0: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
							{
								Prefix: prefixFromCIDR("10.0.0.0/8"),
								SourceTypeArr: [3]SourcePrefixes{{
									Entries: []SourcePrefixEntry{{
										PortMap: map[int]NetworkFilterChainConfig{
											0: {
												HTTPConnMgr: &HTTPConnectionManagerConfig{
													InlineRouteConfig: inlineRouteConfig,
													HTTPFilters:       makeRouterFilterList(t),
												},
											},
										}},
									},
								}},
							},
						},
					},
					DefaultFilterChain: NetworkFilterChainConfig{
						HTTPConnMgr: &HTTPConnectionManagerConfig{
							InlineRouteConfig: inlineRouteConfig,
							HTTPFilters:       makeRouterFilterList(t),
						},
					},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.desc, func(t *testing.T) {
			resource := listenerProtoToAny(t, test.lis)
			_, gotListener, err := unmarshalListenerResource(resource, nil)
			if err != nil {
				t.Fatalf("unmarshalListenerResource(%s) failed with error: %v", pretty.ToJSON(resource), err)
			}
			if diff := cmp.Diff(test.wantListener, gotListener, cmpOptsIgnoreRawProto); diff != "" {
				t.Errorf("unmarshalListenerResource(%s), got unexpected update, diff (-want, +got):\n%s", pretty.ToJSON(resource), diff)
			}
		})
	}
}

func listenerProtoToAny(t *testing.T, lis *v3listenerpb.Listener) *anypb.Any {
	t.Helper()
	mLis, err := proto.Marshal(lis)
	if err != nil {
		t.Fatalf("Failed to marshal listener proto %+v: %v", lis, err)
	}
	return &anypb.Any{
		TypeUrl: version.V3ListenerURL,
		Value:   mLis,
	}
}

func prefixFromCIDR(cidr string) netip.Prefix {
	return netip.MustParsePrefix(cidr).Masked()
}

func cidrRangeFromAddressAndPrefixLen(address string, len int) *v3corepb.CidrRange {
	return &v3corepb.CidrRange{
		AddressPrefix: address,
		PrefixLen: &wrapperspb.UInt32Value{
			Value: uint32(len),
		},
	}
}
