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

package api

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/consul/sdk/testutil/retry"
)

const DefaultCtxDuration = 15 * time.Second

func peerExistsInPeerListings(peer *Peering, peerings []*Peering) bool {
	for _, aPeer := range peerings {
		isEqual := (peer.PeerID == aPeer.PeerID) &&
			(reflect.DeepEqual(peer.PeerCAPems, aPeer.PeerCAPems)) &&
			(peer.PeerServerName == aPeer.PeerServerName) &&
			(peer.Partition == aPeer.Partition) &&
			(peer.Name == aPeer.Name) &&
			(reflect.DeepEqual(peer.PeerServerAddresses, aPeer.PeerServerAddresses)) &&
			(peer.State == aPeer.State) &&
			(peer.CreateIndex == aPeer.CreateIndex) &&
			(peer.ModifyIndex == aPeer.ModifyIndex) &&
			(reflect.DeepEqual(peer.StreamStatus, aPeer.StreamStatus))

		if isEqual {
			return true
		}
	}

	return false
}

func TestAPI_Peering_ACLDeny(t *testing.T) {
	c1, s1 := makeClientWithConfig(t, nil, func(serverConfig *testutil.TestServerConfig) {
		serverConfig.ACL.Tokens.InitialManagement = "root"
		serverConfig.ACL.Enabled = true
		serverConfig.ACL.DefaultPolicy = "deny"
	})
	defer s1.Stop()

	c2, s2 := makeClientWithConfig(t, nil, func(serverConfig *testutil.TestServerConfig) {
		serverConfig.ACL.Tokens.InitialManagement = "root"
		serverConfig.ACL.Enabled = true
		serverConfig.ACL.DefaultPolicy = "deny"
		serverConfig.Datacenter = "dc2"
	})
	defer s2.Stop()

	var peeringToken string
	testutil.RunStep(t, "generate token", func(t *testing.T) {
		peerings := c1.Peerings()

		req := PeeringGenerateTokenRequest{PeerName: "peer1"}

		testutil.RunStep(t, "without ACL token", func(t *testing.T) {
			_, _, err := peerings.GenerateToken(context.Background(), req, &WriteOptions{Token: "anonymous"})
			require.Error(t, err)
			testutil.RequireErrorContains(t, err, "Permission denied")
		})

		testutil.RunStep(t, "with ACL token", func(t *testing.T) {
			resp, wm, err := peerings.GenerateToken(context.Background(), req, &WriteOptions{Token: "root"})
			require.NoError(t, err)
			require.NotNil(t, wm)
			require.NotNil(t, resp)

			peeringToken = resp.PeeringToken
		})
	})

	testutil.RunStep(t, "establish peering", func(t *testing.T) {
		peerings := c2.Peerings()

		req := PeeringEstablishRequest{
			PeerName:     "peer2",
			PeeringToken: peeringToken,
		}
		testutil.RunStep(t, "without ACL token", func(t *testing.T) {
			_, _, err := peerings.Establish(context.Background(), req, &WriteOptions{Token: "anonymous"})
			require.Error(t, err)
			testutil.RequireErrorContains(t, err, "Permission denied")
		})

		testutil.RunStep(t, "with ACL token", func(t *testing.T) {
			resp, wm, err := peerings.Establish(context.Background(), req, &WriteOptions{Token: "root"})
			require.NoError(t, err)
			require.NotNil(t, wm)
			require.NotNil(t, resp)
		})
	})

	testutil.RunStep(t, "read peering", func(t *testing.T) {
		peerings := c1.Peerings()

		testutil.RunStep(t, "without ACL token", func(t *testing.T) {
			_, _, err := peerings.Read(context.Background(), "peer1", &QueryOptions{Token: "anonymous"})
			require.Error(t, err)
			testutil.RequireErrorContains(t, err, "Permission denied")
		})

		testutil.RunStep(t, "with ACL token", func(t *testing.T) {
			resp, qm, err := peerings.Read(context.Background(), "peer1", &QueryOptions{Token: "root"})
			require.NoError(t, err)
			require.NotNil(t, qm)
			require.NotNil(t, resp)
		})
	})

	testutil.RunStep(t, "list peerings", func(t *testing.T) {
		peerings := c1.Peerings()

		testutil.RunStep(t, "without ACL token", func(t *testing.T) {
			_, _, err := peerings.List(context.Background(), &QueryOptions{Token: "anonymous"})
			require.Error(t, err)
			testutil.RequireErrorContains(t, err, "Permission denied")
		})

		testutil.RunStep(t, "with ACL token", func(t *testing.T) {
			resp, qm, err := peerings.List(context.Background(), &QueryOptions{Token: "root"})
			require.NoError(t, err)
			require.NotNil(t, qm)
			require.NotNil(t, resp)
			require.Len(t, resp, 1)
		})
	})

	testutil.RunStep(t, "delete peering", func(t *testing.T) {
		peerings := c1.Peerings()

		testutil.RunStep(t, "without ACL token", func(t *testing.T) {
			_, err := peerings.Delete(context.Background(), "peer1", &WriteOptions{Token: "anonymous"})
			require.Error(t, err)
			testutil.RequireErrorContains(t, err, "Permission denied")
		})

		testutil.RunStep(t, "with ACL token", func(t *testing.T) {
			wm, err := peerings.Delete(context.Background(), "peer1", &WriteOptions{Token: "root"})
			require.NoError(t, err)
			require.NotNil(t, wm)
		})
	})
}

