// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package config

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/durationpb"

	"github.com/hashicorp/consul/agent/structs"
)

func TestParseProxyConfig(t *testing.T) {
	tests := []struct {
		name  string
		input map[string]interface{}
		want  ProxyConfig
	}{
		{
			name:  "defaults - nil",
			input: nil,
			want: ProxyConfig{
				Protocol:              "tcp",
				LocalConnectTimeoutMs: 5000,
			},
		},
		{
			name:  "defaults - empty",
			input: map[string]interface{}{},
			want: ProxyConfig{
				Protocol:              "tcp",
				LocalConnectTimeoutMs: 5000,
			},
		},
		{
			name: "defaults - other stuff",
			input: map[string]interface{}{
				"foo":       "bar",
				"envoy_foo": "envoy_bar",
			},
			want: ProxyConfig{
				Protocol:              "tcp",
				LocalConnectTimeoutMs: 5000,
			},
		},
		{
			name: "protocol override",
			input: map[string]interface{}{
				"protocol": "http",
			},
			want: ProxyConfig{
				Protocol:              "http",
				LocalConnectTimeoutMs: 5000,
			},
		},
		{
			name: "protocol uppercase override",
			input: map[string]interface{}{
				"protocol": "HTTP",
			},
			want: ProxyConfig{
				Protocol:              "http",
				LocalConnectTimeoutMs: 5000,
			},
		},
		{
			name: "bind address override, string",
			input: map[string]interface{}{
				"bind_address": "127.0.0.2",
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				Protocol:              "tcp",
				BindAddress:           "127.0.0.2",
			},
		},
		{
			name: "bind port override, string",
			input: map[string]interface{}{
				"bind_port": "8888",
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				Protocol:              "tcp",
				BindPort:              8888,
			},
		},
		{
			name: "bind port override, int",
			input: map[string]interface{}{
				"bind_port": 8889,
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				Protocol:              "tcp",
				BindPort:              8889,
			},
		},
		{
			name: "local connect timeout override, string",
			input: map[string]interface{}{
				"local_connect_timeout_ms": "1000",
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 1000,
				Protocol:              "tcp",
			},
		},
		{
			name: "local connect timeout override, float ",
			input: map[string]interface{}{
				"local_connect_timeout_ms": float64(1000.0),
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 1000,
				Protocol:              "tcp",
			},
		},
		{
			name: "local connect timeout override, int ",
			input: map[string]interface{}{
				"local_connect_timeout_ms": 1000,
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 1000,
				Protocol:              "tcp",
			},
		},
		{
			name: "local request timeout override, string",
			input: map[string]interface{}{
				"local_request_timeout_ms": "1000",
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				LocalRequestTimeoutMs: intPointer(1000),
				Protocol:              "tcp",
			},
		},
		{
			name: "local request timeout override, float ",
			input: map[string]interface{}{
				"local_request_timeout_ms": float64(1000.0),
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				LocalRequestTimeoutMs: intPointer(1000),
				Protocol:              "tcp",
			},
		},
		{
			name: "local request timeout override, int ",
			input: map[string]interface{}{
				"local_request_timeout_ms": 1000,
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				LocalRequestTimeoutMs: intPointer(1000),
				Protocol:              "tcp",
			},
		},
		{
			name: "local idle timeout override, float ",
			input: map[string]interface{}{
				"local_idle_timeout_ms": float64(1000.0),
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				LocalIdleTimeoutMs:    intPointer(1000),
				Protocol:              "tcp",
			},
		},
		{
			name: "local idle timeout override, int ",
			input: map[string]interface{}{
				"local_idle_timeout_ms": 1000,
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				LocalIdleTimeoutMs:    intPointer(1000),
				Protocol:              "tcp",
			},
		},
		{
			name: "local idle timeout override, string",
			input: map[string]interface{}{
				"local_idle_timeout_ms": "1000",
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				LocalIdleTimeoutMs:    intPointer(1000),
				Protocol:              "tcp",
			},
		},
		{
			name: "balance inbound connections override, string",
			input: map[string]interface{}{
				"balance_inbound_connections": "exact_balance",
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs:     5000,
				Protocol:                  "tcp",
				BalanceInboundConnections: "exact_balance",
			},
		},
		{
			name: "max request headers KB override, int",
			input: map[string]interface{}{
				"max_request_headers_kb": 96,
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				Protocol:              "tcp",
				MaxRequestHeadersKB:   uintPointer(96),
			},
		},
		{
			name: "max request headers KB override, float",
			input: map[string]interface{}{
				"max_request_headers_kb": float64(128.0),
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				Protocol:              "tcp",
				MaxRequestHeadersKB:   uintPointer(128),
			},
		},
		{
			name: "max request headers KB override, string",
			input: map[string]interface{}{
				"max_request_headers_kb": "256",
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				Protocol:              "tcp",
				MaxRequestHeadersKB:   uintPointer(256),
			},
		},
		{
			name: "max request headers KB with zero value",
			input: map[string]interface{}{
				"max_request_headers_kb": 0,
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs: 5000,
				Protocol:              "tcp",
				MaxRequestHeadersKB:   uintPointer(0),
			},
		},
		{
			name: "kitchen sink with max request headers",
			input: map[string]interface{}{
				"protocol":                    "http",
				"bind_address":                "127.0.0.2",
				"bind_port":                   8888,
				"local_connect_timeout_ms":    1000,
				"local_request_timeout_ms":    2000,
				"local_idle_timeout_ms":       3000,
				"balance_inbound_connections": "exact_balance",
				"max_request_headers_kb":      96,
			},
			want: ProxyConfig{
				LocalConnectTimeoutMs:     1000,
				LocalRequestTimeoutMs:     intPointer(2000),
				LocalIdleTimeoutMs:        intPointer(3000),
				Protocol:                  "http",
				BindAddress:               "127.0.0.2",
				BindPort:                  8888,
				BalanceInboundConnections: "exact_balance",
				MaxRequestHeadersKB:       uintPointer(96),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ParseProxyConfig(tt.input)
			require.NoError(t, err)
			require.Equal(t, tt.want, got)
		})
	}
}

