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

package peering

import (
	"fmt"
	"net/http"
	"net/url"
	"testing"

	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/testing/deployer/topology"
	"github.com/hashicorp/go-cleanhttp"
	"github.com/stretchr/testify/require"
)

type ac4ProxyDefaultsSuite struct {
	DC   string
	Peer string

	nodeClient topology.NodeID
	nodeServer topology.NodeID

	serverSID topology.ID
	clientSID topology.ID
	upstream  *topology.Destination
}

var ac4ProxyDefaultsSuites []sharedTopoSuite = []sharedTopoSuite{
	&ac4ProxyDefaultsSuite{DC: "dc1", Peer: "dc2"},
	&ac4ProxyDefaultsSuite{DC: "dc2", Peer: "dc1"},
}

func TestAC4ProxyDefaults(t *testing.T) {
	runShareableSuites(t, ac4ProxyDefaultsSuites)
}

func (s *ac4ProxyDefaultsSuite) testName() string {
	return fmt.Sprintf("ac4 proxy defaults %s->%s", s.DC, s.Peer)
}

// creates clients in s.DC and servers in s.Peer
func (s *ac4ProxyDefaultsSuite) setup(t *testing.T, ct *commonTopo) {
	clu := ct.ClusterByDatacenter(t, s.DC)
	peerClu := ct.ClusterByDatacenter(t, s.Peer)

	partition := "default"
	peer := LocalPeerName(peerClu, "default")
	cluPeerName := LocalPeerName(clu, "default")

	serverSID := topology.ID{
		Name:      "ac4-server-http",
		Partition: partition,
	}
	// Define server as upstream for client
	upstream := &topology.Destination{
		ID:        serverSID,
		LocalPort: 5000,
		Peer:      peer,
	}

	// Make client which will dial server
	clientSID := topology.ID{
		Name:      "ac4-http-client",
		Partition: partition,
	}
	client := serviceExt{
		Workload: NewFortioServiceWithDefaults(
			clu.Datacenter,
			clientSID,
			func(s *topology.Workload) {
				s.Destinations = []*topology.Destination{
					upstream,
				}
			},
		),
		Config: &api.ServiceConfigEntry{
			Kind:      api.ServiceDefaults,
			Name:      clientSID.Name,
			Partition: ConfigEntryPartition(clientSID.Partition),
			Protocol:  "http",
			UpstreamConfig: &api.UpstreamConfiguration{
				Defaults: &api.UpstreamConfig{
					MeshGateway: api.MeshGatewayConfig{
						Mode: api.MeshGatewayModeLocal,
					},
				},
			},
		},
	}
	clientNode := ct.AddServiceNode(clu, client)

	server := serviceExt{
		Workload: NewFortioServiceWithDefaults(
			peerClu.Datacenter,
			serverSID,
			nil,
		),
		Config: &api.ServiceConfigEntry{
			Kind:      api.ServiceDefaults,
			Name:      serverSID.Name,
			Partition: ConfigEntryPartition(serverSID.Partition),
			Protocol:  "http",
		},
		Exports: []api.ServiceConsumer{{Peer: cluPeerName}},
		Intentions: &api.ServiceIntentionsConfigEntry{
			Kind:      api.ServiceIntentions,
			Name:      serverSID.Name,
			Partition: ConfigEntryPartition(serverSID.Partition),
			Sources: []*api.SourceIntention{
				{
					Name:   client.ID.Name,
					Peer:   cluPeerName,
					Action: api.IntentionActionAllow,
				},
			},
		},
	}

	peerClu.InitialConfigEntries = append(peerClu.InitialConfigEntries,
		&api.ProxyConfigEntry{
			Kind:      api.ProxyDefaults,
			Name:      api.ProxyConfigGlobal,
			Partition: ConfigEntryPartition(server.ID.Partition),
			Config: map[string]interface{}{
				"protocol":                 "http",
				"local_request_timeout_ms": 500,
			},
			MeshGateway: api.MeshGatewayConfig{
				Mode: api.MeshGatewayModeLocal,
			},
		},
	)

	serverNode := ct.AddServiceNode(peerClu, server)

	s.clientSID = clientSID
	s.serverSID = serverSID
	s.nodeServer = serverNode.ID()
	s.nodeClient = clientNode.ID()
	s.upstream = upstream
}

func (s *ac4ProxyDefaultsSuite) test(t *testing.T, ct *commonTopo) {
	var client *topology.Workload

	dc := ct.Sprawl.Topology().Clusters[s.DC]
	peer := ct.Sprawl.Topology().Clusters[s.Peer]

	clientSVC := dc.WorkloadByID(
		s.nodeClient,
		s.clientSID,
	)
	serverSVC := peer.WorkloadByID(
		s.nodeServer,
		s.serverSID,
	)

	// preconditions check
	ct.Assert.HealthyWithPeer(t, dc.Name, serverSVC.ID, LocalPeerName(peer, "default"))
	ct.Assert.FortioFetch2HeaderEcho(t, clientSVC, s.upstream)

	t.Run("Validate services exist in catalog", func(t *testing.T) {
		dcSvcs := dc.WorkloadsByID(s.clientSID)
		require.Len(t, dcSvcs, 1, "expected exactly one client")
		client = dcSvcs[0]
		require.Len(t, client.Destinations, 1, "expected exactly one upstream for client")

		server := dc.WorkloadsByID(s.serverSID)
		require.Len(t, server, 1, "expected exactly one server")
		require.Len(t, server[0].Destinations, 0, "expected no upstream for server")
	})

	t.Run("peered upstream exists in catalog", func(t *testing.T) {
		ct.Assert.CatalogServiceExists(t, s.DC, s.upstream.ID.Name, &api.QueryOptions{
			Peer: s.upstream.Peer,
		})
	})

	t.Run("HTTP service fails due to connection timeout", func(t *testing.T) {
		url504 := fmt.Sprintf("http://localhost:%d/fortio/fetch2?url=%s", client.ExposedPort(""),
			url.QueryEscape(fmt.Sprintf("http://localhost:%d/?delay=1000ms", s.upstream.LocalPort)),
		)

		url200 := fmt.Sprintf("http://localhost:%d/fortio/fetch2?url=%s", client.ExposedPort(""),
			url.QueryEscape(fmt.Sprintf("http://localhost:%d/", s.upstream.LocalPort)),
		)

		// validate request timeout error where service has 1000ms response delay and
		// proxy default is set to local_request_timeout_ms: 500ms
		// return 504
		httpClient := cleanhttp.DefaultClient()
		req, err := http.NewRequest(http.MethodGet, url504, nil)
		require.NoError(t, err)

		res, err := httpClient.Do(req)
		require.NoError(t, err)

		defer res.Body.Close()
		require.Equal(t, http.StatusGatewayTimeout, res.StatusCode)

		// validate successful GET request where service has no response delay and
		// proxy default is set to local_request_timeout_ms: 500ms
		// return 200
		req, err = http.NewRequest(http.MethodGet, url200, nil)
		require.NoError(t, err)

		res, err = httpClient.Do(req)
		require.NoError(t, err)

		defer res.Body.Close()
		require.Equal(t, http.StatusOK, res.StatusCode)
	})
}
