package util_test

import (
	"slices"
	"strconv"
	"testing"

	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/stretchr/testify/assert"
)

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

	realWorldErrorMessages := []string{
		"Failed to load state: RequestError: send request failed\ncaused by: Get https://<BUCKET_NAME>.us-west-2.amazonaws.com/?prefix=env%3A%2F: dial tcp 54.231.176.160:443: i/o timeout",
		"aws_cloudwatch_metric_alarm.asg_high_memory_utilization: Creating metric alarm failed: ValidationError: A separate request to update this alarm is in progress. status code: 400, request id: 94309fbd-7e09-11e8-a5f8-1de9e697c6bf",
		"Error configuring the backend \"s3\": RequestError: send request failed\ncaused by: Post https://sts.amazonaws.com/: net/http: TLS handshake timeout",
	}

	testCases := []struct {
		element  string
		list     []string
		expected bool
	}{
		{list: nil, element: "", expected: false},
		{list: []string{}, element: "", expected: false},
		{list: []string{}, element: "foo", expected: false},
		{list: []string{"foo"}, element: "kafoot", expected: true},
		{list: []string{"bar", "foo", ".*Failed to load backend.*TLS handshake timeout.*"}, element: "Failed to load backend: Error...:...  TLS handshake timeout", expected: true},
		{list: []string{"bar", "foo", ".*Failed to load backend.*TLS handshake timeout.*"}, element: "Failed to load backend: Error...:...  TLxS handshake timeout", expected: false},
		{list: []string{"(?s).*Failed to load state.*dial tcp.*timeout.*"}, element: realWorldErrorMessages[0], expected: true},
		{list: []string{"(?s).*Creating metric alarm failed.*request to update this alarm is in progress.*"}, element: realWorldErrorMessages[1], expected: true},
		{list: []string{"(?s).*Error configuring the backend.*TLS handshake timeout.*"}, element: realWorldErrorMessages[2], expected: true},
	}

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := util.MatchesAny(tc.list, tc.element)
			assert.Equal(t, tc.expected, actual, "For list %v and element %s", tc.list, tc.element)
		})
	}
}

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

	testCases := []struct {
		element  string
		list     []string
		expected bool
	}{
		{list: []string{}, element: "", expected: false},
		{list: []string{}, element: "foo", expected: false},
		{list: []string{"foo"}, element: "foo", expected: true},
		{list: []string{"bar", "foo", "baz"}, element: "foo", expected: true},
		{list: []string{"bar", "foo", "baz"}, element: "nope", expected: false},
		{list: []string{"bar", "foo", "baz"}, element: "", expected: false},
	}

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := slices.Contains(tc.list, tc.element)
			assert.Equal(t, tc.expected, actual, "For list %v and element %s", tc.list, tc.element)
		})
	}
}

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

	testCases := []struct {
		a        []string
		b        []string
		expected bool
	}{
		{[]string{""}, []string{}, false},
		{[]string{"foo"}, []string{"bar"}, false},
		{[]string{"foo", "bar"}, []string{"bar"}, false},
		{[]string{"foo"}, []string{"foo", "bar"}, false},
		{[]string{"foo", "bar"}, []string{"bar", "foo"}, false},

		{[]string{}, []string{}, true},
		{[]string{""}, []string{""}, true},
		{[]string{"foo", "bar"}, []string{"foo", "bar"}, true},
	}
	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := slices.Equal(tc.a, tc.b)
			assert.Equal(t, tc.expected, actual, "For list %v and list %v", tc.a, tc.b)
		})
	}
}

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

	testCases := []struct {
		list     []string
		sublist  []string
		expected bool
	}{
		{[]string{}, []string{}, false},
		{[]string{}, []string{"foo"}, false},
		{[]string{"foo"}, []string{}, false},
		{[]string{"foo"}, []string{"bar"}, false},
		{[]string{"foo"}, []string{"foo", "bar"}, false},
		{[]string{"bar", "foo"}, []string{"foo", "bar"}, false},
		{[]string{"bar", "foo", "gee"}, []string{"foo", "bar"}, false},
		{[]string{"foo", "foo", "gee"}, []string{"foo", "bar"}, false},
		{[]string{"zim", "gee", "foo", "foo", "foo"}, []string{"foo", "foo", "bar", "bar"}, false},

		{[]string{""}, []string{""}, true},
		{[]string{"foo"}, []string{"foo"}, true},
		{[]string{"foo", "bar"}, []string{"foo"}, true},
		{[]string{"bar", "foo"}, []string{"foo"}, true},
		{[]string{"foo", "bar", "gee"}, []string{"foo", "bar"}, true},
		{[]string{"zim", "foo", "bar", "gee"}, []string{"foo", "bar"}, true},
		{[]string{"foo", "foo", "bar", "gee"}, []string{"foo", "bar"}, true},
		{[]string{"zim", "gee", "foo", "bar"}, []string{"foo", "bar"}, true},
		{[]string{"foo", "foo", "foo", "bar"}, []string{"foo", "foo"}, true},
		{[]string{"bar", "foo", "foo", "foo"}, []string{"foo", "foo"}, true},
		{[]string{"zim", "gee", "foo", "bar"}, []string{"gee", "foo", "bar"}, true},
	}

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := util.ListContainsSublist(tc.list, tc.sublist)
			assert.Equal(t, tc.expected, actual, "For list %v and sublist %v", tc.list, tc.sublist)
		})
	}
}

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

	testCases := []struct {
		list     []string
		prefix   []string
		expected bool
	}{
		{[]string{}, []string{}, false},
		{[]string{""}, []string{}, false},
		{[]string{"foo"}, []string{"bar"}, false},
		{[]string{"foo", "bar"}, []string{"bar"}, false},
		{[]string{"foo"}, []string{"foo", "bar"}, false},
		{[]string{"foo", "bar", "foo"}, []string{"bar", "foo"}, false},

		{[]string{""}, []string{""}, true},
		{[]string{"", "foo"}, []string{""}, true},
		{[]string{"foo", "bar"}, []string{"foo"}, true},
		{[]string{"foo", "bar"}, []string{"foo", "bar"}, true},
		{[]string{"foo", "bar", "biz"}, []string{"foo", "bar"}, true},
	}
	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := util.ListHasPrefix(tc.list, tc.prefix)
			assert.Equal(t, tc.expected, actual, "For list %v and prefix %v", tc.list, tc.prefix)
		})
	}
}

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

	testCases := []struct {
		list     []string
		expected []string
	}{
		{[]string{}, []string{}},
		{[]string{"foo"}, []string{"foo"}},
		{[]string{"foo", "bar"}, []string{"bar", "foo"}}, // sorted
		{[]string{"foo", "bar", "foobar", "bar", "foo"}, []string{"bar", "foo", "foobar"}},
	}

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := util.RemoveDuplicates(tc.list)
			assert.Equal(t, tc.expected, actual, "For list %v", tc.list)
		})
	}
}

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

	testCases := []struct {
		list     []string
		expected []string
	}{
		{[]string{}, []string{}},
		{[]string{"foo"}, []string{"foo"}},
		{[]string{"foo", "bar"}, []string{"foo", "bar"}},
		{[]string{"foo", "bar", "foobar", "bar", "foo"}, []string{"foobar", "bar", "foo"}},
		{[]string{"foo", "bar", "foobar", "foo", "bar"}, []string{"foobar", "foo", "bar"}},
	}

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := util.RemoveDuplicatesKeepLast(tc.list)
			assert.Equal(t, tc.expected, actual, "For list %v", tc.list)
		})
	}
}

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

	testCases := []struct {
		a        []string
		b        []string
		expected []string
	}{
		{[]string{}, []string{}, []string{}},
		{[]string{"foo"}, []string{}, []string{"foo"}},
		{[]string{}, []string{"bar"}, []string{"bar"}},
		{[]string{"foo"}, []string{"bar"}, []string{"bar", "foo"}}, // sorted
		{[]string{"foo", "bar"}, []string{"bar", "baz"}, []string{"bar", "baz", "foo"}},
	}

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()

			actual := util.MergeSlices(tc.a, tc.b)
			assert.Equal(t, tc.expected, actual, "For lists %v and %v", tc.a, tc.b)
		})
	}
}

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

	type item struct {
		name string
		v    string
	}

	name := func(i item) string { return i.name }

	testCases := []struct {
		desc     string
		base     []item
		override []item
		expected []item
	}{
		{
			desc:     "override replaces same-name base in place and preserves base order",
			base:     []item{{"a", "base-a"}, {"b", "base-b"}, {"c", "base-c"}},
			override: []item{{"b", "over-b"}},
			expected: []item{{"a", "base-a"}, {"b", "over-b"}, {"c", "base-c"}},
		},
		{
			desc:     "new override names append last in override order",
			base:     []item{{"a", "base-a"}},
			override: []item{{"b", "over-b"}, {"c", "over-c"}},
			expected: []item{{"a", "base-a"}, {"b", "over-b"}, {"c", "over-c"}},
		},
		{
			desc:     "mixed replace and append",
			base:     []item{{"a", "base-a"}, {"b", "base-b"}},
			override: []item{{"b", "over-b"}, {"c", "over-c"}},
			expected: []item{{"a", "base-a"}, {"b", "over-b"}, {"c", "over-c"}},
		},
		{
			desc:     "later same-name base entries are dropped once overridden",
			base:     []item{{"a", "base-a1"}, {"a", "base-a2"}},
			override: []item{{"a", "over-a"}},
			expected: []item{{"a", "over-a"}},
		},
		{
			desc:     "intra-base duplicates with no override are left intact",
			base:     []item{{"a", "base-a1"}, {"a", "base-a2"}},
			override: nil,
			expected: []item{{"a", "base-a1"}, {"a", "base-a2"}},
		},
		{
			desc:     "last writer wins within the override set",
			base:     []item{{"a", "base-a"}},
			override: []item{{"a", "over-a1"}, {"a", "over-a2"}},
			expected: []item{{"a", "over-a2"}},
		},
		{
			desc:     "empty-name base entries pass through and empty-name override entries are dropped",
			base:     []item{{"", "base-blank"}, {"a", "base-a"}},
			override: []item{{"", "over-blank"}, {"a", "over-a"}},
			expected: []item{{"", "base-blank"}, {"a", "over-a"}},
		},
		{
			desc:     "empty base returns override entries in order",
			base:     nil,
			override: []item{{"a", "over-a"}, {"b", "over-b"}},
			expected: []item{{"a", "over-a"}, {"b", "over-b"}},
		},
		{
			desc:     "empty override returns base unchanged",
			base:     []item{{"a", "base-a"}},
			override: nil,
			expected: []item{{"a", "base-a"}},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			t.Parallel()

			actual := util.MergeNamed(tc.base, tc.override, name)
			assert.Equal(t, tc.expected, actual)
		})
	}
}
