// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package retry_test

import (
	"reflect"
	"testing"
	"time"

	envoyroute "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	previouspriorities "github.com/envoyproxy/go-control-plane/envoy/extensions/retry/priority/previous_priorities/v3"
	. "github.com/onsi/gomega"
	"google.golang.org/protobuf/types/known/durationpb"
	wrappers "google.golang.org/protobuf/types/known/wrapperspb"

	networking "istio.io/api/networking/v1alpha3"
	"istio.io/istio/pilot/pkg/networking/core/route/retry"
	"istio.io/istio/pilot/pkg/util/protoconv"
	xdsfilters "istio.io/istio/pilot/pkg/xds/filters"
)

func TestRetry(t *testing.T) {
	testCases := []struct {
		name       string
		route      *networking.HTTPRoute
		hashPolicy bool
		assertFunc func(g *WithT, policy *envoyroute.RetryPolicy)
	}{
		{
			name: "TestNilRetryShouldReturnDefault",
			// Create a route where no retry policy has been explicitly set.
			route: &networking.HTTPRoute{},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy).To(Equal(retry.DefaultPolicy()))
			},
		},
		{
			name: "TestZeroAttemptsShouldReturnNilPolicy",
			// Create a route with a retry policy with zero attempts configured.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					// Explicitly not retrying.
					Attempts: 0,
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(BeNil())
			},
		},
		{
			name: "TestRetryWithAllFieldsSet",
			// Create a route with a retry policy with all fields configured.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts:      2,
					RetryOn:       "some,fake,conditions",
					PerTryTimeout: durationpb.New(time.Second * 3),
					Backoff: &durationpb.Duration{
						Seconds: 2,
					},
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal("some,fake,conditions"))
				g.Expect(policy.PerTryTimeout).To(Equal(durationpb.New(time.Second * 3)))
				g.Expect(policy.NumRetries.Value).To(Equal(uint32(2)))
				g.Expect(policy.RetriableStatusCodes).To(Equal(make([]uint32, 0)))
				g.Expect(policy.RetryPriority).To(BeNil())
				g.Expect(policy.HostSelectionRetryMaxAttempts).To(Equal(retry.DefaultPolicy().HostSelectionRetryMaxAttempts))
				retryHostPredicate := []*envoyroute.RetryPolicy_RetryHostPredicate{
					// to configure retries to prefer hosts that haven’t been attempted already,
					// the builtin `envoy.retry_host_predicates.previous_hosts` predicate can be used.
					xdsfilters.RetryPreviousHosts,
				}
				g.Expect(policy.RetryHostPredicate).To(Equal(retryHostPredicate))
				g.Expect(policy.RetryBackOff.BaseInterval).To(Equal(durationpb.New(time.Second * 2)))
			},
		},
		{
			name: "TestRetryOnWithEmptyParts",
			// Create a route with a retry policy with empty retry conditions configured.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					// Explicitly not retrying.
					Attempts: 2,
					RetryOn:  "some,fake,conditions,,,",
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal("some,fake,conditions"))
				g.Expect(policy.RetriableStatusCodes).To(Equal([]uint32{}))
			},
		},
		{
			name: "TestRetryOnWithRetriableStatusCodes",
			// Create a route with a retry policy with retriable status code.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					// Explicitly not retrying.
					Attempts: 2,
					RetryOn:  "gateway-error,retriable-status-codes,503",
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal("gateway-error,retriable-status-codes"))
				g.Expect(policy.RetriableStatusCodes).To(Equal([]uint32{503}))
			},
		},
		{
			name: "TestRetryOnWithResetBeforeRequest",
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
					RetryOn:  "reset-before-request",
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal("reset-before-request"))
			},
		},
		{
			name: "TestRetryOnWithWhitespace",
			// Create a route with a retry policy with retryOn having white spaces.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					// Explicitly not retrying.
					Attempts: 2,
					RetryOn:  " some,	,fake ,	conditions, ,",
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal("some,fake,conditions"))
				g.Expect(policy.RetriableStatusCodes).To(Equal([]uint32{}))
			},
		},
		{
			name: "TestRetryOnContainingStatusCodes",
			// Create a route with a retry policy with status codes.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
					RetryOn:  "some,fake,5xx,404,conditions,503",
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal("some,fake,5xx,conditions,retriable-status-codes"))
				g.Expect(policy.RetriableStatusCodes).To(Equal([]uint32{404, 503}))
			},
		},
		{
			name: "TestRetryOnWithInvalidStatusCodesShouldAddToRetryOn",
			// Create a route with a retry policy with invalid status codes.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
					RetryOn:  "some,fake,conditions,1000",
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal("some,fake,conditions,1000"))
				g.Expect(policy.RetriableStatusCodes).To(Equal([]uint32{}))
			},
		},
		{
			name: "TestMissingRetryOnShouldReturnDefaults",
			// Create a route with a retry policy with two attempts configured.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal(retry.DefaultPolicy().RetryOn))
				g.Expect(policy.RetriableStatusCodes).To(Equal(retry.DefaultPolicy().RetriableStatusCodes))
			},
		},
		{
			name: "TestMissingPerTryTimeoutShouldReturnNil",
			// Create a route with a retry policy without per try timeout.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.PerTryTimeout).To(BeNil())
			},
		},
		{
			name: "TestRetryRemoteLocalities",
			// Create a route with a retry policy with RetryRemoteLocalities enabled.
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
					RetryRemoteLocalities: &wrappers.BoolValue{
						Value: true,
					},
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.RetryOn).To(Equal(retry.DefaultPolicy().RetryOn))
				g.Expect(policy.RetriableStatusCodes).To(Equal(retry.DefaultPolicy().RetriableStatusCodes))

				previousPrioritiesConfig := &previouspriorities.PreviousPrioritiesConfig{
					UpdateFrequency: int32(2),
				}
				expected := &envoyroute.RetryPolicy_RetryPriority{
					Name: "envoy.retry_priorities.previous_priorities",
					ConfigType: &envoyroute.RetryPolicy_RetryPriority_TypedConfig{
						TypedConfig: protoconv.MessageToAny(previousPrioritiesConfig),
					},
				}
				if !reflect.DeepEqual(policy.RetryPriority, expected) {
					t.Fatalf("Expected %v, actual %v", expected, policy.RetryPriority)
				}
			},
		},
		{
			name: "TestRetryPreviousHostsDefault",
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.NumRetries.Value).To(Equal(uint32(2)))
				g.Expect(policy.RetryPriority).To(BeNil())
				g.Expect(policy.HostSelectionRetryMaxAttempts).To(Equal(retry.DefaultPolicy().HostSelectionRetryMaxAttempts))
				retryHostPredicate := []*envoyroute.RetryPolicy_RetryHostPredicate{
					xdsfilters.RetryPreviousHosts,
				}
				g.Expect(policy.RetryHostPredicate).To(Equal(retryHostPredicate))
			},
		},
		{
			name: "TestRetryPreviousHostsDefaultWithHashPolicy",
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
				},
			},
			hashPolicy: true,
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.NumRetries.Value).To(Equal(uint32(2)))
				g.Expect(policy.RetryPriority).To(BeNil())
				g.Expect(policy.HostSelectionRetryMaxAttempts).To(Equal(retry.DefaultPolicy().HostSelectionRetryMaxAttempts))
			},
		},
		{
			name: "TestRetryPreviousHostsForHashPolicyWithRetryIgnorePreviousHosts",
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
					RetryIgnorePreviousHosts: &wrappers.BoolValue{
						Value: true,
					},
				},
			},
			hashPolicy: true,
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.NumRetries.Value).To(Equal(uint32(2)))
				g.Expect(policy.RetryPriority).To(BeNil())
				g.Expect(policy.HostSelectionRetryMaxAttempts).To(Equal(retry.DefaultPolicy().HostSelectionRetryMaxAttempts))
				retryHostPredicate := []*envoyroute.RetryPolicy_RetryHostPredicate{
					xdsfilters.RetryPreviousHosts,
				}
				g.Expect(policy.RetryHostPredicate).To(Equal(retryHostPredicate))
			},
		},
		{
			name: "TestRetryPreviousHostsDisabled",
			route: &networking.HTTPRoute{
				Retries: &networking.HTTPRetry{
					Attempts: 2,
					RetryIgnorePreviousHosts: &wrappers.BoolValue{
						Value: false,
					},
				},
			},
			assertFunc: func(g *WithT, policy *envoyroute.RetryPolicy) {
				g.Expect(policy).To(Not(BeNil()))
				g.Expect(policy.NumRetries.Value).To(Equal(uint32(2)))
				g.Expect(policy.RetryPriority).To(BeNil())
				g.Expect(policy.HostSelectionRetryMaxAttempts).To(Equal(retry.DefaultPolicy().HostSelectionRetryMaxAttempts))
			},
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			g := NewWithT(t)
			policy := retry.ConvertPolicy(tc.route.Retries, tc.hashPolicy)
			if tc.assertFunc != nil {
				tc.assertFunc(g, policy)
			}
		})
	}
}
