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

package structs

import (
	"encoding/base64"
	"fmt"
	"testing"

	"github.com/hashicorp/nomad/acl"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestRemoveAllocs(t *testing.T) {
	ci.Parallel(t)

	l := []*Allocation{
		{ID: "foo"},
		{ID: "bar"},
		{ID: "baz"},
		{ID: "zip"},
	}

	out := RemoveAllocs(l, []*Allocation{l[1], l[3]})
	if len(out) != 2 {
		t.Fatalf("bad: %#v", out)
	}
	if out[0].ID != "foo" && out[1].ID != "baz" {
		t.Fatalf("bad: %#v", out)
	}
}

func TestFilterTerminalAllocs(t *testing.T) {
	ci.Parallel(t)

	l := []*Allocation{
		{
			ID:            "bar",
			Name:          "myname1",
			DesiredStatus: AllocDesiredStatusEvict,
		},
		{ID: "baz", DesiredStatus: AllocDesiredStatusStop},
		{
			ID:            "foo",
			DesiredStatus: AllocDesiredStatusRun,
			ClientStatus:  AllocClientStatusPending,
		},
		{
			ID:            "bam",
			Name:          "myname",
			DesiredStatus: AllocDesiredStatusRun,
			ClientStatus:  AllocClientStatusComplete,
			CreateIndex:   5,
		},
		{
			ID:            "lol",
			Name:          "myname",
			DesiredStatus: AllocDesiredStatusRun,
			ClientStatus:  AllocClientStatusComplete,
			CreateIndex:   2,
		},
	}

	out, terminalAllocs := FilterTerminalAllocs(l)
	if len(out) != 1 {
		t.Fatalf("bad: %#v", out)
	}
	if out[0].ID != "foo" {
		t.Fatalf("bad: %#v", out)
	}

	if len(terminalAllocs) != 3 {
		for _, o := range terminalAllocs {
			fmt.Printf("%#v \n", o)
		}

		t.Fatalf("bad: %#v", terminalAllocs)
	}

	if terminalAllocs["myname"].ID != "bam" {
		t.Fatalf("bad: %#v", terminalAllocs["myname"])
	}
}

// COMPAT(0.11): Remove in 0.11
func TestAllocsFit_PortsOvercommitted_Old(t *testing.T) {
	ci.Parallel(t)

	n := &Node{
		Resources: &Resources{
			Networks: []*NetworkResource{
				{
					Device: "eth0",
					CIDR:   "10.0.0.0/8",
					MBits:  100,
				},
			},
		},
	}

	a1 := &Allocation{
		Job: &Job{
			TaskGroups: []*TaskGroup{
				{
					Name:          "web",
					EphemeralDisk: DefaultEphemeralDisk(),
				},
			},
		},
		TaskResources: map[string]*Resources{
			"web": {
				Networks: []*NetworkResource{
					{
						Device:        "eth0",
						IP:            "10.0.0.1",
						MBits:         50,
						ReservedPorts: []Port{{"main", 8000, 80, ""}},
					},
				},
			},
		},
	}

	// Should fit one allocation
	fit, dim, _, err := AllocsFit(n, []*Allocation{a1}, nil, false)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if !fit {
		t.Fatalf("Bad: %s", dim)
	}

	// Should not fit second allocation
	fit, _, _, err = AllocsFit(n, []*Allocation{a1, a1}, nil, false)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if fit {
		t.Fatalf("Bad")
	}
}

// COMPAT(0.11): Remove in 0.11
func TestAllocsFit_Old(t *testing.T) {
	ci.Parallel(t)

	require := require.New(t)

	n := &Node{
		Resources: &Resources{
			CPU:      2000,
			MemoryMB: 2048,
			DiskMB:   10000,
			Networks: []*NetworkResource{
				{
					Device: "eth0",
					CIDR:   "10.0.0.0/8",
					MBits:  100,
				},
			},
		},
		Reserved: &Resources{
			CPU:      1000,
			MemoryMB: 1024,
			DiskMB:   5000,
			Networks: []*NetworkResource{
				{
					Device:        "eth0",
					IP:            "10.0.0.1",
					MBits:         50,
					ReservedPorts: []Port{{"main", 80, 0, ""}},
				},
			},
		},
	}

	a1 := &Allocation{
		Resources: &Resources{
			CPU:      1000,
			MemoryMB: 1024,
			DiskMB:   5000,
			Networks: []*NetworkResource{
				{
					Device:        "eth0",
					IP:            "10.0.0.1",
					MBits:         50,
					ReservedPorts: []Port{{"main", 8000, 80, ""}},
				},
			},
		},
	}

	// Should fit one allocation
	fit, _, used, err := AllocsFit(n, []*Allocation{a1}, nil, false)
	require.NoError(err)
	require.True(fit)
	require.EqualValues(1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(1024, used.Flattened.Memory.MemoryMB)

	// Should not fit second allocation
	fit, _, used, err = AllocsFit(n, []*Allocation{a1, a1}, nil, false)
	require.NoError(err)
	require.False(fit)
	require.EqualValues(2000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(2048, used.Flattened.Memory.MemoryMB)
}

// COMPAT(0.11): Remove in 0.11
func TestAllocsFit_TerminalAlloc_Old(t *testing.T) {
	ci.Parallel(t)

	require := require.New(t)

	n := &Node{
		Resources: &Resources{
			CPU:      2000,
			MemoryMB: 2048,
			DiskMB:   10000,
			Networks: []*NetworkResource{
				{
					Device: "eth0",
					CIDR:   "10.0.0.0/8",
					MBits:  100,
				},
			},
		},
		Reserved: &Resources{
			CPU:      1000,
			MemoryMB: 1024,
			DiskMB:   5000,
			Networks: []*NetworkResource{
				{
					Device:        "eth0",
					IP:            "10.0.0.1",
					MBits:         50,
					ReservedPorts: []Port{{"main", 80, 0, ""}},
				},
			},
		},
	}

	a1 := &Allocation{
		Resources: &Resources{
			CPU:      1000,
			MemoryMB: 1024,
			DiskMB:   5000,
			Networks: []*NetworkResource{
				{
					Device:        "eth0",
					IP:            "10.0.0.1",
					MBits:         50,
					ReservedPorts: []Port{{"main", 8000, 0, ""}},
				},
			},
		},
	}

	// Should fit one allocation
	fit, _, used, err := AllocsFit(n, []*Allocation{a1}, nil, false)
	require.NoError(err)
	require.True(fit)
	require.EqualValues(1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(1024, used.Flattened.Memory.MemoryMB)

	// Should fit second allocation since it is terminal
	a2 := a1.Copy()
	a2.DesiredStatus = AllocDesiredStatusStop
	a2.ClientStatus = AllocClientStatusComplete
	fit, _, used, err = AllocsFit(n, []*Allocation{a1, a2}, nil, false)
	require.NoError(err)
	require.True(fit)
	require.EqualValues(1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(1024, used.Flattened.Memory.MemoryMB)
}

func TestAllocsFit(t *testing.T) {
	ci.Parallel(t)

	require := require.New(t)

	n := &Node{
		NodeResources: &NodeResources{
			Cpu: NodeCpuResources{
				CpuShares:          2000,
				TotalCpuCores:      2,
				ReservableCpuCores: []uint16{0, 1},
			},
			Memory: NodeMemoryResources{
				MemoryMB: 2048,
			},
			Disk: NodeDiskResources{
				DiskMB: 10000,
			},
			Networks: []*NetworkResource{
				{
					Device: "eth0",
					CIDR:   "10.0.0.0/8",
					MBits:  100,
				},
			},
			NodeNetworks: []*NodeNetworkResource{
				{
					Mode:   "host",
					Device: "eth0",
					Addresses: []NodeNetworkAddress{
						{
							Address: "10.0.0.1",
						},
					},
				},
			},
		},
		ReservedResources: &NodeReservedResources{
			Cpu: NodeReservedCpuResources{
				CpuShares: 1000,
			},
			Memory: NodeReservedMemoryResources{
				MemoryMB: 1024,
			},
			Disk: NodeReservedDiskResources{
				DiskMB: 5000,
			},
			Networks: NodeReservedNetworkResources{
				ReservedHostPorts: "80",
			},
		},
	}

	a1 := &Allocation{
		AllocatedResources: &AllocatedResources{
			Tasks: map[string]*AllocatedTaskResources{
				"web": {
					Cpu: AllocatedCpuResources{
						CpuShares:     1000,
						ReservedCores: []uint16{},
					},
					Memory: AllocatedMemoryResources{
						MemoryMB: 1024,
					},
				},
			},
			Shared: AllocatedSharedResources{
				DiskMB: 5000,
				Networks: Networks{
					{
						Mode:          "host",
						IP:            "10.0.0.1",
						ReservedPorts: []Port{{"main", 8000, 0, ""}},
					},
				},
				Ports: AllocatedPorts{
					{
						Label:  "main",
						Value:  8000,
						HostIP: "10.0.0.1",
					},
				},
			},
		},
	}

	// Should fit one allocation
	fit, dim, used, err := AllocsFit(n, []*Allocation{a1}, nil, false)
	require.NoError(err)
	require.True(fit, "failed for dimension %q", dim)
	require.EqualValues(1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(1024, used.Flattened.Memory.MemoryMB)

	// Should not fit second allocation
	fit, _, used, err = AllocsFit(n, []*Allocation{a1, a1}, nil, false)
	require.NoError(err)
	require.False(fit)
	require.EqualValues(2000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(2048, used.Flattened.Memory.MemoryMB)

	a2 := &Allocation{
		AllocatedResources: &AllocatedResources{
			Tasks: map[string]*AllocatedTaskResources{
				"web": {
					Cpu: AllocatedCpuResources{
						CpuShares:     500,
						ReservedCores: []uint16{0},
					},
					Memory: AllocatedMemoryResources{
						MemoryMB: 512,
					},
				},
			},
			Shared: AllocatedSharedResources{
				DiskMB: 1000,
				Networks: Networks{
					{
						Mode: "host",
						IP:   "10.0.0.1",
					},
				},
			},
		},
	}

	// Should fit one allocation
	fit, dim, used, err = AllocsFit(n, []*Allocation{a2}, nil, false)
	require.NoError(err)
	require.True(fit, "failed for dimension %q", dim)
	require.EqualValues(500, used.Flattened.Cpu.CpuShares)
	require.EqualValues([]uint16{0}, used.Flattened.Cpu.ReservedCores)
	require.EqualValues(512, used.Flattened.Memory.MemoryMB)

	// Should not fit second allocation
	fit, dim, used, err = AllocsFit(n, []*Allocation{a2, a2}, nil, false)
	require.NoError(err)
	require.False(fit)
	require.EqualValues("cores", dim)
	require.EqualValues(1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues([]uint16{0}, used.Flattened.Cpu.ReservedCores)
	require.EqualValues(1024, used.Flattened.Memory.MemoryMB)
}

func TestAllocsFit_TerminalAlloc(t *testing.T) {
	ci.Parallel(t)

	require := require.New(t)

	n := &Node{
		NodeResources: &NodeResources{
			Cpu: NodeCpuResources{
				CpuShares: 2000,
			},
			Memory: NodeMemoryResources{
				MemoryMB: 2048,
			},
			Disk: NodeDiskResources{
				DiskMB: 10000,
			},
			Networks: []*NetworkResource{
				{
					Device: "eth0",
					CIDR:   "10.0.0.0/8",
					IP:     "10.0.0.1",
					MBits:  100,
				},
			},
		},
		ReservedResources: &NodeReservedResources{
			Cpu: NodeReservedCpuResources{
				CpuShares: 1000,
			},
			Memory: NodeReservedMemoryResources{
				MemoryMB: 1024,
			},
			Disk: NodeReservedDiskResources{
				DiskMB: 5000,
			},
			Networks: NodeReservedNetworkResources{
				ReservedHostPorts: "80",
			},
		},
	}

	a1 := &Allocation{
		AllocatedResources: &AllocatedResources{
			Tasks: map[string]*AllocatedTaskResources{
				"web": {
					Cpu: AllocatedCpuResources{
						CpuShares: 1000,
					},
					Memory: AllocatedMemoryResources{
						MemoryMB: 1024,
					},
					Networks: []*NetworkResource{
						{
							Device:        "eth0",
							IP:            "10.0.0.1",
							MBits:         50,
							ReservedPorts: []Port{{"main", 8000, 80, ""}},
						},
					},
				},
			},
			Shared: AllocatedSharedResources{
				DiskMB: 5000,
			},
		},
	}

	// Should fit one allocation
	fit, _, used, err := AllocsFit(n, []*Allocation{a1}, nil, false)
	require.NoError(err)
	require.True(fit)
	require.EqualValues(1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(1024, used.Flattened.Memory.MemoryMB)

	// Should fit second allocation since it is terminal
	a2 := a1.Copy()
	a2.DesiredStatus = AllocDesiredStatusStop
	a2.ClientStatus = AllocClientStatusComplete
	fit, dim, used, err := AllocsFit(n, []*Allocation{a1, a2}, nil, false)
	require.NoError(err)
	require.True(fit, dim)
	require.EqualValues(1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(1024, used.Flattened.Memory.MemoryMB)
}

// TestAllocsFit_ClientTerminalAlloc asserts that allocs which have a terminal
// ClientStatus *do not* have their resources counted as in-use.
func TestAllocsFit_ClientTerminalAlloc(t *testing.T) {
	ci.Parallel(t)

	n := &Node{
		ID: "test-node",
		NodeResources: &NodeResources{
			Cpu: NodeCpuResources{
				CpuShares: 2000,
			},
			Memory: NodeMemoryResources{
				MemoryMB: 2048,
			},
			Disk: NodeDiskResources{
				DiskMB: 10000,
			},
			Networks: []*NetworkResource{
				{
					Device: "eth0",
					CIDR:   "10.0.0.0/8",
					IP:     "10.0.0.1",
					MBits:  100,
				},
			},
		},
		ReservedResources: &NodeReservedResources{
			Cpu: NodeReservedCpuResources{
				CpuShares: 1000,
			},
			Memory: NodeReservedMemoryResources{
				MemoryMB: 1024,
			},
			Disk: NodeReservedDiskResources{
				DiskMB: 5000,
			},
			Networks: NodeReservedNetworkResources{
				ReservedHostPorts: "80",
			},
		},
	}

	liveAlloc := &Allocation{
		ID:            "test-alloc-live",
		ClientStatus:  AllocClientStatusPending,
		DesiredStatus: AllocDesiredStatusRun,
		AllocatedResources: &AllocatedResources{
			Tasks: map[string]*AllocatedTaskResources{
				"web": {
					Cpu: AllocatedCpuResources{
						CpuShares: 1000,
					},
					Memory: AllocatedMemoryResources{
						MemoryMB: 1024,
					},
					Networks: []*NetworkResource{
						{
							Device:        "eth0",
							IP:            "10.0.0.1",
							MBits:         50,
							ReservedPorts: []Port{{"main", 8000, 80, ""}},
						},
					},
				},
			},
			Shared: AllocatedSharedResources{
				DiskMB: 5000,
			},
		},
	}

	deadAlloc := liveAlloc.Copy()
	deadAlloc.ID = "test-alloc-dead"
	deadAlloc.ClientStatus = AllocClientStatusFailed
	deadAlloc.DesiredStatus = AllocDesiredStatusRun

	// *Should* fit both allocations since deadAlloc is not running on the
	// client
	fit, _, used, err := AllocsFit(n, []*Allocation{liveAlloc, deadAlloc}, nil, false)
	require.NoError(t, err)
	require.True(t, fit)
	require.EqualValues(t, 1000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(t, 1024, used.Flattened.Memory.MemoryMB)
}

// TestAllocsFit_ServerTerminalAlloc asserts that allocs which have a terminal
// DesiredStatus but are still running on clients *do* have their resources
// counted as in-use.
func TestAllocsFit_ServerTerminalAlloc(t *testing.T) {
	ci.Parallel(t)

	n := &Node{
		ID: "test-node",
		NodeResources: &NodeResources{
			Cpu: NodeCpuResources{
				CpuShares: 2000,
			},
			Memory: NodeMemoryResources{
				MemoryMB: 2048,
			},
			Disk: NodeDiskResources{
				DiskMB: 10000,
			},
			Networks: []*NetworkResource{
				{
					Device: "eth0",
					CIDR:   "10.0.0.0/8",
					IP:     "10.0.0.1",
					MBits:  100,
				},
			},
		},
		ReservedResources: &NodeReservedResources{
			Cpu: NodeReservedCpuResources{
				CpuShares: 1000,
			},
			Memory: NodeReservedMemoryResources{
				MemoryMB: 1024,
			},
			Disk: NodeReservedDiskResources{
				DiskMB: 5000,
			},
			Networks: NodeReservedNetworkResources{
				ReservedHostPorts: "80",
			},
		},
	}

	liveAlloc := &Allocation{
		ID:            "test-alloc-live",
		ClientStatus:  AllocClientStatusPending,
		DesiredStatus: AllocDesiredStatusRun,
		AllocatedResources: &AllocatedResources{
			Tasks: map[string]*AllocatedTaskResources{
				"web": {
					Cpu: AllocatedCpuResources{
						CpuShares: 1000,
					},
					Memory: AllocatedMemoryResources{
						MemoryMB: 1024,
					},
					Networks: []*NetworkResource{
						{
							Device:        "eth0",
							IP:            "10.0.0.1",
							MBits:         50,
							ReservedPorts: []Port{{"main", 8000, 80, ""}},
						},
					},
				},
			},
			Shared: AllocatedSharedResources{
				DiskMB: 5000,
			},
		},
	}

	deadAlloc := liveAlloc.Copy()
	deadAlloc.ID = "test-alloc-dead"
	deadAlloc.ClientStatus = AllocClientStatusRunning
	deadAlloc.DesiredStatus = AllocDesiredStatusStop

	// Should *not* fit both allocations since deadAlloc is still running
	fit, _, used, err := AllocsFit(n, []*Allocation{liveAlloc, deadAlloc}, nil, false)
	require.NoError(t, err)
	require.False(t, fit)
	require.EqualValues(t, 2000, used.Flattened.Cpu.CpuShares)
	require.EqualValues(t, 2048, used.Flattened.Memory.MemoryMB)
}

// Tests that AllocsFit detects device collisions
func TestAllocsFit_Devices(t *testing.T) {
	ci.Parallel(t)

	require := require.New(t)

	n := MockNvidiaNode()
	a1 := &Allocation{
		AllocatedResources: &AllocatedResources{
			Tasks: map[string]*AllocatedTaskResources{
				"web": {
					Cpu: AllocatedCpuResources{
						CpuShares: 1000,
					},
					Memory: AllocatedMemoryResources{
						MemoryMB: 1024,
					},
					Devices: []*AllocatedDeviceResource{
						{
							Type:      "gpu",
							Vendor:    "nvidia",
							Name:      "1080ti",
							DeviceIDs: []string{n.NodeResources.Devices[0].Instances[0].ID},
						},
					},
				},
			},
			Shared: AllocatedSharedResources{
				DiskMB: 5000,
			},
		},
	}
	a2 := a1.Copy()
	a2.AllocatedResources.Tasks["web"] = &AllocatedTaskResources{
		Cpu: AllocatedCpuResources{
			CpuShares: 1000,
		},
		Memory: AllocatedMemoryResources{
			MemoryMB: 1024,
		},
		Devices: []*AllocatedDeviceResource{
			{
				Type:      "gpu",
				Vendor:    "nvidia",
				Name:      "1080ti",
				DeviceIDs: []string{n.NodeResources.Devices[0].Instances[0].ID}, // Use the same ID
			},
		},
	}

	// Should fit one allocation
	fit, _, _, err := AllocsFit(n, []*Allocation{a1}, nil, true)
	require.NoError(err)
	require.True(fit)

	// Should not fit second allocation
	fit, msg, _, err := AllocsFit(n, []*Allocation{a1, a2}, nil, true)
	require.NoError(err)
	require.False(fit)
	require.Equal("device oversubscribed", msg)

	// Should not fit second allocation but won't detect since we disabled
	// devices
	fit, _, _, err = AllocsFit(n, []*Allocation{a1, a2}, nil, false)
	require.NoError(err)
	require.True(fit)
}

// TestAllocsFit_MemoryOversubscription asserts that only reserved memory is
// used for capacity
func TestAllocsFit_MemoryOversubscription(t *testing.T) {
	ci.Parallel(t)

	n := &Node{
		NodeResources: &NodeResources{
			Cpu: NodeCpuResources{
				CpuShares: 2000,
			},
			Memory: NodeMemoryResources{
				MemoryMB: 2048,
			},
		},
	}

	a1 := &Allocation{
		AllocatedResources: &AllocatedResources{
			Tasks: map[string]*AllocatedTaskResources{
				"web": {
					Cpu: AllocatedCpuResources{
						CpuShares: 100,
					},
					Memory: AllocatedMemoryResources{
						MemoryMB:    1000,
						MemoryMaxMB: 4000,
					},
				},
			},
		},
	}

	// Should fit one allocation
	fit, _, used, err := AllocsFit(n, []*Allocation{a1}, nil, false)
	require.NoError(t, err)
	require.True(t, fit)
	require.EqualValues(t, 100, used.Flattened.Cpu.CpuShares)
	require.EqualValues(t, 1000, used.Flattened.Memory.MemoryMB)
	require.EqualValues(t, 4000, used.Flattened.Memory.MemoryMaxMB)

	// Should fit second allocation
	fit, _, used, err = AllocsFit(n, []*Allocation{a1, a1}, nil, false)
	require.NoError(t, err)
	require.True(t, fit)
	require.EqualValues(t, 200, used.Flattened.Cpu.CpuShares)
	require.EqualValues(t, 2000, used.Flattened.Memory.MemoryMB)
	require.EqualValues(t, 8000, used.Flattened.Memory.MemoryMaxMB)

	// Should not fit a third allocation
	fit, _, used, err = AllocsFit(n, []*Allocation{a1, a1, a1}, nil, false)
	require.NoError(t, err)
	require.False(t, fit)
	require.EqualValues(t, 300, used.Flattened.Cpu.CpuShares)
	require.EqualValues(t, 3000, used.Flattened.Memory.MemoryMB)
	require.EqualValues(t, 12000, used.Flattened.Memory.MemoryMaxMB)
}

// COMPAT(0.11): Remove in 0.11
func TestScoreFitBinPack_Old(t *testing.T) {
	ci.Parallel(t)

	node := &Node{}
	node.Resources = &Resources{
		CPU:      4096,
		MemoryMB: 8192,
	}
	node.Reserved = &Resources{
		CPU:      2048,
		MemoryMB: 4096,
	}

	// Test a perfect fit
	util := &ComparableResources{
		Flattened: AllocatedTaskResources{
			Cpu: AllocatedCpuResources{
				CpuShares: 2048,
			},
			Memory: AllocatedMemoryResources{
				MemoryMB: 4096,
			},
		},
	}
	score := ScoreFitBinPack(node, util)
	if score != 18.0 {
		t.Fatalf("bad: %v", score)
	}

	// Test the worst fit
	util = &ComparableResources{
		Flattened: AllocatedTaskResources{
			Cpu: AllocatedCpuResources{
				CpuShares: 0,
			},
			Memory: AllocatedMemoryResources{
				MemoryMB: 0,
			},
		},
	}
	score = ScoreFitBinPack(node, util)
	if score != 0.0 {
		t.Fatalf("bad: %v", score)
	}

	// Test a mid-case scenario
	util = &ComparableResources{
		Flattened: AllocatedTaskResources{
			Cpu: AllocatedCpuResources{
				CpuShares: 1024,
			},
			Memory: AllocatedMemoryResources{
				MemoryMB: 2048,
			},
		},
	}
	score = ScoreFitBinPack(node, util)
	if score < 10.0 || score > 16.0 {
		t.Fatalf("bad: %v", score)
	}
}

func TestScoreFitBinPack(t *testing.T) {
	ci.Parallel(t)

	node := &Node{}
	node.NodeResources = &NodeResources{
		Cpu: NodeCpuResources{
			CpuShares: 4096,
		},
		Memory: NodeMemoryResources{
			MemoryMB: 8192,
		},
	}
	node.ReservedResources = &NodeReservedResources{
		Cpu: NodeReservedCpuResources{
			CpuShares: 2048,
		},
		Memory: NodeReservedMemoryResources{
			MemoryMB: 4096,
		},
	}

	cases := []struct {
		name         string
		flattened    AllocatedTaskResources
		binPackScore float64
		spreadScore  float64
	}{
		{
			name: "almost filled node, but with just enough hole",
			flattened: AllocatedTaskResources{
				Cpu:    AllocatedCpuResources{CpuShares: 2048},
				Memory: AllocatedMemoryResources{MemoryMB: 4096},
			},
			binPackScore: 18,
			spreadScore:  0,
		},
		{
			name: "unutilized node",
			flattened: AllocatedTaskResources{
				Cpu:    AllocatedCpuResources{CpuShares: 0},
				Memory: AllocatedMemoryResources{MemoryMB: 0},
			},
			binPackScore: 0,
			spreadScore:  18,
		},
		{
			name: "mid-case scnario",
			flattened: AllocatedTaskResources{
				Cpu:    AllocatedCpuResources{CpuShares: 1024},
				Memory: AllocatedMemoryResources{MemoryMB: 2048},
			},
			binPackScore: 13.675,
			spreadScore:  4.325,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			util := &ComparableResources{Flattened: c.flattened}

			binPackScore := ScoreFitBinPack(node, util)
			require.InDelta(t, c.binPackScore, binPackScore, 0.001, "binpack score")

			spreadScore := ScoreFitSpread(node, util)
			require.InDelta(t, c.spreadScore, spreadScore, 0.001, "spread score")

			require.InDelta(t, 18, binPackScore+spreadScore, 0.001, "score sum")
		})
	}
}

func TestACLPolicyListHash(t *testing.T) {
	ci.Parallel(t)

	h1 := ACLPolicyListHash(nil)
	assert.NotEqual(t, "", h1)

	p1 := &ACLPolicy{
		Name:        fmt.Sprintf("policy-%s", uuid.Generate()),
		Description: "Super cool policy!",
		Rules: `
		namespace "default" {
			policy = "write"
		}
		node {
			policy = "read"
		}
		agent {
			policy = "read"
		}
		`,
		CreateIndex: 10,
		ModifyIndex: 20,
	}

	h2 := ACLPolicyListHash([]*ACLPolicy{p1})
	assert.NotEqual(t, "", h2)
	assert.NotEqual(t, h1, h2)

	// Create P2 as copy of P1 with new name
	p2 := &ACLPolicy{}
	*p2 = *p1
	p2.Name = fmt.Sprintf("policy-%s", uuid.Generate())

	h3 := ACLPolicyListHash([]*ACLPolicy{p1, p2})
	assert.NotEqual(t, "", h3)
	assert.NotEqual(t, h2, h3)

	h4 := ACLPolicyListHash([]*ACLPolicy{p2})
	assert.NotEqual(t, "", h4)
	assert.NotEqual(t, h3, h4)

	// ModifyIndex should change the hash
	p2.ModifyIndex++
	h5 := ACLPolicyListHash([]*ACLPolicy{p2})
	assert.NotEqual(t, "", h5)
	assert.NotEqual(t, h4, h5)
}

func TestCompileACLObject(t *testing.T) {
	ci.Parallel(t)

	p1 := &ACLPolicy{
		Name:        fmt.Sprintf("policy-%s", uuid.Generate()),
		Description: "Super cool policy!",
		Rules: `
		namespace "default" {
			policy = "write"
		}
		node {
			policy = "read"
		}
		agent {
			policy = "read"
		}
		`,
		CreateIndex: 10,
		ModifyIndex: 20,
	}

	// Create P2 as copy of P1 with new name
	p2 := &ACLPolicy{}
	*p2 = *p1
	p2.Name = fmt.Sprintf("policy-%s", uuid.Generate())

	// Create a small cache
	cache := NewACLCache[*acl.ACL](10)

	// Test compilation
	aclObj, err := CompileACLObject(cache, []*ACLPolicy{p1})
	assert.Nil(t, err)
	assert.NotNil(t, aclObj)

	// Should get the same object
	aclObj2, err := CompileACLObject(cache, []*ACLPolicy{p1})
	assert.Nil(t, err)
	if aclObj != aclObj2 {
		t.Fatalf("expected the same object")
	}

	// Should get another object
	aclObj3, err := CompileACLObject(cache, []*ACLPolicy{p1, p2})
	assert.Nil(t, err)
	assert.NotNil(t, aclObj3)
	if aclObj == aclObj3 {
		t.Fatalf("unexpected same object")
	}

	// Should be order independent
	aclObj4, err := CompileACLObject(cache, []*ACLPolicy{p2, p1})
	assert.Nil(t, err)
	assert.NotNil(t, aclObj4)
	if aclObj3 != aclObj4 {
		t.Fatalf("expected same object")
	}
}

// TestGenerateMigrateToken asserts the migrate token is valid for use in HTTP
// headers and CompareMigrateToken works as expected.
func TestGenerateMigrateToken(t *testing.T) {
	ci.Parallel(t)

	assert := assert.New(t)
	allocID := uuid.Generate()
	nodeSecret := uuid.Generate()
	token, err := GenerateMigrateToken(allocID, nodeSecret)
	assert.Nil(err)
	_, err = base64.URLEncoding.DecodeString(token)
	assert.Nil(err)

	assert.True(CompareMigrateToken(allocID, nodeSecret, token))
	assert.False(CompareMigrateToken("x", nodeSecret, token))
	assert.False(CompareMigrateToken(allocID, "x", token))
	assert.False(CompareMigrateToken(allocID, nodeSecret, "x"))

	token2, err := GenerateMigrateToken("x", nodeSecret)
	assert.Nil(err)
	assert.False(CompareMigrateToken(allocID, nodeSecret, token2))
	assert.True(CompareMigrateToken("x", nodeSecret, token2))
}

func TestVaultPoliciesSet(t *testing.T) {
	input := map[string]map[string]*Vault{
		"tg1": {
			"task1": {
				Policies: []string{"policy1-1"},
			},
			"task2": {
				Policies: []string{"policy1-2"},
			},
		},
		"tg2": {
			"task1": {
				Policies: []string{"policy2"},
			},
			"task2": {
				Policies: []string{"policy2"},
			},
		},
		"tg3": {
			"task1": {
				Policies: []string{"policy3-1"},
			},
		},
		"tg4": {
			"task1": nil,
		},
		"tg5": {
			"task1": {
				Policies: []string{"policy2"},
			},
		},
		"tg6": {
			"task1": {},
		},
		"tg7": {
			"task1": {
				Policies: []string{"policy7", "policy7"},
			},
		},
		"tg8": {
			"task1": {
				Policies: []string{"policy8-1-1", "policy8-1-2"},
			},
		},
	}
	expected := []string{
		"policy1-1",
		"policy1-2",
		"policy2",
		"policy3-1",
		"policy7",
		"policy8-1-1",
		"policy8-1-2",
	}
	got := VaultPoliciesSet(input)
	require.ElementsMatch(t, expected, got)
}

func TestVaultNamespaceSet(t *testing.T) {
	input := map[string]map[string]*Vault{
		"tg1": {
			"task1": {
				Namespace: "ns1-1",
			},
			"task2": {
				Namespace: "ns1-2",
			},
		},
		"tg2": {
			"task1": {
				Namespace: "ns2",
			},
			"task2": {
				Namespace: "ns2",
			},
		},
		"tg3": {
			"task1": {
				Namespace: "ns3-1",
			},
		},
		"tg4": {
			"task1": nil,
		},
		"tg5": {
			"task1": {
				Namespace: "ns2",
			},
		},
		"tg6": {
			"task1": {},
		},
	}
	expected := []string{
		"ns1-1",
		"ns1-2",
		"ns2",
		"ns3-1",
	}
	got := VaultNamespaceSet(input)
	require.ElementsMatch(t, expected, got)
}

// TestParsePortRanges asserts ParsePortRanges errors on invalid port ranges.
func TestParsePortRanges(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		name string
		spec string
		err  string
	}{
		{
			name: "UnmatchedDash",
			spec: "-1",
			err:  `strconv.ParseUint: parsing "": invalid syntax`,
		},
		{
			name: "Zero",
			spec: "0",
			err:  "port must be > 0",
		},
		{
			name: "TooBig",
			spec: fmt.Sprintf("1-%d", MaxValidPort+1),
			err:  "port must be < 65536 but found 65537",
		},
		{
			name: "WayTooBig",           // would OOM if not caught early enough
			spec: "9223372036854775807", // (2**63)-1
			err:  "port must be < 65536 but found 9223372036854775807",
		},
	}

	for i := range cases {
		tc := cases[i]
		t.Run(tc.name, func(t *testing.T) {
			results, err := ParsePortRanges(tc.spec)
			require.Nil(t, results)
			require.EqualError(t, err, tc.err)
		})
	}
}
