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

package helper

import (
	"fmt"
	"reflect"
	"sort"
	"testing"

	"github.com/hashicorp/go-set"
	"github.com/shoenig/test/must"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"
)

func TestIsSubset(t *testing.T) {
	l := []string{"a", "b", "c"}
	s := []string{"d"}

	sub, offending := IsSubset(l, l[:1])
	must.True(t, sub)
	must.SliceEmpty(t, offending)

	sub, offending = IsSubset(l, s)
	must.False(t, sub)
	must.Eq(t, []string{"d"}, offending)
}

func TestIsDisjoint(t *testing.T) {
	t.Run("yes", func(t *testing.T) {
		a := []string{"a", "b", "c"}
		b := []string{"d", "f"}
		dis, offending := IsDisjoint(a, b)
		must.True(t, dis)
		must.SliceEmpty(t, offending)
	})

	t.Run("no", func(t *testing.T) {
		a := []string{"a", "b", "c", "d", "e"}
		b := []string{"b", "c", "f", "g"}
		dis, offending := IsDisjoint(a, b)
		must.False(t, dis)
		must.True(t, set.From(offending).EqualSlice(offending))
	})
}

func TestStringHasPrefixInSlice(t *testing.T) {
	prefixes := []string{"a", "b", "c", "definitely", "most definitely"}
	// The following strings all start with at least one prefix in the slice above
	require.True(t, StringHasPrefixInSlice("alpha", prefixes))
	require.True(t, StringHasPrefixInSlice("bravo", prefixes))
	require.True(t, StringHasPrefixInSlice("charlie", prefixes))
	require.True(t, StringHasPrefixInSlice("definitely", prefixes))
	require.True(t, StringHasPrefixInSlice("most definitely", prefixes))

	require.False(t, StringHasPrefixInSlice("mos", prefixes))
	require.False(t, StringHasPrefixInSlice("def", prefixes))
	require.False(t, StringHasPrefixInSlice("delta", prefixes))

}

func TestCompareSliceSetString(t *testing.T) {
	cases := []struct {
		A      []string
		B      []string
		Result bool
	}{
		{
			A:      []string{},
			B:      []string{},
			Result: true,
		},
		{
			A:      []string{},
			B:      []string{"a"},
			Result: false,
		},
		{
			A:      []string{"a"},
			B:      []string{"a"},
			Result: true,
		},
		{
			A:      []string{"a"},
			B:      []string{"b"},
			Result: false,
		},
		{
			A:      []string{"a", "b"},
			B:      []string{"b"},
			Result: false,
		},
		{
			A:      []string{"a", "b"},
			B:      []string{"a"},
			Result: false,
		},
		{
			A:      []string{"a", "b"},
			B:      []string{"a", "b"},
			Result: true,
		},
		{
			A:      []string{"a", "b"},
			B:      []string{"b", "a"},
			Result: true,
		},
	}

	for i, tc := range cases {
		tc := tc
		t.Run(fmt.Sprintf("case-%da", i), func(t *testing.T) {
			if res := SliceSetEq(tc.A, tc.B); res != tc.Result {
				t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
					tc.Result, tc.A, tc.B, res,
				)
			}
		})

		// Function is commutative so compare B and A
		t.Run(fmt.Sprintf("case-%db", i), func(t *testing.T) {
			if res := SliceSetEq(tc.B, tc.A); res != tc.Result {
				t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
					tc.Result, tc.B, tc.A, res,
				)
			}
		})
	}
}

func TestUniqueMapSliceValues(t *testing.T) {
	m := map[string][]string{
		"foo": {"1", "2"},
		"bar": {"3"},
		"baz": nil,
	}

	act := UniqueMapSliceValues(m)
	exp := []string{"1", "2", "3"}
	sort.Strings(act)
	must.Eq(t, exp, act)
}

func TestCopyMapStringSliceString(t *testing.T) {
	m := map[string][]string{
		"x": {"a", "b", "c"},
		"y": {"1", "2", "3"},
		"z": nil,
	}

	c := CopyMapOfSlice(m)
	if !reflect.DeepEqual(c, m) {
		t.Fatalf("%#v != %#v", m, c)
	}

	c["x"][1] = "---"
	if reflect.DeepEqual(c, m) {
		t.Fatalf("Shared slices: %#v == %#v", m["x"], c["x"])
	}
}

func TestMergeMapStringString(t *testing.T) {
	type testCase struct {
		map1     map[string]string
		map2     map[string]string
		expected map[string]string
	}

	cases := []testCase{
		{map[string]string{"foo": "bar"}, map[string]string{"baz": "qux"}, map[string]string{"foo": "bar", "baz": "qux"}},
		{map[string]string{"foo": "bar"}, nil, map[string]string{"foo": "bar"}},
		{nil, map[string]string{"baz": "qux"}, map[string]string{"baz": "qux"}},
		{nil, nil, map[string]string{}},
	}

	for _, c := range cases {
		if output := MergeMapStringString(c.map1, c.map2); !maps.Equal(output, c.expected) {
			t.Errorf("MergeMapStringString(%q, %q) -> %q != %q", c.map1, c.map2, output, c.expected)
		}
	}
}

