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

package consul

import (
	"testing"

	uuid "github.com/hashicorp/go-uuid"
	"github.com/hashicorp/serf/serf"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/consul/types"
	"github.com/hashicorp/consul/version"
)

func TestMerge_LAN(t *testing.T) {
	type testcase struct {
		members []*serf.Member
		expect  string
	}

	const thisNodeID = "ee954a2f-80de-4b34-8780-97b942a50a99"

	run := func(t *testing.T, tc testcase) {
		delegate := &lanMergeDelegate{
			dc:       "dc1",
			nodeID:   types.NodeID(thisNodeID),
			nodeName: "node0",
		}

		err := delegate.NotifyMerge(tc.members)

		if tc.expect == "" {
			require.NoError(t, err)
		} else {
			testutil.RequireErrorContains(t, err, tc.expect)
		}
	}

	cases := map[string]testcase{
		"client in the wrong datacenter": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc2",
					name:   "node1",
					server: false,
					build:  "0.7.5",
				}),
			},
			expect: "wrong datacenter",
		},
		"server in the wrong datacenter": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc2",
					name:   "node1",
					server: true,
					build:  "0.7.5",
				}),
			},
			expect: "wrong datacenter",
		},
		"node ID conflict with delegate's ID but same node name with same casing": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node0",
					id:     thisNodeID,
					server: true,
					build:  "0.7.5",
				}),
			},
			expect: "",
		},
		"node ID conflict with delegate's ID but same node name with different casing": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "NoDe0",
					id:     thisNodeID,
					server: true,
					build:  "0.7.5",
				}),
			},
			expect: "",
		},
		"node ID conflict with delegate's ID": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node1",
					id:     thisNodeID,
					server: true,
					build:  "0.7.5",
				}),
			},
			expect: "with this agent's ID",
		},
		"cluster with existing conflicting node IDs": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node1",
					id:     "6185913b-98d7-4441-bd8f-f7f7d854a4af",
					server: true,
					build:  "0.8.5",
				}),
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node2",
					id:     "6185913b-98d7-4441-bd8f-f7f7d854a4af",
					server: true,
					build:  "0.9.0",
				}),
			},
			expect: "with member",
		},
		"cluster with existing conflicting node IDs, but version is old enough to skip the check": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node1",
					id:     "6185913b-98d7-4441-bd8f-f7f7d854a4af",
					server: true,
					build:  "0.8.5",
				}),
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node2",
					id:     "6185913b-98d7-4441-bd8f-f7f7d854a4af",
					server: true,
					build:  "0.8.4",
				}),
			},
			expect: "with member",
		},
		"good cluster": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node1",
					server: true,
					build:  "0.8.5",
				}),
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node2",
					server: true,
					build:  "0.8.5",
				}),
			},
			expect: "",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			run(t, tc)
		})
	}
}

func TestMerge_WAN(t *testing.T) {
	type testcase struct {
		members []*serf.Member
		expect  string
		setupFn func(t *testing.T, delegate *wanMergeDelegate)
	}

	run := func(t *testing.T, tc testcase) {
		delegate := &wanMergeDelegate{
			localDatacenter: "dc1",
		}
		if tc.setupFn != nil {
			tc.setupFn(t, delegate)
		}
		err := delegate.NotifyMerge(tc.members)
		if tc.expect == "" {
			require.NoError(t, err)
		} else {
			testutil.RequireErrorContains(t, err, tc.expect)
		}
	}

	cases := map[string]testcase{
		"not a server": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc2",
					name:   "node1",
					server: false,
					build:  "0.7.5",
				}),
			},
			expect: "not a server",
		},
		"good cluster": {
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc2",
					name:   "node1",
					server: true,
					build:  "0.7.5",
				}),
				makeTestNode(t, testMember{
					dc:     "dc3",
					name:   "node2",
					server: true,
					build:  "0.7.5",
				}),
			},
		},
		"federation disabled and local join allowed": {
			setupFn: func(t *testing.T, delegate *wanMergeDelegate) {
				delegate.SetWANFederationDisabled(true)
			},
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc1",
					name:   "node1",
					server: true,
					build:  "0.7.5",
				}),
			},
		},
		"federation disabled and remote join blocked": {
			setupFn: func(t *testing.T, delegate *wanMergeDelegate) {
				delegate.SetWANFederationDisabled(true)
			},
			members: []*serf.Member{
				makeTestNode(t, testMember{
					dc:     "dc2",
					name:   "node1",
					server: true,
					build:  "0.7.5",
				}),
			},
			expect: `WAN federation is disabled`,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			run(t, tc)
		})
	}
}

type testMember struct {
	dc        string
	name      string
	id        string
	server    bool
	build     string
	segment   string
	partition string
}

func (tm testMember) role() string {
	if tm.server {
		return "consul"
	}
	return "node"
}

func makeTestNode(t *testing.T, tm testMember) *serf.Member {
	if tm.id == "" {
		uuid, err := uuid.GenerateUUID()
		require.NoError(t, err)
		tm.id = uuid
	}
	m := &serf.Member{
		Name: tm.name,
		Tags: map[string]string{
			"role":    tm.role(),
			"dc":      tm.dc,
			"id":      tm.id,
			"port":    "8300",
			"segment": tm.segment,
			"build":   tm.build,
			"vsn":     "2",
			"vsn_max": "3",
			"vsn_min": "2",
			"fips":    version.GetFIPSInfo(),
		},
	}
	if tm.partition != "" {
		m.Tags["ap"] = tm.partition
	}
	return m
}
