/*
 *
 * 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"
	"math"
	"net"
	"strconv"
	"testing"

	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"google.golang.org/grpc/experimental/balancer/hostname"
	"google.golang.org/grpc/internal/envconfig"
	"google.golang.org/grpc/internal/pretty"
	"google.golang.org/grpc/internal/testutils"
	"google.golang.org/grpc/internal/xds/clients"
	"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
	"google.golang.org/grpc/resolver"
	"google.golang.org/grpc/resolver/ringhash"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/structpb"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

// enableA86 enables A86 support for the duration of the test by:
//  1. Setting the GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT env var to true.
//  2. Registering the proxy address converter (and restore original
//     on cleanup).
func enableA86(t *testing.T) {
	testutils.SetEnvConfig(t, &envconfig.XDSHTTPConnectEnabled, true)
	cleanup, err := RegisterMetadataConverterForTesting(version.V3AddressURL)
	if err != nil {
		t.Fatalf("RegisterMetadataConverterForTesting(%q) failed: %v", version.V3AddressURL, err)
	}
	t.Cleanup(cleanup)
}

// disableA86 disables A86 support for the duration of the test by:
//  1. Setting the GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT env var to false.
//  2. Unregistering the proxy address converter for the duration of the test
//     (and restoring the original on cleanup).
func disableA86(t *testing.T) {
	testutils.SetEnvConfig(t, &envconfig.XDSHTTPConnectEnabled, false)
	cleanup := UnregisterMetadataConverterForTesting(version.V3AddressURL)
	t.Cleanup(cleanup)
}

func buildResolverEndpoint(addr []string, host string) resolver.Endpoint {
	address := []resolver.Address{}
	for _, a := range addr {
		address = append(address, resolver.Address{Addr: a})
	}
	resolverEndpoint := resolver.Endpoint{Addresses: address}
	resolverEndpoint = hostname.Set(resolverEndpoint, host)
	return resolverEndpoint
}

func (s) TestEDSParseRespProto(t *testing.T) {
	tests := []struct {
		name    string
		m       *v3endpointpb.ClusterLoadAssignment
		want    EndpointsUpdate
		wantErr bool
	}{
		{
			name: "missing-priority",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
				clab0.addLocality("locality-2", 1, 2, []endpointOpts{{addrWithPort: "addr2:159"}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "missing-locality-ID",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "zero-endpoint-weight",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-0", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, &addLocalityOptions{Weight: []uint32{0}})
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "duplicate-locality-in-the-same-priority",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-0", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
				clab0.addLocality("locality-0", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil) // Duplicate locality with the same priority.
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "missing locality weight",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints1 := []endpointOpts{{addrWithPort: "addr1:314"}}
				locOption1 := &addLocalityOptions{Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_HEALTHY}}
				clab0.addLocality("locality-1", 0, 1, endpoints1, locOption1)
				endpoints2 := []endpointOpts{{addrWithPort: "addr2:159"}}
				locOption2 := &addLocalityOptions{Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_HEALTHY}}
				clab0.addLocality("locality-2", 0, 0, endpoints2, locOption2)
				return clab0.Build()
			}(),
			want: EndpointsUpdate{},
		},
		{
			name: "max sum of endpoint weights within a locality exceeded",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{
					{addrWithPort: "addr1:314"},
					{addrWithPort: "addr2:159"},
				}
				locOption := &addLocalityOptions{
					Weight: []uint32{math.MaxUint32, 1},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "max sum of weights at the same priority exceeded",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
				clab0.addLocality("locality-2", 4294967295, 1, []endpointOpts{{addrWithPort: "addr2:159"}}, nil)
				clab0.addLocality("locality-3", 1, 1, []endpointOpts{{addrWithPort: "addr2:88"}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "duplicate endpoint address",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr:997"}}, nil)
				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr:997"}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "good",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints1 := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption1 := &addLocalityOptions{
					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
					Weight: []uint32{271},
				}
				clab0.addLocality("locality-1", 1, 1, endpoints1, locOption1)
				endpoints2 := []endpointOpts{{addrWithPort: "addr2:159", hostname: "addr2"}}
				locOption2 := &addLocalityOptions{
					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
					Weight: []uint32{828},
				}
				clab0.addLocality("locality-2", 1, 0, endpoints2, locOption2)
				return clab0.Build()
			}(),
			want: EndpointsUpdate{
				Drops: nil,
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnhealthy,
							Weight:           271,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 1,
						Weight:   1,
					},
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr2:159"}, "addr2"),
							HealthStatus:     EndpointHealthStatusDraining,
							Weight:           828,
						}},
						ID:       clients.Locality{SubZone: "locality-2"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
			wantErr: false,
		},
		{
			name: "good duplicate locality with different priority",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints1 := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption1 := &addLocalityOptions{
					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
					Weight: []uint32{271},
				}
				clab0.addLocality("locality-1", 1, 1, endpoints1, locOption1)
				// Same locality name, but with different priority.
				endpoints2 := []endpointOpts{{addrWithPort: "addr2:159", hostname: "addr2"}}
				locOption2 := &addLocalityOptions{
					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
					Weight: []uint32{828},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints2, locOption2)
				return clab0.Build()
			}(),
			want: EndpointsUpdate{
				Drops: nil,
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnhealthy,
							Weight:           271,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 1,
						Weight:   1,
					},
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr2:159"}, "addr2"),
							HealthStatus:     EndpointHealthStatusDraining,
							Weight:           828,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseEDSRespProto(tt.m)
			if (err != nil) != tt.wantErr {
				t.Fatalf("parseEDSRespProto() error = %v, wantErr %v", err, tt.wantErr)
			}
			if d := cmp.Diff(got, tt.want, cmpopts.EquateEmpty()); d != "" {
				t.Errorf("parseEDSRespProto() got = %v, want %v, diff: %v", got, tt.want, d)
			}
		})
	}
}

func (s) TestEDSParseRespProtoAdditionalAddrs(t *testing.T) {
	origDualstackEndpointsEnabled := envconfig.XDSDualstackEndpointsEnabled
	defer func() {
		envconfig.XDSDualstackEndpointsEnabled = origDualstackEndpointsEnabled
	}()
	envconfig.XDSDualstackEndpointsEnabled = true

	tests := []struct {
		name    string
		m       *v3endpointpb.ClusterLoadAssignment
		want    EndpointsUpdate
		wantErr bool
	}{
		{
			name: "duplicate primary address in self additional addresses",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:998"}}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "duplicate primary address in other locality additional addresses",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr:997"}}, nil)
				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:997"}}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "duplicate additional address in self additional addresses",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:999", "addr:999"}}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "duplicate additional address in other locality additional addresses",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 1, []endpointOpts{{addrWithPort: "addr:997", additionalAddrWithPorts: []string{"addr:1000"}}}, nil)
				clab0.addLocality("locality-2", 1, 0, []endpointOpts{{addrWithPort: "addr:998", additionalAddrWithPorts: []string{"addr:1000"}}}, nil)
				return clab0.Build()
			}(),
			want:    EndpointsUpdate{},
			wantErr: true,
		},
		{
			name: "multiple localities",
			m: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints1 := []endpointOpts{{addrWithPort: "addr1:997", additionalAddrWithPorts: []string{"addr1:1000"}, hostname: "addr1"}}
				locOption1 := &addLocalityOptions{
					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
					Weight: []uint32{271},
				}
				clab0.addLocality("locality-1", 1, 1, endpoints1, locOption1)
				endpoints2 := []endpointOpts{{addrWithPort: "addr2:998", additionalAddrWithPorts: []string{"addr2:1000"}, hostname: "addr2"}}
				locOption2 := &addLocalityOptions{
					Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_HEALTHY},
					Weight: []uint32{828},
				}
				clab0.addLocality("locality-2", 1, 0, endpoints2, locOption2)
				return clab0.Build()
			}(),
			want: EndpointsUpdate{
				Drops: nil,
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:997", "addr1:1000"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnhealthy,
							Weight:           271,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 1,
						Weight:   1,
					},
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr2:998", "addr2:1000"}, "addr2"),
							HealthStatus:     EndpointHealthStatusHealthy,
							Weight:           828,
						}},
						ID:       clients.Locality{SubZone: "locality-2"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseEDSRespProto(tt.m)
			if (err != nil) != tt.wantErr {
				t.Fatalf("parseEDSRespProto() error = %v, wantErr %v", err, tt.wantErr)
			}
			if d := cmp.Diff(got, tt.want, cmpopts.EquateEmpty()); d != "" {
				t.Errorf("parseEDSRespProto() got = %v, want %v, diff: %v", got, tt.want, d)
			}
		})
	}
}

func (s) TestUnmarshalEndpointHashKey(t *testing.T) {
	baseCLA := &v3endpointpb.ClusterLoadAssignment{
		ClusterName: "test-cluster",
		Endpoints: []*v3endpointpb.LocalityLbEndpoints{
			{
				Locality: &v3corepb.Locality{Region: "r"},
				LbEndpoints: []*v3endpointpb.LbEndpoint{
					{
						HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
							Endpoint: &v3endpointpb.Endpoint{
								Address: &v3corepb.Address{
									Address: &v3corepb.Address_SocketAddress{
										SocketAddress: &v3corepb.SocketAddress{
											Address: "test-address",
											PortSpecifier: &v3corepb.SocketAddress_PortValue{
												PortValue: 8080,
											},
										},
									},
								},
							},
						},
					},
				},
				LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1},
			},
		},
	}

	tests := []struct {
		name         string
		metadata     *v3corepb.Metadata
		wantHashKey  string
		compatEnvVar bool
	}{
		{
			name:        "no metadata",
			metadata:    nil,
			wantHashKey: "",
		},
		{
			name:        "empty metadata",
			metadata:    &v3corepb.Metadata{},
			wantHashKey: "",
		},
		{
			name: "filter metadata without envoy.lb",
			metadata: &v3corepb.Metadata{
				FilterMetadata: map[string]*structpb.Struct{
					"test-filter": {},
				},
			},
			wantHashKey: "",
		},
		{
			name: "nil envoy.lb",
			metadata: &v3corepb.Metadata{
				FilterMetadata: map[string]*structpb.Struct{
					"envoy.lb": nil,
				},
			},
			wantHashKey: "",
		},
		{
			name: "envoy.lb without hash key",
			metadata: &v3corepb.Metadata{
				FilterMetadata: map[string]*structpb.Struct{
					"envoy.lb": {
						Fields: map[string]*structpb.Value{
							"hash_key": {
								Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
							},
						},
					},
				},
			},
			wantHashKey: "",
		},
		{
			name: "envoy.lb with hash key, compat mode off",
			metadata: &v3corepb.Metadata{
				FilterMetadata: map[string]*structpb.Struct{
					"envoy.lb": {
						Fields: map[string]*structpb.Value{
							"hash_key": {
								Kind: &structpb.Value_StringValue{StringValue: "test-hash-key"},
							},
						},
					},
				},
			},
			wantHashKey: "test-hash-key",
		},
		{
			name: "envoy.lb with hash key, compat mode on",
			metadata: &v3corepb.Metadata{
				FilterMetadata: map[string]*structpb.Struct{
					"envoy.lb": {
						Fields: map[string]*structpb.Value{
							"hash_key": {
								Kind: &structpb.Value_StringValue{StringValue: "test-hash-key"},
							},
						},
					},
				},
			},
			wantHashKey:  "",
			compatEnvVar: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			testutils.SetEnvConfig(t, &envconfig.XDSEndpointHashKeyBackwardCompat, test.compatEnvVar)

			cla := proto.Clone(baseCLA).(*v3endpointpb.ClusterLoadAssignment)
			cla.Endpoints[0].LbEndpoints[0].Metadata = test.metadata
			marshalledCLA := testutils.MarshalAny(t, cla)
			_, update, err := unmarshalEndpointsResource(marshalledCLA)
			if err != nil {
				t.Fatalf("unmarshalEndpointsResource() got error = %v, want success", err)
			}
			got := ringhash.HashKey(update.Localities[0].Endpoints[0].ResolverEndpoint)
			if got != test.wantHashKey {
				t.Errorf("unmarshalEndpointResource() endpoint hash key: got %s, want %s", got, test.wantHashKey)
			}
		})
	}
}

func (s) TestUnmarshalEndpoints(t *testing.T) {
	enableA86(t)
	var v3EndpointsAny = testutils.MarshalAny(t, func() *v3endpointpb.ClusterLoadAssignment {
		clab0 := newClaBuilder("test", nil)
		endpoints1 := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
		locOption1 := &addLocalityOptions{
			Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_UNHEALTHY},
			Weight: []uint32{271},
		}
		clab0.addLocality("locality-1", 1, 1, endpoints1, locOption1)
		endpoints2 := []endpointOpts{{addrWithPort: "addr2:159", hostname: "addr2"}}
		locOption2 := &addLocalityOptions{
			Health: []v3corepb.HealthStatus{v3corepb.HealthStatus_DRAINING},
			Weight: []uint32{828},
		}
		clab0.addLocality("locality-2", 1, 0, endpoints2, locOption2)
		return clab0.Build()
	}())

	tests := []struct {
		name       string
		resource   *anypb.Any
		wantName   string
		wantUpdate EndpointsUpdate
		wantErr    bool
	}{
		{
			name:     "non-clusterLoadAssignment_resourcetype",
			resource: &anypb.Any{TypeUrl: version.V3HTTPConnManagerURL},
			wantErr:  true,
		},
		{
			name: "badly_marshaled_clusterLoadAssignment_resource",
			resource: &anypb.Any{
				TypeUrl: version.V3EndpointsURL,
				Value:   []byte{1, 2, 3, 4},
			},
			wantErr: true,
		},
		{
			name: "bad_endpoints_resource",
			resource: testutils.MarshalAny(t, func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
				clab0.addLocality("locality-2", 1, 2, []endpointOpts{{addrWithPort: "addr2:159"}}, nil)
				return clab0.Build()
			}()),
			wantName: "test",
			wantErr:  true,
		},
		{
			name: "endpoint_resource_with_empty_cluster_name",
			resource: testutils.MarshalAny(t, func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("", nil)
				clab0.addLocality("locality-1", 1, 0, []endpointOpts{{addrWithPort: "addr1:314"}}, nil)
				clab0.addLocality("locality-2", 1, 2, []endpointOpts{{addrWithPort: "addr2:159"}}, nil)
				return clab0.Build()
			}()),
			wantName: "",
			wantErr:  true,
		},
		{
			name:     "v3_endpoints",
			resource: v3EndpointsAny,
			wantName: "test",
			wantUpdate: EndpointsUpdate{
				Drops: nil,
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnhealthy,
							Weight:           271,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 1,
						Weight:   1,
					},
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr2:159"}, "addr2"),
							HealthStatus:     EndpointHealthStatusDraining,
							Weight:           828,
						}},
						ID:       clients.Locality{SubZone: "locality-2"},
						Priority: 0,
						Weight:   1,
					},
				},
				Raw: v3EndpointsAny,
			},
		},
		{
			name:     "v3_endpoints_wrapped",
			resource: testutils.MarshalAny(t, &v3discoverypb.Resource{Resource: v3EndpointsAny}),
			wantName: "test",
			wantUpdate: EndpointsUpdate{
				Drops: nil,
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnhealthy,
							Weight:           271,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 1,
						Weight:   1,
					},
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr2:159"}, "addr2"),
							HealthStatus:     EndpointHealthStatusDraining,
							Weight:           828,
						}},
						ID:       clients.Locality{SubZone: "locality-2"},
						Priority: 0,
						Weight:   1,
					},
				},
				Raw: v3EndpointsAny,
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			name, update, err := unmarshalEndpointsResource(test.resource)
			if (err != nil) != test.wantErr {
				t.Fatalf("unmarshalEndpointsResource(%s), got err: %v, wantErr: %v", pretty.ToJSON(test.resource), err, test.wantErr)
			}
			if name != test.wantName {
				t.Errorf("unmarshalEndpointsResource(%s), got name: %s, want: %s", pretty.ToJSON(test.resource), name, test.wantName)
			}
			if diff := cmp.Diff(update, test.wantUpdate, cmpOpts); diff != "" {
				t.Errorf("unmarshalEndpointsResource(%s), got unexpected update, diff (-got +want): %v", pretty.ToJSON(test.resource), diff)
			}
		})
	}
}

// Tests custom metadata parsing for success cases when the
// GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT environment variable is set.
func (s) TestEDSParseRespProto_HTTP_Connect_CustomMetadata_EnvVarOn(t *testing.T) {
	enableA86(t)
	tests := []struct {
		name          string
		endpointProto *v3endpointpb.ClusterLoadAssignment
		wantEndpoint  EndpointsUpdate
	}{
		{
			name: "typed_filter_metadata_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
					},
					hostname: "addr1",
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
							Metadata: map[string]any{
								"test-key": ProxyAddressMetadataValue{
									Address: "1.2.3.4:1111",
								},
							},
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "filter_metadata_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						FilterMetadata: map[string]*structpb.Struct{
							"test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
					hostname: "addr1",
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
							Metadata: map[string]any{
								"test-key": StructMetadataValue{Data: map[string]any{
									"key": float64(123),
								}},
							},
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "typed_filter_metadata_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
						Metadata: map[string]any{
							"test-key": ProxyAddressMetadataValue{
								Address: "1.2.3.4:1111",
							},
						},
					},
				},
			},
		},
		{
			name: "filter_metadata_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						FilterMetadata: map[string]*structpb.Struct{
							"test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
						Metadata: map[string]any{
							"test-key": StructMetadataValue{Data: map[string]any{
								"key": float64(123),
							}},
						},
					},
				},
			},
		},
		{
			name: "typed_filter_metadata_over_filter_metadata_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
						FilterMetadata: map[string]*structpb.Struct{
							"test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
					hostname: "addr1",
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
							Metadata: map[string]any{
								"test-key": ProxyAddressMetadataValue{
									Address: "1.2.3.4:1111",
								},
							},
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "typed_filter_metadata_over_filter_metadata_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
						FilterMetadata: map[string]*structpb.Struct{
							"test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
						Metadata: map[string]any{
							"test-key": ProxyAddressMetadataValue{
								Address: "1.2.3.4:1111",
							},
						},
					},
				},
			},
		},
		{
			name: "both_filter_and_typed_filter_metadata_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
						FilterMetadata: map[string]*structpb.Struct{
							"another-test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
					hostname: "addr1",
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
							Metadata: map[string]any{
								"test-key": ProxyAddressMetadataValue{
									Address: "1.2.3.4:1111",
								},
								"another-test-key": StructMetadataValue{Data: map[string]any{
									"key": float64(123),
								}},
							},
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "both_filter_and_typed_filter_metadata_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
						FilterMetadata: map[string]*structpb.Struct{
							"another-test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
						Metadata: map[string]any{
							"test-key": ProxyAddressMetadataValue{
								Address: "1.2.3.4:1111",
							},
							"another-test-key": StructMetadataValue{Data: map[string]any{
								"key": float64(123),
							}},
						},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseEDSRespProto(tt.endpointProto)
			if err != nil {
				t.Fatalf("parseEDSRespProto() failed: %v", err)
			}
			if diff := cmp.Diff(tt.wantEndpoint, got, cmpopts.EquateEmpty()); diff != "" {
				t.Errorf("parseEDSRespProto() returned unexpected diff (-want +got):\n%s", diff)
			}
		})
	}
}

// Tests custom metadata parsing for success cases when the
// GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT environment variable is not set and
// GRPC_XDS_ENDPOINT_HASH_KEY_BACKWARD_COMPAT is set to true (disabling A76).
func (s) TestEDSParseRespProto_HTTP_Connect_CustomMetadata_EnvVarOff(t *testing.T) {
	disableA86(t)
	testutils.SetEnvConfig(t, &envconfig.XDSEndpointHashKeyBackwardCompat, true)
	tests := []struct {
		name          string
		endpointProto *v3endpointpb.ClusterLoadAssignment
		wantEndpoint  EndpointsUpdate
	}{
		{
			name: "typed_filter_metadata_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
					},
					hostname: "addr1",
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "filter_metadata_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						FilterMetadata: map[string]*structpb.Struct{
							"test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
					hostname: "addr1",
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "typed_filter_metadata_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "filter_metadata_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						FilterMetadata: map[string]*structpb.Struct{
							"test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "both_filter_and_typed_filter_metadata_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
						FilterMetadata: map[string]*structpb.Struct{
							"another-test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
					hostname: "addr1",
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "both_filter_and_typed_filter_metadata_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314", hostname: "addr1"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"test-key": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "1.2.3.4",
										PortSpecifier: &v3corepb.SocketAddress_PortValue{
											PortValue: 1111,
										}},
								},
							}),
						},
						FilterMetadata: map[string]*structpb.Struct{
							"another-test-key": {
								Fields: map[string]*structpb.Value{
									"key": {
										Kind: &structpb.Value_NumberValue{NumberValue: 123.0},
									},
								},
							},
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
		{
			name: "converter_failure_is_not_triggerred",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"envoy.http11_proxy_transport_socket.proxy_address": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "invalid",
									},
								},
							}),
						},
					},
					hostname: "addr1",
				}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"envoy.http11_proxy_transport_socket.proxy_address": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "invalid",
									},
								},
							}),
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
			wantEndpoint: EndpointsUpdate{
				Localities: []Locality{
					{
						Endpoints: []Endpoint{{
							ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
							HealthStatus:     EndpointHealthStatusUnknown,
							Weight:           1,
						}},
						ID:       clients.Locality{SubZone: "locality-1"},
						Priority: 0,
						Weight:   1,
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseEDSRespProto(tt.endpointProto)
			if err != nil {
				t.Fatalf("parseEDSRespProto() failed: %v", err)
			}
			if diff := cmp.Diff(tt.wantEndpoint, got, cmpopts.EquateEmpty()); diff != "" {
				t.Errorf("parseEDSRespProto() returned unexpected diff (-want +got):\n%s", diff)
			}
		})
	}
}

// Tests custom metadata parsing for converter failure cases when the
// GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT environment variable is set.
func (s) TestEDSParseRespProto_HTTP_Connect_CustomMetadata_ConverterFailure(t *testing.T) {
	enableA86(t)
	tests := []struct {
		name          string
		endpointProto *v3endpointpb.ClusterLoadAssignment
	}{
		{
			name: "converter_failure_in_endpoint",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{
					addrWithPort: "addr1:314",
					metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"envoy.http11_proxy_transport_socket.proxy_address": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "invalid",
									},
								},
							}),
						},
					},
				}}
				clab0.addLocality("locality-1", 1, 0, endpoints, nil)
				return clab0.Build()
			}(),
		},
		{
			name: "converter_failure_in_locality",
			endpointProto: func() *v3endpointpb.ClusterLoadAssignment {
				clab0 := newClaBuilder("test", nil)
				endpoints := []endpointOpts{{addrWithPort: "addr1:314"}}
				locOption := &addLocalityOptions{
					Metadata: &v3corepb.Metadata{
						TypedFilterMetadata: map[string]*anypb.Any{
							"envoy.http11_proxy_transport_socket.proxy_address": testutils.MarshalAny(t, &v3corepb.Address{
								Address: &v3corepb.Address_SocketAddress{
									SocketAddress: &v3corepb.SocketAddress{
										Address: "invalid",
									},
								},
							}),
						},
					},
				}
				clab0.addLocality("locality-1", 1, 0, endpoints, locOption)
				return clab0.Build()
			}(),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if _, err := parseEDSRespProto(tt.endpointProto); err == nil {
				t.Fatalf("parseEDSRespProto() did not return error when expected")
			}
		})
	}
}

// Tests metadata parsing when HTTP Connect is enabled but A76 hash key is
// disabled (backward compat mode). This verifies that:
// - Metadata parsing happens (TypedFilterMetadata + FilterMetadata)
// - Hash key is NOT extracted from envoy.lb
func (s) TestEDSParseRespProto_HTTP_Connect_On_HashKeyBackwardCompat_On(t *testing.T) {
	enableA86(t)
	testutils.SetEnvConfig(t, &envconfig.XDSEndpointHashKeyBackwardCompat, true)

	clab0 := newClaBuilder("test", nil)
	endpoints := []endpointOpts{{
		addrWithPort: "addr1:314",
		metadata: &v3corepb.Metadata{
			TypedFilterMetadata: map[string]*anypb.Any{
				"envoy.http11_proxy_transport_socket.proxy_address": testutils.MarshalAny(t, &v3corepb.Address{
					Address: &v3corepb.Address_SocketAddress{
						SocketAddress: &v3corepb.SocketAddress{
							Address: "1.2.3.4",
							PortSpecifier: &v3corepb.SocketAddress_PortValue{
								PortValue: 1111,
							},
						},
					},
				}),
			},
			FilterMetadata: map[string]*structpb.Struct{
				"envoy.lb": {
					Fields: map[string]*structpb.Value{
						"hash_key": {
							Kind: &structpb.Value_StringValue{StringValue: "test-hash-key"},
						},
					},
				},
			},
		},
		hostname: "addr1",
	}}
	clab0.addLocality("locality-1", 1, 0, endpoints, nil)

	got, err := parseEDSRespProto(clab0.Build())
	if err != nil {
		t.Fatalf("parseEDSRespProto() failed: %v", err)
	}

	wantEndpoint := EndpointsUpdate{
		Localities: []Locality{
			{
				Endpoints: []Endpoint{{
					ResolverEndpoint: buildResolverEndpoint([]string{"addr1:314"}, "addr1"),
					HealthStatus:     EndpointHealthStatusUnknown,
					Weight:           1,
					Metadata: map[string]any{
						"envoy.http11_proxy_transport_socket.proxy_address": ProxyAddressMetadataValue{
							Address: "1.2.3.4:1111",
						},
						"envoy.lb": StructMetadataValue{Data: map[string]any{
							"hash_key": "test-hash-key",
						}},
					},
				}},
				ID:       clients.Locality{SubZone: "locality-1"},
				Priority: 0,
				Weight:   1,
			},
		},
	}

	if diff := cmp.Diff(wantEndpoint, got, cmpopts.EquateEmpty()); diff != "" {
		t.Errorf("parseEDSRespProto() returned unexpected diff (-want +got):\n%s", diff)
	}

	// Verify hash key is NOT extracted when backward compat is on.
	hashKey := ringhash.HashKey(got.Localities[0].Endpoints[0].ResolverEndpoint)
	if hashKey != "" {
		t.Errorf("Expected empty hash key with backward compat on, got %q", hashKey)
	}
}

// Tests that when A76 is enabled but A86 is disabled, invalid typed metadata
// does not cause a parsing failure, and hash key is still extracted.
func (s) TestEDSParseRespProto_HTTP_Connect_Off_HashKeyBackwardCompat_Off_InvalidTypedMetadata(t *testing.T) {
	disableA86(t)
	testutils.SetEnvConfig(t, &envconfig.XDSEndpointHashKeyBackwardCompat, false) // A76 on

	clab0 := newClaBuilder("test", nil)
	endpoints := []endpointOpts{{
		addrWithPort: "addr1:314",
		metadata: &v3corepb.Metadata{
			TypedFilterMetadata: map[string]*anypb.Any{
				"envoy.http11_proxy_transport_socket.proxy_address": testutils.MarshalAny(t, &v3corepb.Address{
					Address: &v3corepb.Address_SocketAddress{
						SocketAddress: &v3corepb.SocketAddress{
							Address: "invalid", // This would fail conversion if processed.
						},
					},
				}),
			},
			FilterMetadata: map[string]*structpb.Struct{
				"envoy.lb": {
					Fields: map[string]*structpb.Value{
						"hash_key": {
							Kind: &structpb.Value_StringValue{StringValue: "test-hash-key"},
						},
					},
				},
			},
		},
		hostname: "addr1",
	}}
	clab0.addLocality("locality-1", 1, 0, endpoints, nil)

	got, err := parseEDSRespProto(clab0.Build())
	if err != nil {
		t.Fatalf("parseEDSRespProto() failed unexpectedly with A76 on and A86 off: %v", err)
	}

	// Verify hash key is extracted when A76 is on.
	hashKey := ringhash.HashKey(got.Localities[0].Endpoints[0].ResolverEndpoint)
	if want := "test-hash-key"; hashKey != want {
		t.Errorf("Expected hash key %q with A76 on, got %q", want, hashKey)
	}
}

// claBuilder builds a ClusterLoadAssignment, aka EDS
// response.
type claBuilder struct {
	v *v3endpointpb.ClusterLoadAssignment
}

// newClaBuilder creates a claBuilder.
func newClaBuilder(clusterName string, dropPercents []uint32) *claBuilder {
	var drops []*v3endpointpb.ClusterLoadAssignment_Policy_DropOverload
	for i, d := range dropPercents {
		drops = append(drops, &v3endpointpb.ClusterLoadAssignment_Policy_DropOverload{
			Category: fmt.Sprintf("test-drop-%d", i),
			DropPercentage: &v3typepb.FractionalPercent{
				Numerator:   d,
				Denominator: v3typepb.FractionalPercent_HUNDRED,
			},
		})
	}

	return &claBuilder{
		v: &v3endpointpb.ClusterLoadAssignment{
			ClusterName: clusterName,
			Policy: &v3endpointpb.ClusterLoadAssignment_Policy{
				DropOverloads: drops,
			},
		},
	}
}

// addLocalityOptions contains options when adding locality to the builder.
type addLocalityOptions struct {
	Health   []v3corepb.HealthStatus
	Weight   []uint32
	Metadata *v3corepb.Metadata
}

type endpointOpts struct {
	addrWithPort            string
	additionalAddrWithPorts []string
	metadata                *v3corepb.Metadata
	hostname                string
}

func addressFromStr(addrWithPort string) *v3corepb.Address {
	host, portStr, err := net.SplitHostPort(addrWithPort)
	if err != nil {
		panic("failed to split " + addrWithPort)
	}
	port, err := strconv.Atoi(portStr)
	if err != nil {
		panic("failed to atoi " + portStr)
	}

	return &v3corepb.Address{
		Address: &v3corepb.Address_SocketAddress{
			SocketAddress: &v3corepb.SocketAddress{
				Protocol:      v3corepb.SocketAddress_TCP,
				Address:       host,
				PortSpecifier: &v3corepb.SocketAddress_PortValue{PortValue: uint32(port)},
			},
		},
	}
}

// addLocality adds a locality to the builder.
func (clab *claBuilder) addLocality(subzone string, weight uint32, priority uint32, endpoints []endpointOpts, opts *addLocalityOptions) {
	var lbEndPoints []*v3endpointpb.LbEndpoint
	for i, e := range endpoints {
		var additionalAddrs []*v3endpointpb.Endpoint_AdditionalAddress
		for _, a := range e.additionalAddrWithPorts {
			additionalAddrs = append(additionalAddrs, &v3endpointpb.Endpoint_AdditionalAddress{
				Address: addressFromStr(a),
			})
		}
		lbe := &v3endpointpb.LbEndpoint{
			HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{
				Endpoint: &v3endpointpb.Endpoint{
					Address:             addressFromStr(e.addrWithPort),
					AdditionalAddresses: additionalAddrs,
					Hostname:            e.hostname,
				},
			},
			Metadata: e.metadata,
		}
		if opts != nil {
			if i < len(opts.Health) {
				lbe.HealthStatus = opts.Health[i]
			}
			if i < len(opts.Weight) {
				lbe.LoadBalancingWeight = &wrapperspb.UInt32Value{Value: opts.Weight[i]}
			}
		}
		lbEndPoints = append(lbEndPoints, lbe)
	}

	var localityID *v3corepb.Locality
	if subzone != "" {
		localityID = &v3corepb.Locality{
			Region:  "",
			Zone:    "",
			SubZone: subzone,
		}
	}

	locality := &v3endpointpb.LocalityLbEndpoints{
		Locality:            localityID,
		LbEndpoints:         lbEndPoints,
		LoadBalancingWeight: &wrapperspb.UInt32Value{Value: weight},
		Priority:            priority,
	}
	if opts != nil {
		locality.Metadata = opts.Metadata
	}
	clab.v.Endpoints = append(clab.v.Endpoints, locality)
}

// Build builds ClusterLoadAssignment.
func (clab *claBuilder) Build() *v3endpointpb.ClusterLoadAssignment {
	return clab.v
}
