/*
 *
 * Copyright 2020 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 resolver_test

import (
	"context"
	"testing"

	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
	"github.com/google/uuid"
	"google.golang.org/grpc/internal/testutils"
	"google.golang.org/grpc/internal/testutils/xds/e2e"
	"google.golang.org/grpc/resolver"
	"google.golang.org/protobuf/types/known/wrapperspb"

	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
)

// Tests the case where the listener resource starts pointing to a new route
// configuration resource after the xDS resolver has successfully resolved the
// service name and pushed an update on the channel. The test verifies that the
// resolver stops requesting the old route configuration resource and requests
// the new resource, and once successfully resolved, verifies that the update
// from the resolver matches expected service config.
func (s) TestServiceWatch_ListenerPointsToNewRouteConfiguration(t *testing.T) {
	// Spin up an xDS management server for the test.
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()
	nodeID := uuid.New().String()
	mgmtServer, lisCh, routeCfgCh, bc := setupManagementServerForTest(t, nodeID)

	// Configure resources on the management server.
	resources := e2e.DefaultClientResources(e2e.ResourceParams{
		DialTarget: defaultTestServiceName,
		NodeID:     nodeID,
		Host:       defaultTestHostname,
		Port:       defaultTestPort[0],
		SecLevel:   e2e.SecurityLevelNone,
	})
	if err := mgmtServer.Update(ctx, resources); err != nil {
		t.Fatal(err)
	}

	stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)

	// Verify initial update from the resolver.
	waitForResourceNames(ctx, t, lisCh, []string{defaultTestServiceName})
	waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
	verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))

	// Update the listener resource to point to a new route configuration name.
	// The old route configuration resource is left unchanged to prevent a race:
	// if it were removed immediately, the resolver might encounter a
	// "resource-not-found" error for the old route before the listener update
	// successfully transitions the client to the new route.
	newTestRouteConfigName := defaultTestRouteConfigName + "-new"
	resources.Listeners = []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, newTestRouteConfigName)}
	resources.SkipValidation = true
	mgmtServer.Update(ctx, resources)

	// Verify that the new route configuration resource is requested.
	waitForResourceNames(ctx, t, routeCfgCh, []string{newTestRouteConfigName})

	// Update the old route configuration resource by adding a new route.
	resources.Routes[0].VirtualHosts[0].Routes = append(resources.Routes[0].VirtualHosts[0].Routes, &v3routepb.Route{
		Match: &v3routepb.RouteMatch{
			PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/foo/bar"},
			CaseSensitive: &wrapperspb.BoolValue{Value: false},
		},
		Action: &v3routepb.Route_Route{
			Route: &v3routepb.RouteAction{
				ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: "some-random-cluster"},
			},
		},
	})
	mgmtServer.Update(ctx, resources)

	// Wait for no update from the resolver since the listener resource no
	// longer points to the old route resource and new route resource hasn't
	// been sent yet.
	verifyNoUpdateFromResolver(ctx, t, stateCh)

	// Update the management server with the new route configuration resource.
	resources.Routes = append(resources.Routes, e2e.DefaultRouteConfig(newTestRouteConfigName, defaultTestServiceName, resources.Clusters[0].Name))
	configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, resources.Listeners, resources.Routes)

	// Ensure update from the resolver.
	verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
}

// Tests the case where the listener resource changes to contain an inline route
// configuration and changes back to having a route configuration resource name.
// Verifies that the expected xDS resource names are requested by the resolver
// and that the update from the resolver matches expected service config.
func (s) TestServiceWatch_ListenerPointsToInlineRouteConfiguration(t *testing.T) {
	// Spin up an xDS management server for the test.
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()
	nodeID := uuid.New().String()
	mgmtServer, lisCh, routeCfgCh, bc := setupManagementServerForTest(t, nodeID)

	// Configure resources on the management server.
	resources := e2e.DefaultClientResources(e2e.ResourceParams{
		DialTarget: defaultTestServiceName,
		NodeID:     nodeID,
		Host:       defaultTestHostname,
		Port:       defaultTestPort[0],
		SecLevel:   e2e.SecurityLevelNone,
	})
	if err := mgmtServer.Update(ctx, resources); err != nil {
		t.Fatal(err)
	}
	stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)

	// Verify initial update from the resolver.
	waitForResourceNames(ctx, t, lisCh, []string{defaultTestServiceName})
	waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
	verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))

	// Update listener to contain an inline route configuration.
	hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
		RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
			RouteConfig: &v3routepb.RouteConfiguration{
				Name: resources.Routes[0].Name,
				VirtualHosts: []*v3routepb.VirtualHost{{
					Domains: []string{defaultTestServiceName},
					Routes: []*v3routepb.Route{{
						Match: &v3routepb.RouteMatch{
							PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
						},
						Action: &v3routepb.Route_Route{
							Route: &v3routepb.RouteAction{
								ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: resources.Clusters[0].Name},
							},
						},
					}},
				}},
			},
		},
		HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
	})
	resources.Listeners = []*v3listenerpb.Listener{{
		Name:        defaultTestServiceName,
		ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm},
		FilterChains: []*v3listenerpb.FilterChain{{
			Name: "filter-chain-name",
			Filters: []*v3listenerpb.Filter{{
				Name:       wellknown.HTTPConnectionManager,
				ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm},
			}},
		}},
	}}
	configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, resources.Listeners, nil)

	// Verify that the old route configuration is not requested anymore.
	waitForResourceNames(ctx, t, routeCfgCh, []string{})
	verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))

	// Update listener back to contain a route configuration name.
	resources.Listeners = []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, resources.Routes[0].Name)}
	configureResourcesOnManagementServer(ctx, t, mgmtServer, nodeID, resources.Listeners, resources.Routes)

	// Verify that that route configuration resource is requested.
	waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})

	// Verify that appropriate SC is pushed on the channel.
	verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
}