func TestCleanEnvVar(t *testing.T) {
	type testCase struct {
		input    string
		expected string
	}
	cases := []testCase{
		{"asdf", "asdf"},
		{"ASDF", "ASDF"},
		{"0sdf", "_sdf"},
		{"asd0", "asd0"},
		{"_asd", "_asd"},
		{"-asd", "_asd"},
		{"asd.fgh", "asd.fgh"},
		{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A______________________________Z"},
		{"A\U0001f4a9Z", "A____Z"},
	}
	for _, c := range cases {
		if output := CleanEnvVar(c.input, '_'); output != c.expected {
			t.Errorf("CleanEnvVar(%q, '_') -> %q != %q", c.input, output, c.expected)
		}
	}
}

func BenchmarkCleanEnvVar(b *testing.B) {
	in := "NOMAD_ADDR_redis-cache"
	replacement := byte('_')
	b.SetBytes(int64(len(in)))
	b.ReportAllocs()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CleanEnvVar(in, replacement)
	}
}

type testCase struct {
	input    string
	expected string
}

func commonCleanFilenameCases() (cases []testCase) {
	// Common set of test cases for all 3 TestCleanFilenameX functions
	cases = []testCase{
		{"asdf", "asdf"},
		{"ASDF", "ASDF"},
		{"0sdf", "0sdf"},
		{"asd0", "asd0"},
		{"_asd", "_asd"},
		{"-asd", "-asd"},
		{"asd.fgh", "asd.fgh"},
		{"Linux/Forbidden", "Linux_Forbidden"},
		{"Windows<>:\"/\\|?*Forbidden", "Windows_________Forbidden"},
		{`Windows<>:"/\|?*Forbidden_StringLiteral`, "Windows_________Forbidden_StringLiteral"},
	}
	return cases
}

func TestCleanFilename(t *testing.T) {
	cases := append(
		[]testCase{
			{"A\U0001f4a9Z", "A💩Z"}, // CleanFilename allows unicode
			{"A💩Z", "A💩Z"},
			{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A~!@#$%^&_()_+-={}[]__;_'__,___Z"},
		}, commonCleanFilenameCases()...)

	for i, c := range cases {
		t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
			output := CleanFilename(c.input, "_")
			failMsg := fmt.Sprintf("CleanFilename(%q, '_') -> %q != %q", c.input, output, c.expected)
			require.Equal(t, c.expected, output, failMsg)
		})
	}
}

func TestCleanFilenameASCIIOnly(t *testing.T) {
	ASCIIOnlyCases := append(
		[]testCase{
			{"A\U0001f4a9Z", "A_Z"}, // CleanFilenameASCIIOnly does not allow unicode
			{"A💩Z", "A_Z"},
			{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A~!@#$%^&_()_+-={}[]__;_'__,___Z"},
		}, commonCleanFilenameCases()...)

	for i, c := range ASCIIOnlyCases {
		t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
			output := CleanFilenameASCIIOnly(c.input, "_")
			failMsg := fmt.Sprintf("CleanFilenameASCIIOnly(%q, '_') -> %q != %q", c.input, output, c.expected)
			require.Equal(t, c.expected, output, failMsg)
		})
	}
}

func TestCleanFilenameStrict(t *testing.T) {
	strictCases := append(
		[]testCase{
			{"A\U0001f4a9Z", "A💩Z"}, // CleanFilenameStrict allows unicode
			{"A💩Z", "A💩Z"},
			{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A_!___%^______-_{}_____________Z"},
		}, commonCleanFilenameCases()...)

	for i, c := range strictCases {
		t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
			output := CleanFilenameStrict(c.input, "_")
			failMsg := fmt.Sprintf("CleanFilenameStrict(%q, '_') -> %q != %q", c.input, output, c.expected)
			require.Equal(t, c.expected, output, failMsg)
		})
	}
}

func TestCheckNamespaceScope(t *testing.T) {
	cases := []struct {
		desc      string
		provided  string
		requested []string
		offending []string
	}{
		{
			desc:      "root ns requesting namespace",
			provided:  "",
			requested: []string{"engineering"},
		},
		{
			desc:      "matching parent ns with child",
			provided:  "engineering",
			requested: []string{"engineering", "engineering/sub-team"},
		},
		{
			desc:      "mismatch ns",
			provided:  "engineering",
			requested: []string{"finance", "engineering/sub-team", "eng"},
			offending: []string{"finance", "eng"},
		},
		{
			desc:      "mismatch child",
			provided:  "engineering/sub-team",
			requested: []string{"engineering/new-team", "engineering/sub-team", "engineering/sub-team/child"},
			offending: []string{"engineering/new-team"},
		},
		{
			desc:      "matching prefix",
			provided:  "engineering",
			requested: []string{"engineering/new-team", "engineering/new-team/sub-team"},
		},
	}

	for _, tc := range cases {
		t.Run(tc.desc, func(t *testing.T) {
			offending := CheckNamespaceScope(tc.provided, tc.requested)
			require.Equal(t, offending, tc.offending)
		})
	}
}

