/*
 *
 * Copyright 2024 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 xdsclient_fallback_test

import (
	"context"
	"fmt"
	"sync/atomic"
	"testing"
	"time"

	"github.com/google/uuid"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/internal"
	"google.golang.org/grpc/internal/grpctest"
	"google.golang.org/grpc/internal/stubserver"
	"google.golang.org/grpc/internal/testutils"
	"google.golang.org/grpc/internal/testutils/xds/e2e"
	"google.golang.org/grpc/internal/xds/bootstrap"
	"google.golang.org/grpc/internal/xds/xdsclient"
	"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/resolver"
	"google.golang.org/grpc/status"

	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	testgrpc "google.golang.org/grpc/interop/grpc_testing"
	testpb "google.golang.org/grpc/interop/grpc_testing"

	_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter.
	_ "google.golang.org/grpc/xds"                            // To ensure internal.NewXDSResolverWithConfigForTesting is set.
)

type s struct {
	grpctest.Tester
}

func Test(t *testing.T) {
	grpctest.RunSubTests(t, s{})
}

// Give the fallback tests additional time to complete because they need to
// first identify failed connections before establishing new ones.
const defaultFallbackTestTimeout = 20 * time.Second
const defaultTestShortTimeout = 10 * time.Millisecond // For events expected to *not* happen.

func waitForRPCsToReachBackend(ctx context.Context, client testgrpc.TestServiceClient, backend string) error {
	var lastErr error
	for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
		var peer peer.Peer
		if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&peer)); err != nil {
			lastErr = err
			continue
		}
		// Veirfy the peer when the RPC succeeds.
		if peer.Addr.String() == backend {
			break
		}
	}
	if ctx.Err() != nil {
		return fmt.Errorf("timeout when waiting for RPCs to reach expected backend. Last error: %v", lastErr)
	}
	return nil
}

// Tests fallback on startup where the xDS client is unable to establish a
// connection to the primary server. The test verifies that the xDS client falls
// back to the secondary server, and when the primary comes back up, it reverts
// to it. The test also verifies that when all requested resources are cached
// from the primary, fallback is not triggered when the connection goes down.
func (s) TestFallback_OnStartup(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultFallbackTestTimeout)
	defer cancel()

	// Create two listeners for the two management servers. The test can
	// start/stop these listeners and can also get notified when the listener
	// receives a connection request.
	primaryWrappedLis := testutils.NewListenerWrapper(t, nil)
	primaryLis := testutils.NewRestartableListener(primaryWrappedLis)
	fallbackWrappedLis := testutils.NewListenerWrapper(t, nil)
	fallbackLis := testutils.NewRestartableListener(fallbackWrappedLis)

	// Start two management servers, primary and fallback, with the above
	// listeners.
	primaryManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: primaryLis})
	fallbackManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: fallbackLis})

	// Start two test service backends.
	backend1 := stubserver.StartTestService(t, nil)
	defer backend1.Stop()
	backend2 := stubserver.StartTestService(t, nil)
	defer backend2.Stop()

	// Configure xDS resource on the primary management server, with a cluster
	// resource that contains an endpoint for backend1.
	nodeID := uuid.New().String()
	const serviceName = "my-service-fallback-xds"
	resources := e2e.DefaultClientResources(e2e.ResourceParams{
		DialTarget: serviceName,
		NodeID:     nodeID,
		Host:       "localhost",
		Port:       testutils.ParsePort(t, backend1.Address),
		SecLevel:   e2e.SecurityLevelNone,
	})
	if err := primaryManagementServer.Update(ctx, resources); err != nil {
		t.Fatal(err)
	}

	// Configure xDS resource on the secondary management server, with a cluster
	// resource that contains an endpoint for backend2. Only the listener
	// resource has the same name on both servers.
	fallbackRouteConfigName := "fallback-route-" + serviceName
	fallbackClusterName := "fallback-cluster-" + serviceName
	fallbackEndpointsName := "fallback-endpoints-" + serviceName
	resources = e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, fallbackRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(fallbackRouteConfigName, serviceName, fallbackClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(fallbackClusterName, fallbackEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(fallbackEndpointsName, "localhost", []uint32{testutils.ParsePort(t, backend2.Address)})},
	}
	if err := fallbackManagementServer.Update(ctx, resources); err != nil {
		t.Fatal(err)
	}

	// Shut both management servers down before starting the gRPC client to
	// trigger fallback on startup.
	primaryLis.Stop()
	fallbackLis.Stop()

	// Generate bootstrap configuration with the above two servers.
	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
		Servers: []byte(fmt.Sprintf(`[
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		},
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		}]`, primaryManagementServer.Address, fallbackManagementServer.Address)),
		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
	})
	if err != nil {
		t.Fatalf("Failed to create bootstrap file: %v", err)
	}

	// Create an xDS client with the above bootstrap configuration.
	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
	if err != nil {
		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
	}
	pool := xdsclient.NewPool(config)
	if err != nil {
		t.Fatalf("Failed to create xDS client: %v", err)
	}

	// Get the xDS resolver to use the above xDS client.
	resolverBuilder := internal.NewXDSResolverWithPoolForTesting.(func(*xdsclient.Pool) (resolver.Builder, error))
	resolver, err := resolverBuilder(pool)
	if err != nil {
		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
	}

	// Start a gRPC client that uses the above xDS resolver.
	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
	if err != nil {
		t.Fatalf("Failed to create gRPC client: %v", err)
	}
	defer cc.Close()
	cc.Connect()

	// Ensure that a connection is attempted to the primary.
	if _, err := primaryWrappedLis.NewConnCh.Receive(ctx); err != nil {
		t.Fatalf("Failure when waiting for a connection to be opened to the primary management server: %v", err)
	}

	// Ensure that a connection is attempted to the fallback.
	if _, err := fallbackWrappedLis.NewConnCh.Receive(ctx); err != nil {
		t.Fatalf("Failure when waiting for a connection to be opened to the primary management server: %v", err)
	}

	// Make an RPC with a shortish deadline and expect it to fail.
	sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
	defer sCancel()
	client := testgrpc.NewTestServiceClient(cc)
	if _, err := client.EmptyCall(sCtx, &testpb.Empty{}, grpc.WaitForReady(true)); err == nil || status.Code(err) != codes.DeadlineExceeded {
		t.Fatalf("EmptyCall() = %v, want DeadlineExceeded", err)
	}

	// Start the fallback server. Ensure that an RPC can succeed, and that it
	// reaches backend2.
	fallbackLis.Restart()
	if err := waitForRPCsToReachBackend(ctx, client, backend2.Address); err != nil {
		t.Fatal(err)
	}

	// Start the primary server. It can take a while before the xDS client
	// notices this, since the ADS stream implementation uses a backoff before
	// retrying the stream.
	primaryLis.Restart()

	// Wait for the connection to the secondary to be closed and ensure that an
	// RPC can succeed, and that it reaches backend1.
	c, err := fallbackWrappedLis.NewConnCh.Receive(ctx)
	if err != nil {
		t.Fatalf("Failure when retrieving the most recent connection to the fallback management server: %v", err)
	}
	conn := c.(*testutils.ConnWrapper)
	if _, err := conn.CloseCh.Receive(ctx); err != nil {
		t.Fatalf("Connection to fallback server not closed once primary becomes ready: %v", err)
	}
	if err := waitForRPCsToReachBackend(ctx, client, backend1.Address); err != nil {
		t.Fatal(err)
	}

	// Stop the primary servers. Since all xDS resources were received from the
	// primary (and RPCs were succeeding to the clusters returned by the
	// primary), we will not trigger fallback.
	primaryLis.Stop()
	sCtx, sCancel = context.WithTimeout(ctx, defaultTestShortTimeout)
	defer sCancel()
	if _, err := fallbackWrappedLis.NewConnCh.Receive(sCtx); err == nil {
		t.Fatalf("Fallback attempted when not expected to. There are no uncached resources from the primary server at this point.")
	}

	// Ensure that RPCs still succeed, and that they use the configuration
	// received from the primary.
	if err := waitForRPCsToReachBackend(ctx, client, backend1.Address); err != nil {
		t.Fatal(err)
	}
}

// Tests fallback when the primary management server fails during an update.
func (s) TestFallback_MidUpdate(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultFallbackTestTimeout)
	defer cancel()

	// Create two listeners for the two management servers. The test can
	// start/stop these listeners and can also get notified when the listener
	// receives a connection request.
	primaryWrappedLis := testutils.NewListenerWrapper(t, nil)
	primaryLis := testutils.NewRestartableListener(primaryWrappedLis)
	fallbackWrappedLis := testutils.NewListenerWrapper(t, nil)
	fallbackLis := testutils.NewRestartableListener(fallbackWrappedLis)

	// This boolean helps with triggering fallback mid update. When this boolean
	// is set and the below defined cluster resource is requested, the primary
	// management server shuts down the connection, forcing the client to
	// fallback to the secondary server.
	var closeConnOnMidUpdateClusterResource atomic.Bool
	const (
		serviceName              = "my-service-fallback-xds"
		routeConfigName          = "route-" + serviceName
		clusterName              = "cluster-" + serviceName
		endpointsName            = "endpoints-" + serviceName
		midUpdateRouteConfigName = "mid-update-route-" + serviceName
		midUpdateClusterName     = "mid-update-cluster-" + serviceName
		midUpdateEndpointsName   = "mid-update-endpoints-" + serviceName
	)

	// Start two management servers, primary and fallback, with the above
	// listeners.
	primaryManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
		Listener: primaryLis,
		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
			if closeConnOnMidUpdateClusterResource.Load() == false {
				return nil
			}
			if req.GetTypeUrl() != version.V3ClusterURL {
				return nil
			}
			for _, name := range req.GetResourceNames() {
				if name == midUpdateClusterName {
					primaryLis.Stop()
					return fmt.Errorf("closing ADS stream because %q resource was requested", midUpdateClusterName)
				}
			}
			return nil
		},
		AllowResourceSubset: true,
	})
	fallbackManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: fallbackLis})

	// Start three test service backends.
	backend1 := stubserver.StartTestService(t, nil)
	defer backend1.Stop()
	backend2 := stubserver.StartTestService(t, nil)
	defer backend2.Stop()
	backend3 := stubserver.StartTestService(t, nil)
	defer backend3.Stop()

	// Configure xDS resource on the primary management server, with a cluster
	// resource that contains an endpoint for backend1.
	nodeID := uuid.New().String()
	primaryResources := e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, endpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(endpointsName, "localhost", []uint32{testutils.ParsePort(t, backend1.Address)})},
	}
	if err := primaryManagementServer.Update(ctx, primaryResources); err != nil {
		t.Fatal(err)
	}

	// Configure xDS resource on the secondary management server, with a cluster
	// resource that contains an endpoint for backend2. Only the listener
	// resource has the same name on both servers.
	const (
		fallbackRouteConfigName = "fallback-route-" + serviceName
		fallbackClusterName     = "fallback-cluster-" + serviceName
		fallbackEndpointsName   = "fallback-endpoints-" + serviceName
	)
	fallbackResources := e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, fallbackRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(fallbackRouteConfigName, serviceName, fallbackClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(fallbackClusterName, fallbackEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(fallbackEndpointsName, "localhost", []uint32{testutils.ParsePort(t, backend2.Address)})},
	}
	if err := fallbackManagementServer.Update(ctx, fallbackResources); err != nil {
		t.Fatal(err)
	}

	// Generate bootstrap configuration with the above two servers.
	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
		Servers: []byte(fmt.Sprintf(`[
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		},
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		}]`, primaryManagementServer.Address, fallbackManagementServer.Address)),
		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
	})
	if err != nil {
		t.Fatalf("Failed to create bootstrap file: %v", err)
	}

	// Create an xDS client with the above bootstrap configuration.
	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
	if err != nil {
		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
	}
	pool := xdsclient.NewPool(config)
	if err != nil {
		t.Fatalf("Failed to create xDS client: %v", err)
	}

	// Get the xDS resolver to use the above xDS client.
	resolverBuilder := internal.NewXDSResolverWithPoolForTesting.(func(*xdsclient.Pool) (resolver.Builder, error))
	resolver, err := resolverBuilder(pool)
	if err != nil {
		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
	}

	// Start a gRPC client that uses the above xDS resolver.
	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
	if err != nil {
		t.Fatalf("Failed to create gRPC client: %v", err)
	}
	defer cc.Close()
	cc.Connect()

	// Ensure that RPCs reach the cluster specified by the primary server and
	// that no connection is attempted to the fallback server.
	client := testgrpc.NewTestServiceClient(cc)
	if err := waitForRPCsToReachBackend(ctx, client, backend1.Address); err != nil {
		t.Fatal(err)
	}
	sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
	defer sCancel()
	if _, err := fallbackWrappedLis.NewConnCh.Receive(sCtx); err != context.DeadlineExceeded {
		t.Fatalf("Connection attempt made to fallback server when none expected: %v", err)
	}

	// Instruct the primary server to close the connection if below defined
	// cluster resource is requested.
	closeConnOnMidUpdateClusterResource.Store(true)

	// Update the listener resource on the primary server to point to a new
	// route configuration that points to a new cluster that points to a new
	// endpoints resource that contains backend3.
	primaryResources = e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, midUpdateRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(midUpdateRouteConfigName, serviceName, midUpdateClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(midUpdateClusterName, midUpdateEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(midUpdateEndpointsName, "localhost", []uint32{testutils.ParsePort(t, backend3.Address)})},
	}
	if err := primaryManagementServer.Update(ctx, primaryResources); err != nil {
		t.Fatal(err)
	}

	// Ensure that a connection is attempted to the fallback (because both
	// conditions mentioned for fallback in A71 are satisfied: connectivity
	// failure and a watcher for an uncached resource), and that RPCs are
	// routed to the cluster returned by the fallback server.
	c, err := fallbackWrappedLis.NewConnCh.Receive(ctx)
	if err != nil {
		t.Fatalf("Failure when waiting for a connection to be opened to the fallback management server: %v", err)
	}
	fallbackConn := c.(*testutils.ConnWrapper)
	if err := waitForRPCsToReachBackend(ctx, client, backend2.Address); err != nil {
		t.Fatal(err)
	}

	// Set the primary management server to not close the connection anymore if
	// the mid-update cluster resource is requested, and get it to start serving
	// again.
	closeConnOnMidUpdateClusterResource.Store(false)
	primaryLis.Restart()

	// A new snapshot, with the same resources, is pushed to the management
	// server to get it to respond for already requested resource names.
	if err := primaryManagementServer.Update(ctx, primaryResources); err != nil {
		t.Fatal(err)
	}

	// Ensure that RPCs reach the backend pointed to by the new cluster.
	if err := waitForRPCsToReachBackend(ctx, client, backend3.Address); err != nil {
		t.Fatal(err)
	}

	// Wait for the connection to the secondary to be closed since we have
	// reverted back to the primary.
	if _, err := fallbackConn.CloseCh.Receive(ctx); err != nil {
		t.Fatalf("Connection to fallback server not closed once primary becomes ready: %v", err)
	}
}

// Tests fallback when the primary management server fails during startup.
func (s) TestFallback_MidStartup(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultFallbackTestTimeout)
	defer cancel()

	// Create two listeners for the two management servers. The test can
	// start/stop these listeners and can also get notified when the listener
	// receives a connection request.
	primaryWrappedLis := testutils.NewListenerWrapper(t, nil)
	primaryLis := testutils.NewRestartableListener(primaryWrappedLis)
	fallbackWrappedLis := testutils.NewListenerWrapper(t, nil)
	fallbackLis := testutils.NewRestartableListener(fallbackWrappedLis)

	// This boolean helps with triggering fallback during startup. When this
	// boolean is set and a cluster resource is requested, the primary
	// management server shuts down the connection, forcing the client to
	// fallback to the secondary server.
	var closeConnOnClusterResource atomic.Bool
	closeConnOnClusterResource.Store(true)

	// Start two management servers, primary and fallback, with the above
	// listeners.
	primaryManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
		Listener: primaryLis,
		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
			if closeConnOnClusterResource.Load() == false {
				return nil
			}
			if req.GetTypeUrl() != version.V3ClusterURL {
				return nil
			}
			primaryLis.Stop()
			return fmt.Errorf("closing ADS stream because cluster resource was requested")
		},
		AllowResourceSubset: true,
	})
	fallbackManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: fallbackLis})

	// Start two test service backends.
	backend1 := stubserver.StartTestService(t, nil)
	defer backend1.Stop()
	backend2 := stubserver.StartTestService(t, nil)
	defer backend2.Stop()

	// Configure xDS resource on the primary management server, with a cluster
	// resource that contains an endpoint for backend1.
	nodeID := uuid.New().String()
	const serviceName = "my-service-fallback-xds"
	primaryResources := e2e.DefaultClientResources(e2e.ResourceParams{
		DialTarget: serviceName,
		NodeID:     nodeID,
		Host:       "localhost",
		Port:       testutils.ParsePort(t, backend1.Address),
		SecLevel:   e2e.SecurityLevelNone,
	})
	if err := primaryManagementServer.Update(ctx, primaryResources); err != nil {
		t.Fatal(err)
	}

	// Configure xDS resource on the secondary management server, with a cluster
	// resource that contains an endpoint for backend2. Only the listener
	// resource has the same name on both servers.
	fallbackRouteConfigName := "fallback-route-" + serviceName
	fallbackClusterName := "fallback-cluster-" + serviceName
	fallbackEndpointsName := "fallback-endpoints-" + serviceName
	fallbackResources := e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, fallbackRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(fallbackRouteConfigName, serviceName, fallbackClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(fallbackClusterName, fallbackEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(fallbackEndpointsName, "localhost", []uint32{testutils.ParsePort(t, backend2.Address)})},
	}
	if err := fallbackManagementServer.Update(ctx, fallbackResources); err != nil {
		t.Fatal(err)
	}

	// Generate bootstrap configuration with the above two servers.
	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
		Servers: []byte(fmt.Sprintf(`[
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		},
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		}]`, primaryManagementServer.Address, fallbackManagementServer.Address)),
		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
	})
	if err != nil {
		t.Fatalf("Failed to create bootstrap file: %v", err)
	}

	// Create an xDS client with the above bootstrap configuration.
	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
	if err != nil {
		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
	}
	pool := xdsclient.NewPool(config)
	if err != nil {
		t.Fatalf("Failed to create xDS client: %v", err)
	}

	// Get the xDS resolver to use the above xDS client.
	resolverBuilder := internal.NewXDSResolverWithPoolForTesting.(func(*xdsclient.Pool) (resolver.Builder, error))
	resolver, err := resolverBuilder(pool)
	if err != nil {
		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
	}

	// Start a gRPC client that uses the above xDS resolver.
	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
	if err != nil {
		t.Fatalf("Failed to create gRPC client: %v", err)
	}
	defer cc.Close()
	cc.Connect()

	// Ensure that a connection is attempted to the primary.
	if _, err := primaryWrappedLis.NewConnCh.Receive(ctx); err != nil {
		t.Fatalf("Failure when waiting for a connection to be opened to the primary management server: %v", err)
	}

	// Ensure that a connection is attempted to the fallback.
	c, err := fallbackWrappedLis.NewConnCh.Receive(ctx)
	if err != nil {
		t.Fatalf("Failure when waiting for a connection to be opened to the secondary management server: %v", err)
	}
	fallbackConn := c.(*testutils.ConnWrapper)

	// Ensure that RPCs are routed to the cluster returned by the fallback
	// management server.
	client := testgrpc.NewTestServiceClient(cc)
	if err := waitForRPCsToReachBackend(ctx, client, backend2.Address); err != nil {
		t.Fatal(err)
	}

	// Get the primary management server to no longer close the connection when
	// the cluster resource is requested.
	closeConnOnClusterResource.Store(false)
	primaryLis.Restart()

	// A new snapshot, with the same resources, is pushed to the management
	// server to get it to respond for already requested resource names.
	if err := primaryManagementServer.Update(ctx, primaryResources); err != nil {
		t.Fatal(err)
	}

	// Ensure that RPCs are routed to the cluster returned by the primary
	// management server.
	if err := waitForRPCsToReachBackend(ctx, client, backend1.Address); err != nil {
		t.Fatal(err)
	}

	// Wait for the connection to the secondary to be closed since we have
	// reverted back to the primary.
	if _, err := fallbackConn.CloseCh.Receive(ctx); err != nil {
		t.Fatalf("Connection to fallback server not closed once primary becomes ready: %v", err)
	}
}

// Tests that RPCs succeed at startup when the primary management server is
// down, but the secondary is available.
func (s) TestFallback_OnStartup_RPCSuccess(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultFallbackTestTimeout)
	defer cancel()

	// Create two listeners for the two management servers. The test can
	// start/stop these listeners.
	l, err := testutils.LocalTCPListener()
	if err != nil {
		t.Fatalf("Failed to create listener: %v", err)
	}
	primaryLis := testutils.NewRestartableListener(l)
	l, err = testutils.LocalTCPListener()
	if err != nil {
		t.Fatalf("Failed to create listener: %v", err)
	}
	fallbackLis := testutils.NewRestartableListener(l)

	// Start two management servers, primary and fallback, with the above
	// listeners.
	primaryManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: primaryLis})
	fallbackManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: fallbackLis})

	// Start two test service backends.
	backend1 := stubserver.StartTestService(t, nil)
	defer backend1.Stop()
	backend2 := stubserver.StartTestService(t, nil)
	defer backend2.Stop()

	// Configure xDS resource on the primary management server, with a cluster
	// resource that contains an endpoint for backend1.
	nodeID := uuid.New().String()
	const serviceName = "my-service-fallback-xds"
	resources := e2e.DefaultClientResources(e2e.ResourceParams{
		DialTarget: serviceName,
		NodeID:     nodeID,
		Host:       "localhost",
		Port:       testutils.ParsePort(t, backend1.Address),
		SecLevel:   e2e.SecurityLevelNone,
	})
	if err := primaryManagementServer.Update(ctx, resources); err != nil {
		t.Fatal(err)
	}

	// Configure xDS resource on the secondary management server, with a cluster
	// resource that contains an endpoint for backend2. Only the listener
	// resource has the same name on both servers.
	fallbackRouteConfigName := "fallback-route-" + serviceName
	fallbackClusterName := "fallback-cluster-" + serviceName
	fallbackEndpointsName := "fallback-endpoints-" + serviceName
	resources = e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, fallbackRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(fallbackRouteConfigName, serviceName, fallbackClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(fallbackClusterName, fallbackEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(fallbackEndpointsName, "localhost", []uint32{testutils.ParsePort(t, backend2.Address)})},
	}
	if err := fallbackManagementServer.Update(ctx, resources); err != nil {
		t.Fatal(err)
	}

	// Shutdown the primary management server before starting the gRPC client to
	// trigger fallback on startup.
	primaryLis.Stop()

	// Generate bootstrap configuration with the above two servers.
	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
		Servers: []byte(fmt.Sprintf(`[
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		},
		{
			"server_uri": %q,
			"channel_creds": [{"type": "insecure"}]
		}]`, primaryManagementServer.Address, fallbackManagementServer.Address)),
		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
	})
	if err != nil {
		t.Fatalf("Failed to create bootstrap file: %v", err)
	}

	// Create an xDS client with the above bootstrap configuration.
	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
	if err != nil {
		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
	}
	pool := xdsclient.NewPool(config)
	if err != nil {
		t.Fatalf("Failed to create xDS client: %v", err)
	}

	// Get the xDS resolver to use the above xDS client.
	resolverBuilder := internal.NewXDSResolverWithPoolForTesting.(func(*xdsclient.Pool) (resolver.Builder, error))
	resolver, err := resolverBuilder(pool)
	if err != nil {
		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
	}

	// Start a gRPC client that uses the above xDS resolver.
	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
	if err != nil {
		t.Fatalf("Failed to create gRPC client: %v", err)
	}
	defer cc.Close()

	// Make an RPC (without the `wait_for_ready` call option) and expect it to
	// succeed since the fallback management server is up and running.
	client := testgrpc.NewTestServiceClient(cc)
	var peer peer.Peer
	if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&peer)); err != nil {
		t.Fatalf("EmptyCall() failed: %v", err)
	}
	if got, want := peer.Addr.String(), backend2.Address; got != want {
		t.Fatalf("Unexpected peer address: got %q, want %q", got, want)
	}

	// Start the primary server. It can take a while before the xDS client
	// notices this, since the ADS stream implementation uses a backoff before
	// retrying the stream.
	primaryLis.Restart()
	if err := waitForRPCsToReachBackend(ctx, client, backend1.Address); err != nil {
		t.Fatal(err)
	}
}

// Test verifies that when the primary management server is unavailable, the
// system attempts to connect to the first fallback server, and if that is also
// down, to the second fallback server. It also ensures that the system switches
// back to the first fallback server once it becomes available again, and
// eventually returns to the primary server when it comes back online, closing
// connections to the fallback servers accordingly.
func (s) TestFallback_ThreeServerPromotion(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultFallbackTestTimeout)
	defer cancel()

	// Create three listener wrappers for three management servers.
	primaryWrappedLis := testutils.NewListenerWrapper(t, nil)
	primaryLis := testutils.NewRestartableListener(primaryWrappedLis)

	secondaryWrappedLis := testutils.NewListenerWrapper(t, nil)
	secondaryLis := testutils.NewRestartableListener(secondaryWrappedLis)

	tertiaryWrappedLis := testutils.NewListenerWrapper(t, nil)
	tertiaryLis := testutils.NewRestartableListener(tertiaryWrappedLis)

	// Start the three management servers.
	primaryManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: primaryLis})
	secondaryManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: secondaryLis})
	tertiaryManagementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: tertiaryLis})

	// Start three test service backends.
	backend1 := stubserver.StartTestService(t, nil)
	backend1Port := testutils.ParsePort(t, backend1.Address)
	defer backend1.Stop()
	backend2 := stubserver.StartTestService(t, nil)
	backend2Port := testutils.ParsePort(t, backend2.Address)
	defer backend2.Stop()
	backend3 := stubserver.StartTestService(t, nil)
	backend3Port := testutils.ParsePort(t, backend3.Address)
	defer backend3.Stop()

	nodeID := uuid.New().String()
	const (
		serviceName              = "my-service-fallback-xds"
		primaryRouteConfigName   = "primary-route-" + serviceName
		secondaryRouteConfigName = "secondary-route-" + serviceName
		tertiaryRouteConfigName  = "tertiary-route-" + serviceName
		primaryClusterName       = "primary-cluster-" + serviceName
		secondaryClusterName     = "secondary-cluster-" + serviceName
		tertiaryClusterName      = "tertiary-cluster-" + serviceName
		primaryEndpointsName     = "primary-endpoints-" + serviceName
		secondaryEndpointsName   = "secondary-endpoints-" + serviceName
		tertiaryEndpointsName    = "tertiary-endpoints-" + serviceName
	)

	// Configure partial resources on the primary and secondary
	// management servers.
	primaryManagementServer.Update(ctx, e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, primaryRouteConfigName)},
	})
	secondaryManagementServer.Update(ctx, e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, secondaryRouteConfigName)},
	})

	// Configure full resources on tertiary management server.
	updateOpts := e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, tertiaryRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(tertiaryRouteConfigName, serviceName, tertiaryClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(tertiaryClusterName, tertiaryEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(tertiaryEndpointsName, "localhost", []uint32{backend3Port})},
	}
	tertiaryManagementServer.Update(ctx, updateOpts)

	// Create bootstrap configuration for all three management servers.
	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
		Servers: []byte(fmt.Sprintf(`[
            {
                "server_uri": %q,
                "channel_creds": [{"type": "insecure"}]
            },
            {
                "server_uri": %q,
                "channel_creds": [{"type": "insecure"}]
            },
             {
                "server_uri": %q,
                "channel_creds": [{"type": "insecure"}]
            }
        ]`, primaryManagementServer.Address, secondaryManagementServer.Address, tertiaryManagementServer.Address)),
		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
	})
	if err != nil {
		t.Fatalf("Failed to create bootstrap file: %v", err)
	}

	// Create an xDS client with the above bootstrap configuration.
	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
	if err != nil {
		t.Fatalf("Failed to parse bootstrap contents: %v", err)
	}
	pool := xdsclient.NewPool(config)
	if err != nil {
		t.Fatalf("Failed to create xDS client: %v", err)
	}

	// Get the xDS resolver to use the above xDS client.
	resolverBuilder := internal.NewXDSResolverWithPoolForTesting.(func(*xdsclient.Pool) (resolver.Builder, error))
	resolver, err := resolverBuilder(pool)
	if err != nil {
		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
	}

	// Start a gRPC client that uses the above xDS resolver.
	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
	if err != nil {
		t.Fatalf("Failed to create gRPC client: %v", err)
	}
	defer cc.Close()
	cc.Connect()
	client := testgrpc.NewTestServiceClient(cc)

	// Verify that connection attempts were made to primaryWrappedLis and
	// secondaryWrappedLis, before using tertiaryWrappedLis to make
	// successful RPCs to backend3.
	if _, err := primaryWrappedLis.NewConnCh.Receive(ctx); err != nil {
		t.Fatalf("Timeout when waiting for connection to primary: %v", err)
	}

	// Stop primary, client should connect to secondary.
	primaryLis.Stop()
	if _, err := secondaryWrappedLis.NewConnCh.Receive(ctx); err != nil {
		t.Fatalf("Timeout when waiting for connection to secondary after primary stopped: %v", err)
	}

	// Stop secondary, client should connect to tertiary.
	secondaryLis.Stop()
	tertiaryConn, err := tertiaryWrappedLis.NewConnCh.Receive(ctx)
	if err != nil {
		t.Fatalf("Timeout when waiting for connection to tertiary after secondary stopped: %v", err)
	}

	// Tertiary has all resources, RPCs should succeed to backend3.
	if err := waitForRPCsToReachBackend(ctx, client, backend3.Address); err != nil {
		t.Fatal(err)
	}

	// Secondary1 becomes available, RPCs go to backend2.
	secondaryLis.Restart()
	updateOpts = e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, secondaryRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(secondaryRouteConfigName, serviceName, secondaryClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(secondaryClusterName, secondaryEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(secondaryEndpointsName, "localhost", []uint32{backend2Port})},
	}
	secondaryManagementServer.Update(ctx, updateOpts)

	secondaryConn, err := secondaryWrappedLis.NewConnCh.Receive(ctx)
	if err != nil {
		t.Fatalf("Timeout when waiting for new connection to secondary: %v", err)
	}
	if _, err := tertiaryConn.(*testutils.ConnWrapper).CloseCh.Receive(ctx); err != nil {
		t.Fatalf("Timeout when waiting for connection to the tertiary to be closed after promotion to secondary: %v", err)
	}
	if err := waitForRPCsToReachBackend(ctx, client, backend2.Address); err != nil {
		t.Fatal(err)
	}

	// Primary becomes available, RPCs go to backend1.
	primaryLis.Restart()
	updateOpts = e2e.UpdateOptions{
		NodeID:    nodeID,
		Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, primaryRouteConfigName)},
		Routes:    []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(primaryRouteConfigName, serviceName, primaryClusterName)},
		Clusters:  []*v3clusterpb.Cluster{e2e.DefaultCluster(primaryClusterName, primaryEndpointsName, e2e.SecurityLevelNone)},
		Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(primaryEndpointsName, "localhost", []uint32{backend1Port})},
	}
	primaryManagementServer.Update(ctx, updateOpts)

	if _, err := primaryWrappedLis.NewConnCh.Receive(ctx); err != nil {
		t.Fatalf("Timeout when waiting for new connection to primary: %v", err)
	}
	if _, err := secondaryConn.(*testutils.ConnWrapper).CloseCh.Receive(ctx); err != nil {
		t.Fatalf("Timeout when waiting for connection to the secondary to be closed after promotion to primary: %v", err)
	}
	if err := waitForRPCsToReachBackend(ctx, client, backend1.Address); err != nil {
		t.Fatal(err)
	}
}
