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

package api

import (
	"testing"
	"time"

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

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

	config_entries := c.ConfigEntries()

	ingress1 := &IngressGatewayConfigEntry{
		Kind: IngressGateway,
		Name: "foo",
		Meta: map[string]string{
			"foo": "bar",
			"gir": "zim",
		},
	}

	ingress2 := &IngressGatewayConfigEntry{
		Kind: IngressGateway,
		Name: "bar",
		TLS: GatewayTLSConfig{
			Enabled:       true,
			TLSMinVersion: "TLSv1_2",
		},
		Defaults: &IngressServiceConfig{
			MaxConnections:     uint32Pointer(2048),
			MaxPendingRequests: uint32Pointer(4096),
			PassiveHealthCheck: &PassiveHealthCheck{
				MaxFailures: 20,
				Interval:    500000000,
			},
		},
	}

	global := &ProxyConfigEntry{
		Kind: ProxyDefaults,
		Name: ProxyConfigGlobal,
		Config: map[string]interface{}{
			"protocol": "http",
		},
	}
	// set default protocol to http so that ingress gateways pass validation
	_, wm, err := config_entries.Set(global, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

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

	// also set the second one
	_, wm, err = config_entries.Set(ingress2, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// get it
	entry, qm, err := config_entries.Get(IngressGateway, "foo", nil)
	require.NoError(t, err)
	require.NotNil(t, qm)
	require.NotEqual(t, 0, qm.RequestTime)

	// verify it
	readIngress, ok := entry.(*IngressGatewayConfigEntry)
	require.True(t, ok)
	require.Equal(t, ingress1.Kind, readIngress.Kind)
	require.Equal(t, ingress1.Name, readIngress.Name)
	require.Equal(t, ingress1.Meta, readIngress.Meta)
	require.Equal(t, ingress1.Meta, readIngress.GetMeta())

	// update it
	ingress1.Listeners = []IngressListener{
		{
			Port:     2222,
			Protocol: "http",
			Services: []IngressService{
				{
					Name:  "asdf",
					Hosts: []string{"test.example.com"},
					RequestHeaders: &HTTPHeaderModifiers{
						Set: map[string]string{
							"x-foo": "bar",
						},
					},
					ResponseHeaders: &HTTPHeaderModifiers{
						Remove: []string{"x-foo"},
					},
					TLS: &GatewayServiceTLSConfig{
						SDS: &GatewayTLSSDSConfig{
							ClusterName:  "foo",
							CertResource: "bar",
						},
					},
					MaxConnections:        uint32Pointer(5120),
					MaxPendingRequests:    uint32Pointer(512),
					MaxConcurrentRequests: uint32Pointer(2048),
					PassiveHealthCheck: &PassiveHealthCheck{
						MaxFailures: 10,
					},
				},
			},
			TLS: &GatewayTLSConfig{
				SDS: &GatewayTLSSDSConfig{
					ClusterName:  "baz",
					CertResource: "qux",
				},
			},
		},
	}
	ingress1.TLS = GatewayTLSConfig{
		SDS: &GatewayTLSSDSConfig{
			ClusterName:  "qux",
			CertResource: "bug",
		},
	}

	// CAS fail
	written, _, err := config_entries.CAS(ingress1, 0, nil)
	require.NoError(t, err)
	require.False(t, written)

	// CAS success
	written, wm, err = config_entries.CAS(ingress1, readIngress.ModifyIndex, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)
	require.True(t, written)

	// update no cas
	ingress2.Listeners = []IngressListener{
		{
			Port:     3333,
			Protocol: "http",
			Services: []IngressService{
				{
					Name: "qwer",
				},
			},
		},
	}
	_, wm, err = config_entries.Set(ingress2, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// list them
	entries, qm, err := config_entries.List(IngressGateway, nil)
	require.NoError(t, err)
	require.NotNil(t, qm)
	require.NotEqual(t, 0, qm.RequestTime)
	require.Len(t, entries, 2)

	for _, entry = range entries {
		switch entry.GetName() {
		case "foo":
			// this also verifies that the update value was persisted and
			// the updated values are seen
			readIngress, ok = entry.(*IngressGatewayConfigEntry)
			require.True(t, ok)
			require.Equal(t, ingress1.Kind, readIngress.Kind)
			require.Equal(t, ingress1.Name, readIngress.Name)

			require.Len(t, readIngress.Listeners, 1)
			require.Len(t, readIngress.Listeners[0].Services, 1)
			// Set namespace and partition to blank so that CE and ent can utilize the same tests
			readIngress.Listeners[0].Services[0].Namespace = ""
			readIngress.Listeners[0].Services[0].Partition = ""

			require.Equal(t, ingress1.Listeners, readIngress.Listeners)
		case "bar":
			readIngress, ok = entry.(*IngressGatewayConfigEntry)
			require.True(t, ok)
			require.Equal(t, ingress2.Kind, readIngress.Kind)
			require.Equal(t, ingress2.Name, readIngress.Name)
			require.Equal(t, *ingress2.Defaults.MaxConnections, *readIngress.Defaults.MaxConnections)
			require.Equal(t, uint32(4096), *readIngress.Defaults.MaxPendingRequests)
			require.Equal(t, uint32(0), *readIngress.Defaults.MaxConcurrentRequests)
			require.Equal(t, uint32(20), readIngress.Defaults.PassiveHealthCheck.MaxFailures)
			require.Equal(t, time.Duration(500000000), readIngress.Defaults.PassiveHealthCheck.Interval)
			require.Nil(t, readIngress.Defaults.PassiveHealthCheck.EnforcingConsecutive5xx)

			require.Len(t, readIngress.Listeners, 1)
			require.Len(t, readIngress.Listeners[0].Services, 1)
			// Set namespace and partition to blank so that CE and ent can utilize the same tests
			readIngress.Listeners[0].Services[0].Namespace = ""
			readIngress.Listeners[0].Services[0].Partition = ""

			require.Equal(t, ingress2.Listeners, readIngress.Listeners)
		}
	}

	// delete it
	wm, err = config_entries.Delete(IngressGateway, "foo", nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// verify deletion
	_, _, err = config_entries.Get(IngressGateway, "foo", nil)
	require.Error(t, err)
}

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

	configEntries := c.ConfigEntries()

	terminating1 := &TerminatingGatewayConfigEntry{
		Kind: TerminatingGateway,
		Name: "foo",
		Meta: map[string]string{
			"foo": "bar",
			"gir": "zim",
		},
	}

	terminating2 := &TerminatingGatewayConfigEntry{
		Kind: TerminatingGateway,
		Name: "bar",
	}

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

	// also set the second one
	_, wm, err = configEntries.Set(terminating2, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// get it
	entry, qm, err := configEntries.Get(TerminatingGateway, "foo", nil)
	require.NoError(t, err)
	require.NotNil(t, qm)
	require.NotEqual(t, 0, qm.RequestTime)

	// verify it
	readTerminating, ok := entry.(*TerminatingGatewayConfigEntry)
	require.True(t, ok)
	require.Equal(t, terminating1.Kind, readTerminating.Kind)
	require.Equal(t, terminating1.Name, readTerminating.Name)
	require.Equal(t, terminating1.Meta, readTerminating.Meta)
	require.Equal(t, terminating1.Meta, readTerminating.GetMeta())

	// update it
	terminating1.Services = []LinkedService{
		{
			Name:     "web",
			CAFile:   "/etc/web/ca.crt",
			CertFile: "/etc/web/client.crt",
			KeyFile:  "/etc/web/tls.key",
			SNI:      "mydomain",
		},
	}

	// CAS fail
	written, _, err := configEntries.CAS(terminating1, 0, nil)
	require.NoError(t, err)
	require.False(t, written)

	// CAS success
	written, wm, err = configEntries.CAS(terminating1, readTerminating.ModifyIndex, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)
	require.True(t, written)

	// re-setting should not yield an error
	_, wm, err = configEntries.Set(terminating1, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	terminating2.Services = []LinkedService{
		{
			Name:     "*",
			CAFile:   "/etc/certs/ca.crt",
			CertFile: "/etc/certs/client.crt",
			KeyFile:  "/etc/certs/tls.key",
			SNI:      "mydomain",
		},
	}
	_, wm, err = configEntries.Set(terminating2, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// list them
	entries, qm, err := configEntries.List(TerminatingGateway, nil)
	require.NoError(t, err)
	require.NotNil(t, qm)
	require.NotEqual(t, 0, qm.RequestTime)
	require.Len(t, entries, 2)

	for _, entry = range entries {
		switch entry.GetName() {
		case "foo":
			// this also verifies that the update value was persisted and
			// the updated values are seen
			readTerminating, ok = entry.(*TerminatingGatewayConfigEntry)
			require.True(t, ok)
			require.Equal(t, terminating1.Kind, readTerminating.Kind)
			require.Equal(t, terminating1.Name, readTerminating.Name)
			require.Len(t, readTerminating.Services, 1)
			// Set namespace to blank so that CE and ent can utilize the same tests
			readTerminating.Services[0].Namespace = ""

			require.Equal(t, terminating1.Services, readTerminating.Services)
		case "bar":
			readTerminating, ok = entry.(*TerminatingGatewayConfigEntry)
			require.True(t, ok)
			require.Equal(t, terminating2.Kind, readTerminating.Kind)
			require.Equal(t, terminating2.Name, readTerminating.Name)
			require.Len(t, readTerminating.Services, 1)
			// Set namespace to blank so that CE and ent can utilize the same tests
			readTerminating.Services[0].Namespace = ""

			require.Equal(t, terminating2.Services, readTerminating.Services)
		}
	}

	// delete it
	wm, err = configEntries.Delete(TerminatingGateway, "foo", nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// verify deletion
	_, _, err = configEntries.Get(TerminatingGateway, "foo", nil)
	require.Error(t, err)
}

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

	configEntries := c.ConfigEntries()
	listener1 := APIGatewayListener{
		Name:     "listener1",
		Hostname: "host.com",
		Port:     3360,
		Protocol: "http",
	}

	listener2 := APIGatewayListener{
		Name:     "listener2",
		Hostname: "host2.com",
		Port:     3362,
		Protocol: "http",
	}

	// Listener with MaxRequestHeadersKB configured
	listenerWithHeaders := APIGatewayListener{
		Name:                "listener-with-headers",
		Hostname:            "headers.com",
		Port:                3363,
		Protocol:            "http",
		MaxRequestHeadersKB: uintPointer(128),
	}

	apigw1 := &APIGatewayConfigEntry{
		Kind: APIGateway,
		Name: "foo",
		Meta: map[string]string{
			"foo": "bar",
			"gir": "zim",
		},
		Listeners: []APIGatewayListener{listener1},
	}

	apigw2 := &APIGatewayConfigEntry{
		Kind:      APIGateway,
		Name:      "bar",
		Listeners: []APIGatewayListener{listener2},
	}

	// API Gateway with MaxRequestHeadersKB
	apigw3 := &APIGatewayConfigEntry{
		Kind:      APIGateway,
		Name:      "headers-gw",
		Listeners: []APIGatewayListener{listenerWithHeaders},
	}

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

	// also set the second one
	_, wm, err = configEntries.Set(apigw2, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// set the third one with MaxRequestHeadersKB
	_, wm, err = configEntries.Set(apigw3, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// get it
	entry, qm, err := configEntries.Get(APIGateway, "foo", nil)
	require.NoError(t, err)
	require.NotNil(t, qm)
	require.NotEqual(t, 0, qm.RequestTime)

	// verify it
	readGW, ok := entry.(*APIGatewayConfigEntry)
	require.True(t, ok)
	require.Equal(t, apigw1.Kind, readGW.Kind)
	require.Equal(t, apigw1.Name, readGW.Name)
	require.Equal(t, apigw1.Meta, readGW.Meta)
	require.Equal(t, apigw1.Meta, readGW.GetMeta())

	// update it
	apigw1.Listeners = []APIGatewayListener{
		listener1,
		{
			Name:     "listener3",
			Hostname: "host3.com",
			Port:     3363,
			Protocol: "http",
		},
	}

	// CAS fail
	written, _, err := configEntries.CAS(apigw1, 0, nil)
	require.NoError(t, err)
	require.False(t, written)

	// CAS success
	written, wm, err = configEntries.CAS(apigw1, readGW.ModifyIndex, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)
	require.True(t, written)

	// re-setting should not yield an error
	_, wm, err = configEntries.Set(apigw1, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	apigw2.Listeners = []APIGatewayListener{
		listener2,
		{
			Name:     "listener4",
			Hostname: "host4.com",
			Port:     3364,
			Protocol: "http",
		},
	}

	_, wm, err = configEntries.Set(apigw2, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// list them
	entries, qm, err := configEntries.List(APIGateway, nil)
	require.NoError(t, err)
	require.NotNil(t, qm)
	require.NotEqual(t, 0, qm.RequestTime)
	require.Len(t, entries, 3)

	for _, entry = range entries {
		switch entry.GetName() {
		case "foo":
			// this also verifies that the update value was persisted and
			// the updated values are seen
			readGW, ok = entry.(*APIGatewayConfigEntry)
			require.True(t, ok)
			require.Equal(t, apigw1.Kind, readGW.Kind)
			require.Equal(t, apigw1.Name, readGW.Name)
			require.Len(t, readGW.Listeners, 2)

			require.Equal(t, apigw1.Listeners, readGW.Listeners)
		case "bar":
			readGW, ok = entry.(*APIGatewayConfigEntry)
			require.True(t, ok)
			require.Equal(t, apigw2.Kind, readGW.Kind)
			require.Equal(t, apigw2.Name, readGW.Name)
			require.Len(t, readGW.Listeners, 2)

			require.Equal(t, apigw2.Listeners, readGW.Listeners)
		case "headers-gw":
			readGW, ok = entry.(*APIGatewayConfigEntry)
			require.True(t, ok)
			require.Equal(t, apigw3.Kind, readGW.Kind)
			require.Equal(t, apigw3.Name, readGW.Name)
			require.Len(t, readGW.Listeners, 1)

			// Verify MaxRequestHeadersKB is properly handled
			require.Equal(t, apigw3.Listeners, readGW.Listeners)
			require.NotNil(t, readGW.Listeners[0].MaxRequestHeadersKB)
			require.Equal(t, uint32(128), *readGW.Listeners[0].MaxRequestHeadersKB)
		}
	}

	// delete them
	wm, err = configEntries.Delete(APIGateway, "foo", nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	wm, err = configEntries.Delete(APIGateway, "bar", nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	wm, err = configEntries.Delete(APIGateway, "headers-gw", nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotEqual(t, 0, wm.RequestTime)

	// verify deletion
	_, _, err = configEntries.Get(APIGateway, "foo", nil)
	require.Error(t, err)

	_, _, err = configEntries.Get(APIGateway, "headers-gw", nil)
	require.Error(t, err)
}

// Helper function to create uint32 pointers for testing
func uintPointer(u uint32) *uint32 {
	return &u
}