func TestTimer_NewSafeTimer(t *testing.T) {
	t.Run("zero", func(t *testing.T) {
		timer, stop := NewSafeTimer(0)
		defer stop()
		<-timer.C
	})

	t.Run("positive", func(t *testing.T) {
		timer, stop := NewSafeTimer(1)
		defer stop()
		<-timer.C
	})
}

func TestTimer_NewStoppedTimer(t *testing.T) {
	timer, stop := NewStoppedTimer()
	defer stop()

	select {
	case <-timer.C:
		must.Unreachable(t)
	default:
	}
}

func Test_ConvertSlice(t *testing.T) {
	t.Run("string wrapper", func(t *testing.T) {

		type wrapper struct{ id string }
		input := []string{"foo", "bar", "bad", "had"}
		cFn := func(id string) *wrapper { return &wrapper{id: id} }

		expectedOutput := []*wrapper{{id: "foo"}, {id: "bar"}, {id: "bad"}, {id: "had"}}
		actualOutput := ConvertSlice(input, cFn)
		require.ElementsMatch(t, expectedOutput, actualOutput)
	})

	t.Run("int wrapper", func(t *testing.T) {

		type wrapper struct{ id int }
		input := []int{10, 13, 1987, 2020}
		cFn := func(id int) *wrapper { return &wrapper{id: id} }

		expectedOutput := []*wrapper{{id: 10}, {id: 13}, {id: 1987}, {id: 2020}}
		actualOutput := ConvertSlice(input, cFn)
		require.ElementsMatch(t, expectedOutput, actualOutput)

	})
}

func Test_IsMethodHTTP(t *testing.T) {
	t.Run("is method", func(t *testing.T) {
		cases := []string{
			"GET", "Get", "get",
			"HEAD", "Head", "head",
			"POST", "Post", "post",
			"PUT", "Put", "put",
			"PATCH", "Patch", "patch",
			"DELETE", "Delete", "delete",
			"CONNECT", "Connect", "connect",
			"OPTIONS", "Options", "options",
			"TRACE", "Trace", "trace",
		}
		for _, tc := range cases {
			result := IsMethodHTTP(tc)
			must.True(t, result)
		}
	})

	t.Run("is not method", func(t *testing.T) {
		not := []string{"GETTER", "!GET", ""}
		for _, tc := range not {
			result := IsMethodHTTP(tc)
			must.False(t, result)
		}
	})
}

type employee struct {
	id   int
	name string
}

func (e *employee) Equal(o *employee) bool {
	return e.id == o.id // name can be different
}

func Test_ElementsEquals(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		a := []*employee(nil)
		var b []*employee
		must.True(t, ElementsEqual(a, b))
		must.True(t, ElementsEqual(b, a))
	})

	t.Run("different sizes", func(t *testing.T) {
		a := []*employee{{1, "mitchell"}, {2, "armon"}, {3, "jack"}}
		b := []*employee{{1, "mitchell"}, {2, "armon"}}
		must.False(t, ElementsEqual(a, b))
		must.False(t, ElementsEqual(b, a))
	})

	t.Run("equal", func(t *testing.T) {
		a := []*employee{{1, "mitchell"}, {2, "armon"}, {3, "jack"}}
		b := []*employee{{1, "M.H."}, {2, "A.D."}, {3, "J.P."}}
		must.True(t, ElementsEqual(a, b))
		must.True(t, ElementsEqual(b, a))
	})

	t.Run("different", func(t *testing.T) {
		a := []*employee{{1, "mitchell"}, {2, "armon"}, {3, "jack"}}
		b := []*employee{{0, "mitchell."}, {2, "armon"}, {3, "jack"}}
		must.False(t, ElementsEqual(a, b))
		must.False(t, ElementsEqual(b, a))
	})
}

func Test_SliceSetEq(t *testing.T) {
	t.Run("empty", func(t *testing.T) {
		a := make([]int, 0)
		b := make([]int, 0)
		must.True(t, SliceSetEq(a, b))
	})

	t.Run("subset small", func(t *testing.T) {
		a := []int{1, 2, 3, 4, 5}
		b := []int{1, 2, 3}
		must.False(t, SliceSetEq(a, b))
		must.False(t, SliceSetEq(b, a))
	})

	t.Run("subset large", func(t *testing.T) {
		a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
		b := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
		must.False(t, SliceSetEq(a, b))
		must.False(t, SliceSetEq(b, a))
	})

	t.Run("same small", func(t *testing.T) {
		a := []int{1, 2, 3, 4, 5}
		b := []int{1, 2, 3, 4, 5}
		must.True(t, SliceSetEq(a, b))
	})

	t.Run("same large", func(t *testing.T) {
		a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
		b := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
		must.True(t, SliceSetEq(a, b))
	})
}
