// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package api

import (
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) {
	t.Parallel()
	c, s := makeClient(t)
	defer s.Stop()

	config_entries := c.ConfigEntries()

	verifyResolver := func(t *testing.T, initial ConfigEntry) {
		t.Helper()
		require.IsType(t, &ServiceResolverConfigEntry{}, initial)
		testEntry := initial.(*ServiceResolverConfigEntry)

		// set it
		_, wm, err := config_entries.Set(testEntry, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// get it
		entry, qm, err := config_entries.Get(ServiceResolver, testEntry.Name, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)

		// generic verification
		require.Equal(t, testEntry.Meta, entry.GetMeta())

		// verify it
		readResolver, ok := entry.(*ServiceResolverConfigEntry)
		require.True(t, ok)
		readResolver.ModifyIndex = 0 // reset for Equals()
		readResolver.CreateIndex = 0 // reset for Equals()

		require.Equal(t, testEntry, readResolver)

		// TODO(rb): cas?
		// TODO(rb): list?
	}

	verifySplitter := func(t *testing.T, initial ConfigEntry) {
		t.Helper()
		require.IsType(t, &ServiceSplitterConfigEntry{}, initial)
		testEntry := initial.(*ServiceSplitterConfigEntry)

		// set it
		_, wm, err := config_entries.Set(testEntry, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// get it
		entry, qm, err := config_entries.Get(ServiceSplitter, testEntry.Name, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)

		// generic verification
		require.Equal(t, testEntry.Meta, entry.GetMeta())

		// verify it
		readSplitter, ok := entry.(*ServiceSplitterConfigEntry)
		require.True(t, ok)
		readSplitter.ModifyIndex = 0 // reset for Equals()
		readSplitter.CreateIndex = 0 // reset for Equals()

		require.Equal(t, testEntry, readSplitter)

		// TODO(rb): cas?
		// TODO(rb): list?
	}

	verifyRouter := func(t *testing.T, initial ConfigEntry) {
		t.Helper()
		require.IsType(t, &ServiceRouterConfigEntry{}, initial)
		testEntry := initial.(*ServiceRouterConfigEntry)

		// set it
		_, wm, err := config_entries.Set(testEntry, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// get it
		entry, qm, err := config_entries.Get(ServiceRouter, testEntry.Name, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)

		// generic verification
		require.Equal(t, testEntry.Meta, entry.GetMeta())

		// verify it
		readRouter, ok := entry.(*ServiceRouterConfigEntry)
		require.True(t, ok)
		readRouter.ModifyIndex = 0 // reset for Equals()
		readRouter.CreateIndex = 0 // reset for Equals()

		require.Equal(t, testEntry, readRouter)

		// TODO(rb): cas?
		// TODO(rb): list?
	}

	// First set the necessary protocols to allow advanced routing features.
	for _, service := range []string{
		"test-failover",
		"test-redirect",
		"alternate",
		"test-split",
		"test-route",
		"test-route-case-insensitive",
	} {
		serviceDefaults := &ServiceConfigEntry{
			Kind:     ServiceDefaults,
			Name:     service,
			Protocol: "http",
		}
		_, _, err := config_entries.Set(serviceDefaults, nil)
		require.NoError(t, err)
	}

	// NOTE: Due to service graph validation, these have to happen in a specific order.
	for _, tc := range []struct {
		name   string
		entry  ConfigEntry
		verify func(t *testing.T, initial ConfigEntry)
	}{
		{
			name: "failover",
			entry: &ServiceResolverConfigEntry{
				Kind:          ServiceResolver,
				Name:          "test-failover",
				Partition:     defaultPartition,
				Namespace:     defaultNamespace,
				DefaultSubset: "v1",
				Subsets: map[string]ServiceResolverSubset{
					"v1": {
						Filter: "Service.Meta.version == v1",
					},
					"v2": {
						Filter: "Service.Meta.version == v2",
					},
					"v3": {
						Filter: "Service.Meta.version == v3",
					},
				},
				Failover: map[string]ServiceResolverFailover{
					"*": {
						Datacenters: []string{"dc2"},
					},
					"v1": {
						Service:   "alternate",
						Namespace: defaultNamespace,
					},
					"v3": {
						Targets: []ServiceResolverFailoverTarget{
							{Peer: "cluster-01"},
							{Datacenter: "dc1"},
							{Service: "another-service", ServiceSubset: "v1"},
						},
					},
				},
				ConnectTimeout: 5 * time.Second,
				RequestTimeout: 10 * time.Second,
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
			},
			verify: verifyResolver,
		},
		{
			name: "redirect",
			entry: &ServiceResolverConfigEntry{
				Kind:      ServiceResolver,
				Name:      "test-redirect",
				Partition: defaultPartition,
				Namespace: defaultNamespace,
				Redirect: &ServiceResolverRedirect{
					Service:       "test-failover",
					ServiceSubset: "v2",
					Namespace:     defaultNamespace,
					Datacenter:    "d",
				},
			},
			verify: verifyResolver,
		},
		{
			name: "redirect to peer",
			entry: &ServiceResolverConfigEntry{
				Kind:      ServiceResolver,
				Name:      "test-redirect",
				Partition: defaultPartition,
				Namespace: defaultNamespace,
				Redirect: &ServiceResolverRedirect{
					Service: "test-failover",
					Peer:    "cluster-01",
				},
			},
			verify: verifyResolver,
		},
		{
			name: "mega splitter", // use one mega object to avoid multiple trips
			entry: &ServiceSplitterConfigEntry{
				Kind:      ServiceSplitter,
				Name:      "test-split",
				Partition: defaultPartition,
				Namespace: defaultNamespace,
				Splits: []ServiceSplit{
					{
						Weight:        90,
						Service:       "test-failover",
						ServiceSubset: "v1",
						Namespace:     defaultNamespace,
						RequestHeaders: &HTTPHeaderModifiers{
							Set: map[string]string{
								"x-foo": "bar",
							},
						},
						ResponseHeaders: &HTTPHeaderModifiers{
							Remove: []string{"x-foo"},
						},
					},
					{
						Weight:    10,
						Service:   "test-redirect",
						Namespace: defaultNamespace,
					},
				},
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
			},
			verify: verifySplitter,
		},
		{
			name: "mega router", // use one mega object to avoid multiple trips
			entry: &ServiceRouterConfigEntry{
				Kind:      ServiceRouter,
				Name:      "test-route",
				Partition: defaultPartition,
				Namespace: defaultNamespace,
				Routes: []ServiceRoute{
					{
						Match: &ServiceRouteMatch{
							HTTP: &ServiceRouteHTTPMatch{
								PathPrefix: "/prefix",
								Header: []ServiceRouteHTTPMatchHeader{
									{Name: "x-debug", Exact: "1"},
								},
								QueryParam: []ServiceRouteHTTPMatchQueryParam{
									{Name: "debug", Exact: "1"},
								},
							},
						},
						Destination: &ServiceRouteDestination{
							Service:               "test-failover",
							ServiceSubset:         "v2",
							Namespace:             defaultNamespace,
							Partition:             defaultPartition,
							PrefixRewrite:         "/",
							RequestTimeout:        5 * time.Second,
							NumRetries:            5,
							RetryOnConnectFailure: true,
							RetryOnStatusCodes:    []uint32{500, 503, 401},
							RetryOn: []string{
								"gateway-error",
								"reset",
								"envoy-ratelimited",
								"retriable-4xx",
								"refused-stream",
								"cancelled",
								"deadline-exceeded",
								"internal",
								"resource-exhausted",
								"unavailable",
							},
							RequestHeaders: &HTTPHeaderModifiers{
								Set: map[string]string{
									"x-foo": "bar",
								},
							},
							ResponseHeaders: &HTTPHeaderModifiers{
								Remove: []string{"x-foo"},
							},
						},
					},
				},
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
			},
			verify: verifyRouter,
		},
		{
			name: "mega router case insensitive", // use one mega object to avoid multiple trips
			entry: &ServiceRouterConfigEntry{
				Kind:      ServiceRouter,
				Name:      "test-route-case-insensitive",
				Partition: defaultPartition,
				Namespace: defaultNamespace,
				Routes: []ServiceRoute{
					{
						Match: &ServiceRouteMatch{
							HTTP: &ServiceRouteHTTPMatch{
								PathPrefix:      "/prEfix",
								CaseInsensitive: true,
								Header: []ServiceRouteHTTPMatchHeader{
									{Name: "x-debug", Exact: "1"},
								},
								QueryParam: []ServiceRouteHTTPMatchQueryParam{
									{Name: "debug", Exact: "1"},
								},
							},
						},
						Destination: &ServiceRouteDestination{
							Service:               "test-failover",
							ServiceSubset:         "v2",
							Namespace:             defaultNamespace,
							Partition:             defaultPartition,
							PrefixRewrite:         "/",
							RequestTimeout:        5 * time.Second,
							NumRetries:            5,
							RetryOnConnectFailure: true,
							RetryOnStatusCodes:    []uint32{500, 503, 401},
							RetryOn: []string{
								"gateway-error",
								"reset",
								"envoy-ratelimited",
								"retriable-4xx",
								"refused-stream",
								"cancelled",
								"deadline-exceeded",
								"internal",
								"resource-exhausted",
								"unavailable",
							},
							RequestHeaders: &HTTPHeaderModifiers{
								Set: map[string]string{
									"x-foo": "bar",
								},
							},
							ResponseHeaders: &HTTPHeaderModifiers{
								Remove: []string{"x-foo"},
							},
						},
					},
				},
				Meta: map[string]string{
					"foo": "bar",
					"gir": "zim",
				},
			},
			verify: verifyRouter,
		},
	} {
		tc := tc
		name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
		ok := t.Run(name, func(t *testing.T) {
			tc.verify(t, tc.entry)
		})
		require.True(t, ok, "subtest %q failed so aborting remainder", name)
	}
}

func TestAPI_ConfigEntry_ServiceResolver_LoadBalancer(t *testing.T) {
	t.Parallel()
	c, s := makeClient(t)
	defer s.Stop()

	config_entries := c.ConfigEntries()

	verifyResolver := func(t *testing.T, initial ConfigEntry) {
		t.Helper()
		require.IsType(t, &ServiceResolverConfigEntry{}, initial)
		testEntry := initial.(*ServiceResolverConfigEntry)

		// set it
		_, wm, err := config_entries.Set(testEntry, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotEqual(t, 0, wm.RequestTime)

		// get it
		entry, qm, err := config_entries.Get(ServiceResolver, testEntry.Name, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotEqual(t, 0, qm.RequestTime)

		// verify it
		readResolver, ok := entry.(*ServiceResolverConfigEntry)
		require.True(t, ok)
		readResolver.ModifyIndex = 0 // reset for Equals()
		readResolver.CreateIndex = 0 // reset for Equals()

		require.Equal(t, testEntry, readResolver)
	}

	// First set the necessary protocols to allow advanced routing features.
	for _, service := range []string{
		"test-least-req",
		"test-ring-hash",
	} {
		serviceDefaults := &ServiceConfigEntry{
			Kind:     ServiceDefaults,
			Name:     service,
			Protocol: "http",
		}
		_, _, err := config_entries.Set(serviceDefaults, nil)
		require.NoError(t, err)
	}

	// NOTE: Due to service graph validation, these have to happen in a specific order.
	for _, tc := range []struct {
		name   string
		entry  ConfigEntry
		verify func(t *testing.T, initial ConfigEntry)
	}{
		{
			name: "least-req",
			entry: &ServiceResolverConfigEntry{
				Kind:      ServiceResolver,
				Name:      "test-least-req",
				Partition: defaultPartition,
				Namespace: defaultNamespace,
				LoadBalancer: &LoadBalancer{
					Policy:             "least_request",
					LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 10},
				},
			},
			verify: verifyResolver,
		},
		{
			name: "ring-hash-with-policies",
			entry: &ServiceResolverConfigEntry{
				Kind:      ServiceResolver,
				Name:      "test-ring-hash",
				Namespace: defaultNamespace,
				Partition: defaultPartition,
				LoadBalancer: &LoadBalancer{
					Policy: "ring_hash",
					RingHashConfig: &RingHashConfig{
						MinimumRingSize: 1024 * 2,
						MaximumRingSize: 1024 * 4,
					},
					HashPolicies: []HashPolicy{
						{
							Field:      "header",
							FieldValue: "my-session-header",
							Terminal:   true,
						},
						{
							Field:      "cookie",
							FieldValue: "oreo",
							CookieConfig: &CookieConfig{
								Path: "/tray",
								TTL:  20 * time.Millisecond,
							},
						},
						{
							Field:      "cookie",
							FieldValue: "sugar",
							CookieConfig: &CookieConfig{
								Session: true,
								Path:    "/tin",
							},
						},
						{
							SourceIP: true,
						},
					},
				},
			},
			verify: verifyResolver,
		},
	} {
		tc := tc
		name := fmt.Sprintf("%s:%s: %s", tc.entry.GetKind(), tc.entry.GetName(), tc.name)
		ok := t.Run(name, func(t *testing.T) {
			tc.verify(t, tc.entry)
		})
		require.True(t, ok, "subtest %q failed so aborting remainder", name)
	}
}