func TestAPI_Peering_Read_ErrorHandling(t *testing.T) {
	t.Parallel()

	c, s := makeClientWithCA(t)
	defer s.Stop()
	s.WaitForSerfCheck(t)

	ctx, cancel := context.WithTimeout(context.Background(), DefaultCtxDuration)
	defer cancel()

	peerings := c.Peerings()

	t.Run("call Read with no name", func(t *testing.T) {
		_, _, err := peerings.Read(ctx, "", nil)
		require.EqualError(t, err, "peering name cannot be empty")
	})

	t.Run("read peer that does not exist on server", func(t *testing.T) {
		resp, qm, err := peerings.Read(ctx, "peer1", nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.Nil(t, resp)
	})
}

// TestAPI_Peering_List
func TestAPI_Peering_List(t *testing.T) {
	t.Parallel()

	c, s := makeClientWithCA(t)
	defer s.Stop()
	s.WaitForSerfCheck(t)

	ctx, cancel := context.WithTimeout(context.Background(), DefaultCtxDuration)
	defer cancel()

	peerings := c.Peerings()

	testutil.RunStep(t, "list with no peers", func(t *testing.T) {
		// call List when no peers should exist
		resp, qm, err := peerings.List(ctx, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.Empty(t, resp) // no peerings so this should be empty
	})

	testutil.RunStep(t, "list with some peers", func(t *testing.T) {
		// call List when peers are present
		resp1, wm, err := peerings.GenerateToken(ctx, PeeringGenerateTokenRequest{PeerName: "peer1"}, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotNil(t, resp1)

		resp2, wm, err := peerings.GenerateToken(ctx, PeeringGenerateTokenRequest{PeerName: "peer2"}, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotNil(t, resp2)

		peering1, qm, err := peerings.Read(ctx, "peer1", nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotNil(t, peering1)

		peering2, qm, err := peerings.Read(ctx, "peer2", nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotNil(t, peering2)

		peeringsList, qm, err := peerings.List(ctx, nil)
		require.NoError(t, err)
		require.NotNil(t, qm)

		require.Len(t, peeringsList, 2)
		require.True(t, peerExistsInPeerListings(peering1, peeringsList), "expected to find peering in list response")
		require.True(t, peerExistsInPeerListings(peering2, peeringsList), "expected to find peering in list response")
	})
}

func TestAPI_Peering_GenerateToken_ExternalAddresses(t *testing.T) {
	t.Parallel()

	c, s := makeClient(t) // this is "dc1"
	defer s.Stop()
	s.WaitForSerfCheck(t)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	externalAddress := "32.1.2.3:8502"

	// Generate a token happy path
	p1 := PeeringGenerateTokenRequest{
		PeerName:                "peer1",
		Meta:                    map[string]string{"foo": "bar"},
		ServerExternalAddresses: []string{externalAddress},
	}
	resp, wm, err := c.Peerings().GenerateToken(ctx, p1, nil)
	require.NoError(t, err)
	require.NotNil(t, wm)
	require.NotNil(t, resp)

	tokenJSON, err := base64.StdEncoding.DecodeString(resp.PeeringToken)
	require.NoError(t, err)

	// Put the token in an arbitrary map, because the struct isn't available in the api package.
	token := make(map[string]interface{})
	require.NoError(t, json.Unmarshal(tokenJSON, &token))
	require.Equal(t, []interface{}{s.GRPCTLSAddr}, token["ServerAddresses"])
	require.Equal(t, []interface{}{externalAddress}, token["ManualServerAddresses"])
}

// TestAPI_Peering_GenerateToken_Read_Establish_Delete tests the following use case:
// a server creates a peering token, reads the token, then another server calls establish peering
// finally, we delete the token on the first server
func TestAPI_Peering_GenerateToken_Read_Establish_Delete(t *testing.T) {
	t.Parallel()

	c, s := makeClientWithConfig(t, nil, nil) // this is "dc1"
	defer s.Stop()
	s.WaitForSerfCheck(t)

	// make a "client" server in second DC for peering
	c2, s2 := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
		conf.Datacenter = "dc2"
	})
	defer s2.Stop()

	testNodeServiceCheckRegistrations(t, c2, "dc2")

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	var token1 string
	testutil.RunStep(t, "generate token", func(t *testing.T) {
		// Generate a token happy path
		p1 := PeeringGenerateTokenRequest{
			PeerName: "peer1",
			Meta:     map[string]string{"foo": "bar"},
		}
		resp, wm, err := c.Peerings().GenerateToken(ctx, p1, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)
		require.NotNil(t, resp)

		token1 = resp.PeeringToken
	})

	testutil.RunStep(t, "verify token", func(t *testing.T) {
		// Read token generated on server
		resp, qm, err := c.Peerings().Read(ctx, "peer1", nil)
		require.NoError(t, err)
		require.NotNil(t, qm)
		require.NotNil(t, resp)

		// token specific assertions on the "server"
		require.Equal(t, "peer1", resp.Name)
		require.Equal(t, PeeringStatePending, resp.State)
		require.Equal(t, map[string]string{"foo": "bar"}, resp.Meta)
	})

	testutil.RunStep(t, "establish peering", func(t *testing.T) {
		i := PeeringEstablishRequest{
			PeerName:     "peer1",
			PeeringToken: token1,
			Meta:         map[string]string{"foo": "bar"},
		}

		_, wm, err := c2.Peerings().Establish(ctx, i, nil)
		require.NoError(t, err)
		require.NotNil(t, wm)

		retry.Run(t, func(r *retry.R) {
			resp, qm, err := c2.Peerings().Read(ctx, "peer1", nil)
			require.NoError(r, err)
			require.NotNil(r, qm)

			// require that the peering state is not undefined
			require.Equal(r, PeeringStateEstablishing, resp.State)
			require.Equal(r, map[string]string{"foo": "bar"}, resp.Meta)
		})
	})

	testutil.RunStep(t, "look for active state of peering in dc2", func(t *testing.T) {
		// read and list the peer to make sure the status transitions to active
		retry.Run(t, func(r *retry.R) {
			peering, qm, err := c2.Peerings().Read(ctx, "peer1", nil)
			require.NoError(r, err)
			require.NotNil(r, qm)
			require.NotNil(r, peering)
			require.Equal(r, PeeringStateActive, peering.State)

			peerings, qm, err := c2.Peerings().List(ctx, nil)

			require.NoError(r, err)
			require.NotNil(r, qm)
			require.NotNil(r, peerings)
			require.Equal(r, PeeringStateActive, peerings[0].State)
		})
	})

	testutil.RunStep(t, "look for active state of peering in dc1", func(t *testing.T) {
		// read and list the peer to make sure the status transitions to active
		retry.Run(t, func(r *retry.R) {
			peering, qm, err := c.Peerings().Read(ctx, "peer1", nil)
			require.NoError(r, err)
			require.NotNil(r, qm)
			require.NotNil(r, peering)
			require.Equal(r, PeeringStateActive, peering.State)

			peerings, qm, err := c.Peerings().List(ctx, nil)

			require.NoError(r, err)
			require.NotNil(r, qm)
			require.NotNil(r, peerings)
			require.Equal(r, PeeringStateActive, peerings[0].State)
		})
	})

	testutil.RunStep(t, "delete peering at source", func(t *testing.T) {
		// Delete the token on server 1
		wm, err := c.Peerings().Delete(ctx, "peer1", nil)
		require.NoError(t, err)
		require.NotNil(t, wm)

		// Read to see if the token is gone
		retry.Run(t, func(r *retry.R) {
			resp, qm, err := c.Peerings().Read(ctx, "peer1", nil)
			require.NoError(r, err)
			require.NotNil(r, qm)
			require.Nil(r, resp)
		})
	})
}