func TestParseGatewayConfig(t *testing.T) {
	tests := []struct {
		name  string
		input map[string]interface{}
		want  GatewayConfig
	}{
		{
			name:  "defaults - nil",
			input: nil,
			want: GatewayConfig{
				ConnectTimeoutMs: 5000,
			},
		},
		{
			name:  "defaults - empty",
			input: map[string]interface{}{},
			want: GatewayConfig{
				ConnectTimeoutMs: 5000,
			},
		},
		{
			name: "defaults - other stuff",
			input: map[string]interface{}{
				"foo":       "bar",
				"envoy_foo": "envoy_bar",
			},
			want: GatewayConfig{
				ConnectTimeoutMs: 5000,
			},
		},
		{
			name: "kitchen sink",
			input: map[string]interface{}{
				"envoy_gateway_bind_tagged_addresses": true,
				"envoy_gateway_bind_addresses":        map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
				"envoy_gateway_no_default_bind":       true,
				"envoy_dns_discovery_type":            "StRiCt_DnS",
				"connect_timeout_ms":                  10,
			},
			want: GatewayConfig{
				ConnectTimeoutMs:    10,
				BindTaggedAddresses: true,
				NoDefaultBind:       true,
				BindAddresses:       map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
				DNSDiscoveryType:    "strict_dns",
			},
		},
		{
			name: "deprecated kitchen sink",
			input: map[string]interface{}{
				"envoy_mesh_gateway_bind_tagged_addresses": true,
				"envoy_mesh_gateway_bind_addresses":        map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
				"envoy_mesh_gateway_no_default_bind":       true,
				"connect_timeout_ms":                       10,
			},
			want: GatewayConfig{
				ConnectTimeoutMs:    10,
				BindTaggedAddresses: true,
				NoDefaultBind:       true,
				BindAddresses:       map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
			},
		},
		{
			name: "new fields override deprecated ones",
			input: map[string]interface{}{
				// Deprecated
				"envoy_mesh_gateway_bind_tagged_addresses": true,
				"envoy_mesh_gateway_bind_addresses":        map[string]structs.ServiceAddress{"foo": {Address: "127.0.0.1", Port: 80}},
				"envoy_mesh_gateway_no_default_bind":       true,

				// New
				"envoy_gateway_bind_tagged_addresses": false,
				"envoy_gateway_bind_addresses":        map[string]structs.ServiceAddress{"bar": {Address: "127.0.0.1", Port: 8080}},
				"envoy_gateway_no_default_bind":       false,
			},
			want: GatewayConfig{
				ConnectTimeoutMs:    5000,
				BindTaggedAddresses: false,
				NoDefaultBind:       false,
				BindAddresses:       map[string]structs.ServiceAddress{"bar": {Address: "127.0.0.1", Port: 8080}},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ParseGatewayConfig(tt.input)
			require.NoError(t, err)
			require.Equal(t, tt.want, got)
		})
	}
}

func intPointer(i int) *int {
	return &i
}

func uintPointer(i uint32) *uint32 {
	return &i
}

func Test_ProxyConfig_GetXDSFetchTimeout(t *testing.T) {
	tests := map[string]struct {
		conf      map[string]interface{}
		expect    *durationpb.Duration
		expectErr bool
	}{
		"nil_conf": {
			expect: nil,
			conf:   nil,
		},
		"key_not_found": {
			expect: nil,
			conf:   map[string]interface{}{},
		},
		"invalid_key": {
			expect: nil,
			conf: map[string]interface{}{
				"i_do_not_exist": "as_an_expected_field",
			},
		},
		"nil_value_for_key": {
			expect: nil,
			conf: map[string]interface{}{
				"xds_fetch_timeout_ms": nil,
			},
		},
		"bad_value_for_key": {
			expectErr: true,
			conf: map[string]interface{}{
				"xds_fetch_timeout_ms": "bad",
			},
		},
		"negative_value_for_key": {
			expect: nil,
			conf: map[string]interface{}{
				"xds_fetch_timeout_ms": -1,
			},
		},
		"valid_value_for_key": {
			expect: durationpb.New(99 * time.Millisecond),
			conf: map[string]interface{}{
				"xds_fetch_timeout_ms": 99,
			},
		},
	}
	for n, tc := range tests {
		t.Run(n, func(t *testing.T) {
			conf, err := ParseXDSCommonConfig(tc.conf)
			if tc.expectErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
			require.True(t, proto.Equal(tc.expect, conf.GetXDSFetchTimeout()))
		})
	}
}
