package funcs

import (
	"fmt"
	"math"
	"testing"

	"github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks"
	"github.com/zclconf/go-cty/cty"
)

func TestLength(t *testing.T) {
	tests := []struct {
		Value cty.Value
		Want  cty.Value
	}{
		{
			cty.ListValEmpty(cty.Number),
			cty.NumberIntVal(0),
		},
		{
			cty.ListVal([]cty.Value{cty.True}),
			cty.NumberIntVal(1),
		},
		{
			cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
			cty.NumberIntVal(1),
		},
		{
			cty.SetValEmpty(cty.Number),
			cty.NumberIntVal(0),
		},
		{
			cty.SetVal([]cty.Value{cty.True}),
			cty.NumberIntVal(1),
		},
		{
			cty.MapValEmpty(cty.Bool),
			cty.NumberIntVal(0),
		},
		{
			cty.MapVal(map[string]cty.Value{"hello": cty.True}),
			cty.NumberIntVal(1),
		},
		{
			cty.EmptyTupleVal,
			cty.NumberIntVal(0),
		},
		{
			cty.UnknownVal(cty.EmptyTuple),
			cty.NumberIntVal(0),
		},
		{
			cty.TupleVal([]cty.Value{cty.True}),
			cty.NumberIntVal(1),
		},
		{
			cty.EmptyObjectVal,
			cty.NumberIntVal(0),
		},
		{
			cty.UnknownVal(cty.EmptyObject),
			cty.NumberIntVal(0),
		},
		{
			cty.ObjectVal(map[string]cty.Value{"true": cty.True}),
			cty.NumberIntVal(1),
		},
		{
			cty.UnknownVal(cty.List(cty.Bool)),
			cty.UnknownVal(cty.Number).Refine().NotNull().NumberRangeInclusive(cty.NumberIntVal(0), cty.NumberIntVal(math.MaxInt64)).NewValue(),
		},
		{
			cty.DynamicVal,
			cty.UnknownVal(cty.Number),
		},
		{
			cty.StringVal("hello"),
			cty.NumberIntVal(5),
		},
		{
			cty.StringVal(""),
			cty.NumberIntVal(0),
		},
		{
			cty.StringVal("1"),
			cty.NumberIntVal(1),
		},
		{
			cty.StringVal("Живой Журнал"),
			cty.NumberIntVal(12),
		},
		{
			// note that the dieresis here is intentionally a combining
			// ligature.
			cty.StringVal("noël"),
			cty.NumberIntVal(4),
		},
		{
			// The Es in this string has three combining acute accents.
			// This tests something that NFC-normalization cannot collapse
			// into a single precombined codepoint, since otherwise we might
			// be cheating and relying on the single-codepoint forms.
			cty.StringVal("wé́́é́́é́́!"),
			cty.NumberIntVal(5),
		},
		{
			// Go's normalization forms don't handle this ligature, so we
			// will produce the wrong result but this is now a compatibility
			// constraint and so we'll test it.
			cty.StringVal("baﬄe"),
			cty.NumberIntVal(4),
		},
		{
			cty.StringVal("😸😾"),
			cty.NumberIntVal(2),
		},
		{
			cty.UnknownVal(cty.String),
			cty.UnknownVal(cty.Number).Refine().NotNull().NumberRangeLowerBound(cty.NumberIntVal(0), true).NewValue(),
		},
		{
			cty.DynamicVal,
			cty.UnknownVal(cty.Number),
		},
		{ // Marked collections return a marked length
			cty.ListVal([]cty.Value{
				cty.StringVal("hello"),
				cty.StringVal("world"),
			}).Mark("secret"),
			cty.NumberIntVal(2).Mark("secret"),
		},
		{ // Marks on values in unmarked collections do not propagate
			cty.ListVal([]cty.Value{
				cty.StringVal("hello").Mark("a"),
				cty.StringVal("world").Mark("b"),
			}),
			cty.NumberIntVal(2),
		},
		{ // Marked strings return a marked length
			cty.StringVal("hello world").Mark("secret"),
			cty.NumberIntVal(11).Mark("secret"),
		},
		{ // Marked tuples return a marked length
			cty.TupleVal([]cty.Value{
				cty.StringVal("hello"),
				cty.StringVal("world"),
			}).Mark("secret"),
			cty.NumberIntVal(2).Mark("secret"),
		},
		{ // Marks on values in unmarked tuples do not propagate
			cty.TupleVal([]cty.Value{
				cty.StringVal("hello").Mark("a"),
				cty.StringVal("world").Mark("b"),
			}),
			cty.NumberIntVal(2),
		},
		{ // Marked objects return a marked length
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
				"b": cty.StringVal("world"),
				"c": cty.StringVal("nice to meet you"),
			}).Mark("secret"),
			cty.NumberIntVal(3).Mark("secret"),
		},
		{ // Marks on object attribute values do not propagate
			cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello").Mark("a"),
				"b": cty.StringVal("world").Mark("b"),
				"c": cty.StringVal("nice to meet you").Mark("c"),
			}),
			cty.NumberIntVal(3),
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("Length(%#v)", test.Value), func(t *testing.T) {
			got, err := Length(test.Value)

			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestAllTrue(t *testing.T) {
	tests := []struct {
		Collection cty.Value
		Want       cty.Value
		Err        bool
	}{
		{
			cty.ListValEmpty(cty.Bool),
			cty.True,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.True}),
			cty.True,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.False}),
			cty.False,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.True, cty.False}),
			cty.False,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.False, cty.True}),
			cty.False,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.True, cty.NullVal(cty.Bool)}),
			cty.False,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			cty.ListVal([]cty.Value{
				cty.UnknownVal(cty.Bool),
				cty.UnknownVal(cty.Bool),
			}),
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			cty.UnknownVal(cty.List(cty.Bool)),
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			cty.NullVal(cty.List(cty.Bool)),
			cty.NilVal,
			true,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("alltrue(%#v)", test.Collection), func(t *testing.T) {
			got, err := AllTrue(test.Collection)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestAnyTrue(t *testing.T) {
	tests := []struct {
		Collection cty.Value
		Want       cty.Value
		Err        bool
	}{
		{
			cty.ListValEmpty(cty.Bool),
			cty.False,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.True}),
			cty.True,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.False}),
			cty.False,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.True, cty.False}),
			cty.True,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.False, cty.True}),
			cty.True,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.NullVal(cty.Bool), cty.True}),
			cty.True,
			false,
		},
		{
			cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			cty.ListVal([]cty.Value{
				cty.UnknownVal(cty.Bool),
				cty.False,
			}),
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			cty.ListVal([]cty.Value{
				cty.UnknownVal(cty.Bool),
				cty.True,
			}),
			cty.True,
			false,
		},
		{
			cty.UnknownVal(cty.List(cty.Bool)),
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			cty.NullVal(cty.List(cty.Bool)),
			cty.NilVal,
			true,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("anytrue(%#v)", test.Collection), func(t *testing.T) {
			got, err := AnyTrue(test.Collection)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestCoalesce(t *testing.T) {
	tests := []struct {
		Values []cty.Value
		Want   cty.Value
		Err    bool
	}{
		{
			[]cty.Value{cty.StringVal("first"), cty.StringVal("second"), cty.StringVal("third")},
			cty.StringVal("first"),
			false,
		},
		{
			[]cty.Value{cty.StringVal(""), cty.StringVal("second"), cty.StringVal("third")},
			cty.StringVal("second"),
			false,
		},
		{
			[]cty.Value{cty.StringVal(""), cty.StringVal("")},
			cty.NilVal,
			true,
		},
		{
			[]cty.Value{cty.True},
			cty.True,
			false,
		},
		{
			[]cty.Value{cty.NullVal(cty.Bool), cty.True},
			cty.True,
			false,
		},
		{
			[]cty.Value{cty.NullVal(cty.Bool), cty.False},
			cty.False,
			false,
		},
		{
			[]cty.Value{cty.NullVal(cty.Bool), cty.False, cty.StringVal("hello")},
			cty.StringVal("false"),
			false,
		},
		{
			[]cty.Value{cty.True, cty.UnknownVal(cty.Bool)},
			cty.True,
			false,
		},
		{
			[]cty.Value{cty.UnknownVal(cty.Bool), cty.True},
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			[]cty.Value{cty.UnknownVal(cty.Bool), cty.StringVal("hello")},
			cty.UnknownVal(cty.String),
			false,
		},
		{
			[]cty.Value{cty.DynamicVal, cty.True},
			cty.UnknownVal(cty.Bool),
			false,
		},
		{
			[]cty.Value{cty.DynamicVal},
			cty.DynamicVal,
			false,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("Coalesce(%#v...)", test.Values), func(t *testing.T) {
			got, err := Coalesce(test.Values...)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestIndex(t *testing.T) {
	tests := []struct {
		List  cty.Value
		Value cty.Value
		Want  cty.Value
		Err   bool
	}{
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.StringVal("a"),
			cty.NumberIntVal(0),
			false,
		},
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.UnknownVal(cty.String),
			}),
			cty.StringVal("a"),
			cty.NumberIntVal(0),
			false,
		},
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.StringVal("b"),
			cty.NumberIntVal(1),
			false,
		},
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.StringVal("z"),
			cty.NilVal,
			true,
		},
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("1"),
				cty.StringVal("2"),
				cty.StringVal("3"),
			}),
			cty.NumberIntVal(1),
			cty.NumberIntVal(0),
			true,
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.NumberIntVal(2),
			cty.NumberIntVal(1),
			false,
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.NumberIntVal(4),
			cty.NilVal,
			true,
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.StringVal("1"),
			cty.NumberIntVal(0),
			true,
		},
		{
			cty.TupleVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.NumberIntVal(1),
			cty.NumberIntVal(0),
			false,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("index(%#v, %#v)", test.List, test.Value), func(t *testing.T) {
			got, err := Index(test.List, test.Value)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestLookup(t *testing.T) {
	simpleMap := cty.MapVal(map[string]cty.Value{
		"foo": cty.StringVal("bar"),
	})
	intsMap := cty.MapVal(map[string]cty.Value{
		"foo": cty.NumberIntVal(42),
	})
	mapOfLists := cty.MapVal(map[string]cty.Value{
		"foo": cty.ListVal([]cty.Value{
			cty.StringVal("bar"),
			cty.StringVal("baz"),
		}),
	})
	mapOfMaps := cty.MapVal(map[string]cty.Value{
		"foo": cty.MapVal(map[string]cty.Value{
			"a": cty.StringVal("bar"),
		}),
		"baz": cty.MapVal(map[string]cty.Value{
			"b": cty.StringVal("bat"),
		}),
	})
	mapOfTuples := cty.MapVal(map[string]cty.Value{
		"foo": cty.TupleVal([]cty.Value{cty.StringVal("bar")}),
		"baz": cty.TupleVal([]cty.Value{cty.StringVal("bat")}),
	})
	objectOfMaps := cty.ObjectVal(map[string]cty.Value{
		"foo": cty.MapVal(map[string]cty.Value{
			"a": cty.StringVal("bar"),
		}),
		"baz": cty.MapVal(map[string]cty.Value{
			"b": cty.StringVal("bat"),
		}),
	})
	mapWithUnknowns := cty.MapVal(map[string]cty.Value{
		"foo": cty.StringVal("bar"),
		"baz": cty.UnknownVal(cty.String),
	})
	mapWithObjects := cty.ObjectVal(map[string]cty.Value{
		"foo": cty.StringVal("bar"),
		"baz": cty.NumberIntVal(42),
	})

	tests := []struct {
		Values []cty.Value
		Want   cty.Value
		Err    bool
	}{
		{
			[]cty.Value{
				simpleMap,
				cty.StringVal("foo"),
			},
			cty.StringVal("bar"),
			false,
		},
		{
			[]cty.Value{
				mapWithObjects,
				cty.StringVal("foo"),
			},
			cty.StringVal("bar"),
			false,
		},
		{
			[]cty.Value{
				intsMap,
				cty.StringVal("foo"),
			},
			cty.NumberIntVal(42),
			false,
		},
		{
			[]cty.Value{
				mapOfMaps,
				cty.StringVal("foo"),
			},
			cty.MapVal(map[string]cty.Value{
				"a": cty.StringVal("bar"),
			}),
			false,
		},
		{
			[]cty.Value{
				objectOfMaps,
				cty.StringVal("foo"),
			},
			cty.MapVal(map[string]cty.Value{
				"a": cty.StringVal("bar"),
			}),
			false,
		},
		{
			[]cty.Value{
				mapOfTuples,
				cty.StringVal("foo"),
			},
			cty.TupleVal([]cty.Value{cty.StringVal("bar")}),
			false,
		},
		{ // Invalid key
			[]cty.Value{
				simpleMap,
				cty.StringVal("bar"),
			},
			cty.NilVal,
			true,
		},
		{ // Invalid key
			[]cty.Value{
				mapWithObjects,
				cty.StringVal("bar"),
			},
			cty.NilVal,
			true,
		},
		{ // Supplied default with valid key
			[]cty.Value{
				simpleMap,
				cty.StringVal("foo"),
				cty.StringVal(""),
			},
			cty.StringVal("bar"),
			false,
		},
		{ // Supplied default with valid (int) key
			[]cty.Value{
				simpleMap,
				cty.StringVal("foo"),
				cty.NumberIntVal(-1),
			},
			cty.StringVal("bar"),
			false,
		},
		{ // Supplied default with valid (int) key
			[]cty.Value{
				simpleMap,
				cty.StringVal("foobar"),
				cty.NumberIntVal(-1),
			},
			cty.StringVal("-1"),
			false,
		},
		{ // Supplied default with valid key
			[]cty.Value{
				mapWithObjects,
				cty.StringVal("foobar"),
				cty.StringVal(""),
			},
			cty.StringVal(""),
			false,
		},
		{ // Supplied default with invalid key
			[]cty.Value{
				simpleMap,
				cty.StringVal("baz"),
				cty.StringVal(""),
			},
			cty.StringVal(""),
			false,
		},
		{ // Supplied default with type mismatch: expects a map return
			[]cty.Value{
				mapOfMaps,
				cty.StringVal("foo"),
				cty.StringVal(""),
			},
			cty.NilVal,
			true,
		},
		{ // Supplied non-empty default with invalid key
			[]cty.Value{
				simpleMap,
				cty.StringVal("bar"),
				cty.StringVal("xyz"),
			},
			cty.StringVal("xyz"),
			false,
		},
		{ // too many args
			[]cty.Value{
				simpleMap,
				cty.StringVal("foo"),
				cty.StringVal("bar"),
				cty.StringVal("baz"),
			},
			cty.NilVal,
			true,
		},
		{ // cannot search a map of lists
			[]cty.Value{
				mapOfLists,
				cty.StringVal("baz"),
			},
			cty.NilVal,
			true,
		},
		{
			[]cty.Value{
				mapWithUnknowns,
				cty.StringVal("baz"),
			},
			cty.UnknownVal(cty.String),
			false,
		},
		{
			[]cty.Value{
				mapWithUnknowns,
				cty.StringVal("foo"),
			},
			cty.StringVal("bar"),
			false,
		},
		{
			[]cty.Value{
				simpleMap,
				cty.UnknownVal(cty.String),
			},
			cty.UnknownVal(cty.String),
			false,
		},
		{
			[]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("a"),
					"bar": cty.StringVal("b"),
				}),
				cty.UnknownVal(cty.String),
			},
			cty.DynamicVal, // if the key is unknown then we don't know which object attribute and thus can't know the type
			false,
		},
		{ // successful marked collection lookup returns marked value
			[]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"boop": cty.StringVal("beep"),
				}).Mark("a"),
				cty.StringVal("boop"),
				cty.StringVal("nope"),
			},
			cty.StringVal("beep").Mark("a"),
			false,
		},
		{ // apply collection marks to unknown return vaue
			[]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"boop": cty.StringVal("beep"),
					"frob": cty.UnknownVal(cty.String),
				}).Mark("a"),
				cty.StringVal("frob"),
				cty.StringVal("nope"),
			},
			cty.UnknownVal(cty.String).Mark("a"),
			false,
		},
		{ // propagate collection marks to default when returning
			[]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"boop": cty.StringVal("beep"),
				}).Mark("a"),
				cty.StringVal("frob"),
				cty.StringVal("nope").Mark("b"),
			},
			cty.StringVal("nope").WithMarks(cty.NewValueMarks("a", "b")),
			false,
		},
		{ // on unmarked collection, return only marks from found value
			[]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"boop": cty.StringVal("beep").Mark("a"),
					"frob": cty.StringVal("honk").Mark("b"),
				}),
				cty.StringVal("frob"),
				cty.StringVal("nope").Mark("c"),
			},
			cty.StringVal("honk").Mark("b"),
			false,
		},
		{ // on unmarked collection, return default exactly on missing
			[]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"boop": cty.StringVal("beep").Mark("a"),
					"frob": cty.StringVal("honk").Mark("b"),
				}),
				cty.StringVal("squish"),
				cty.StringVal("nope").Mark("c"),
			},
			cty.StringVal("nope").Mark("c"),
			false,
		},
		{ // retain marks on default if converted
			[]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"boop": cty.StringVal("beep").Mark("a"),
					"frob": cty.StringVal("honk").Mark("b"),
				}),
				cty.StringVal("squish"),
				cty.NumberIntVal(5).Mark("c"),
			},
			cty.StringVal("5").Mark("c"),
			false,
		},
		{ // propagate marks from key
			[]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"boop": cty.StringVal("beep"),
					"frob": cty.StringVal("honk"),
				}),
				cty.StringVal("boop").Mark("a"),
				cty.StringVal("nope"),
			},
			cty.StringVal("beep").Mark("a"),
			false,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("lookup(%#v)", test.Values), func(t *testing.T) {
			got, err := Lookup(test.Values...)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestLookup_error(t *testing.T) {
	simpleMap := cty.MapVal(map[string]cty.Value{
		"foo": cty.StringVal("bar"),
	})

	tests := map[string]struct {
		Values  []cty.Value
		WantErr string
	}{
		"failed to find non-sensitive key": {
			[]cty.Value{
				simpleMap,
				cty.StringVal("boop"),
			},
			`lookup failed to find key "boop"`,
		},
		"failed to find sensitive key": {
			[]cty.Value{
				simpleMap,
				cty.StringVal("boop").Mark(marks.Sensitive),
			},
			"lookup failed to find key (sensitive value)",
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			_, err := Lookup(test.Values...)

			if err == nil {
				t.Fatal("succeeded; want error")
			}

			if err.Error() != test.WantErr {
				t.Errorf("wrong error\ngot:  %#v\nwant: %#v", err, test.WantErr)
			}
		})
	}
}

func TestMatchkeys(t *testing.T) {
	tests := []struct {
		Keys      cty.Value
		Values    cty.Value
		Searchset cty.Value
		Want      cty.Value
		Err       bool
	}{
		{ // normal usage
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref1"),
				cty.StringVal("ref2"),
				cty.StringVal("ref3"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref1"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			false,
		},
		{ // normal usage 2, check the order
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref1"),
				cty.StringVal("ref2"),
				cty.StringVal("ref3"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref2"),
				cty.StringVal("ref1"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
			}),
			false,
		},
		{ // no matches
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref1"),
				cty.StringVal("ref2"),
				cty.StringVal("ref3"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref4"),
			}),
			cty.ListValEmpty(cty.String),
			false,
		},
		{ // no matches 2
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref1"),
				cty.StringVal("ref2"),
				cty.StringVal("ref3"),
			}),
			cty.ListValEmpty(cty.String),
			cty.ListValEmpty(cty.String),
			false,
		},
		{ // zero case
			cty.ListValEmpty(cty.String),
			cty.ListValEmpty(cty.String),
			cty.ListVal([]cty.Value{cty.StringVal("nope")}),
			cty.ListValEmpty(cty.String),
			false,
		},
		{ // complex values
			cty.ListVal([]cty.Value{
				cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("a"),
				}),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.ListVal([]cty.Value{
				cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("a"),
				}),
			}),
			false,
		},
		{ // unknowns
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.UnknownVal(cty.String),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref1"),
				cty.StringVal("ref2"),
				cty.UnknownVal(cty.String),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("ref1"),
			}),
			cty.UnknownVal(cty.List(cty.String)),
			false,
		},
		{ // different types that can be unified
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.ListValEmpty(cty.String),
			false,
		},
		{ // complex values: values is a different type from keys and searchset
			cty.ListVal([]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				cty.MapVal(map[string]cty.Value{
					"foo": cty.StringVal("baz"),
				}),
				cty.MapVal(map[string]cty.Value{
					"foo": cty.StringVal("beep"),
				}),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("c"),
			}),
			cty.ListVal([]cty.Value{
				cty.MapVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				cty.MapVal(map[string]cty.Value{
					"foo": cty.StringVal("beep"),
				}),
			}),
			false,
		},
		// errors
		{ // different types
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.ListVal([]cty.Value{
				cty.ListVal([]cty.Value{
					cty.StringVal("a"),
				}),
				cty.ListVal([]cty.Value{
					cty.StringVal("a"),
				}),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.NilVal,
			true,
		},
		{ // lists of different length
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
			}),
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
			}),
			cty.NilVal,
			true,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("matchkeys(%#v, %#v, %#v)", test.Keys, test.Values, test.Searchset), func(t *testing.T) {
			got, err := Matchkeys(test.Keys, test.Values, test.Searchset)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestOne(t *testing.T) {
	tests := []struct {
		List cty.Value
		Want cty.Value
		Err  string
	}{
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
			}),
			cty.NumberIntVal(1),
			"",
		},
		{
			cty.ListValEmpty(cty.Number),
			cty.NullVal(cty.Number),
			"",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.ListVal([]cty.Value{
				cty.UnknownVal(cty.Number),
			}),
			cty.UnknownVal(cty.Number),
			"",
		},
		{
			cty.ListVal([]cty.Value{
				cty.UnknownVal(cty.Number),
				cty.UnknownVal(cty.Number),
			}),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.UnknownVal(cty.List(cty.String)),
			cty.UnknownVal(cty.String),
			"",
		},
		{
			cty.NullVal(cty.List(cty.String)),
			cty.NilVal,
			"argument must not be null",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
			}).Mark("boop"),
			cty.NumberIntVal(1).Mark("boop"),
			"",
		},
		{
			cty.ListValEmpty(cty.Bool).Mark("boop"),
			cty.NullVal(cty.Bool).Mark("boop"),
			"",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1).Mark("boop"),
			}),
			cty.NumberIntVal(1).Mark("boop"),
			"",
		},

		{
			cty.SetVal([]cty.Value{
				cty.NumberIntVal(1),
			}),
			cty.NumberIntVal(1),
			"",
		},
		{
			cty.SetValEmpty(cty.Number),
			cty.NullVal(cty.Number),
			"",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.SetVal([]cty.Value{
				cty.UnknownVal(cty.Number),
			}),
			cty.UnknownVal(cty.Number),
			"",
		},
		{
			cty.SetVal([]cty.Value{
				cty.UnknownVal(cty.Number),
				cty.UnknownVal(cty.Number),
			}),
			// The above would be valid if those two unknown values were
			// equal known values, so this returns unknown rather than failing.
			cty.UnknownVal(cty.Number),
			"",
		},
		{
			cty.UnknownVal(cty.Set(cty.String)),
			cty.UnknownVal(cty.String),
			"",
		},
		{
			cty.NullVal(cty.Set(cty.String)),
			cty.NilVal,
			"argument must not be null",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberIntVal(1),
			}).Mark("boop"),
			cty.NumberIntVal(1).Mark("boop"),
			"",
		},
		{
			cty.SetValEmpty(cty.Bool).Mark("boop"),
			cty.NullVal(cty.Bool).Mark("boop"),
			"",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberIntVal(1).Mark("boop"),
			}),
			cty.NumberIntVal(1).Mark("boop"),
			"",
		},

		{
			cty.TupleVal([]cty.Value{
				cty.NumberIntVal(1),
			}),
			cty.NumberIntVal(1),
			"",
		},
		{
			cty.EmptyTupleVal,
			cty.NullVal(cty.DynamicPseudoType),
			"",
		},
		{
			cty.TupleVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.TupleVal([]cty.Value{
				cty.UnknownVal(cty.Number),
			}),
			cty.UnknownVal(cty.Number),
			"",
		},
		{
			cty.TupleVal([]cty.Value{
				cty.UnknownVal(cty.Number),
				cty.UnknownVal(cty.Number),
			}),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.UnknownVal(cty.EmptyTuple),
			// Could actually return null here, but don't for consistency with unknown lists
			cty.UnknownVal(cty.DynamicPseudoType),
			"",
		},
		{
			cty.UnknownVal(cty.Tuple([]cty.Type{cty.Bool})),
			cty.UnknownVal(cty.Bool),
			"",
		},
		{
			cty.UnknownVal(cty.Tuple([]cty.Type{cty.Bool, cty.Number})),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.NullVal(cty.EmptyTuple),
			cty.NilVal,
			"argument must not be null",
		},
		{
			cty.NullVal(cty.Tuple([]cty.Type{cty.Bool})),
			cty.NilVal,
			"argument must not be null",
		},
		{
			cty.NullVal(cty.Tuple([]cty.Type{cty.Bool, cty.Number})),
			cty.NilVal,
			"argument must not be null",
		},
		{
			cty.TupleVal([]cty.Value{
				cty.NumberIntVal(1),
			}).Mark("boop"),
			cty.NumberIntVal(1).Mark("boop"),
			"",
		},
		{
			cty.EmptyTupleVal.Mark("boop"),
			cty.NullVal(cty.DynamicPseudoType).Mark("boop"),
			"",
		},
		{
			cty.TupleVal([]cty.Value{
				cty.NumberIntVal(1).Mark("boop"),
			}),
			cty.NumberIntVal(1).Mark("boop"),
			"",
		},

		{
			cty.DynamicVal,
			cty.DynamicVal,
			"",
		},
		{
			cty.NullVal(cty.DynamicPseudoType),
			cty.NilVal,
			"argument must not be null",
		},
		{
			cty.MapValEmpty(cty.String),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.EmptyObjectVal,
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.True,
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
		{
			cty.UnknownVal(cty.Bool),
			cty.NilVal,
			"must be a list, set, or tuple value with either zero or one elements",
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("one(%#v)", test.List), func(t *testing.T) {
			got, err := One(test.List)

			if test.Err != "" {
				if err == nil {
					t.Fatal("succeeded; want error")
				} else if got, want := err.Error(), test.Err; got != want {
					t.Fatalf("wrong error\n got: %s\nwant: %s", got, want)
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !test.Want.RawEquals(got) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestSum(t *testing.T) {
	tests := []struct {
		List cty.Value
		Want cty.Value
		Err  string
	}{
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1),
				cty.NumberIntVal(2),
				cty.NumberIntVal(3),
			}),
			cty.NumberIntVal(6),
			"",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(1476),
				cty.NumberIntVal(2093),
				cty.NumberIntVal(2092495),
				cty.NumberIntVal(64589234),
				cty.NumberIntVal(234),
			}),
			cty.NumberIntVal(66685532),
			"",
		},
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.UnknownVal(cty.String),
			"argument must be list, set, or tuple of number values",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(10),
				cty.NumberIntVal(-19),
				cty.NumberIntVal(5),
			}),
			cty.NumberIntVal(-4),
			"",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberFloatVal(10.2),
				cty.NumberFloatVal(19.4),
				cty.NumberFloatVal(5.7),
			}),
			cty.NumberFloatVal(35.3),
			"",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberFloatVal(-10.2),
				cty.NumberFloatVal(-19.4),
				cty.NumberFloatVal(-5.7),
			}),
			cty.NumberFloatVal(-35.3),
			"",
		},
		{
			cty.ListVal([]cty.Value{cty.NullVal(cty.Number)}),
			cty.NilVal,
			"argument must be list, set, or tuple of number values",
		},
		{
			cty.ListVal([]cty.Value{
				cty.NumberIntVal(5),
				cty.NullVal(cty.Number),
			}),
			cty.NilVal,
			"argument must be list, set, or tuple of number values",
		},
		{
			cty.SetVal([]cty.Value{
				cty.StringVal("a"),
				cty.StringVal("b"),
				cty.StringVal("c"),
			}),
			cty.UnknownVal(cty.String),
			"argument must be list, set, or tuple of number values",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberIntVal(10),
				cty.NumberIntVal(-19),
				cty.NumberIntVal(5),
			}),
			cty.NumberIntVal(-4),
			"",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberIntVal(10),
				cty.NumberIntVal(25),
				cty.NumberIntVal(30),
			}),
			cty.NumberIntVal(65),
			"",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberFloatVal(2340.8),
				cty.NumberFloatVal(10.2),
				cty.NumberFloatVal(3),
			}),
			cty.NumberFloatVal(2354),
			"",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberFloatVal(2),
			}),
			cty.NumberFloatVal(2),
			"",
		},
		{
			cty.SetVal([]cty.Value{
				cty.NumberFloatVal(-2),
				cty.NumberFloatVal(-50),
				cty.NumberFloatVal(-20),
				cty.NumberFloatVal(-123),
				cty.NumberFloatVal(-4),
			}),
			cty.NumberFloatVal(-199),
			"",
		},
		{
			cty.TupleVal([]cty.Value{
				cty.NumberIntVal(12),
				cty.StringVal("a"),
				cty.NumberIntVal(38),
			}),
			cty.UnknownVal(cty.String),
			"argument must be list, set, or tuple of number values",
		},
		{
			cty.NumberIntVal(12),
			cty.NilVal,
			"cannot sum noniterable",
		},
		{
			cty.ListValEmpty(cty.Number),
			cty.NilVal,
			"cannot sum an empty list",
		},
		{
			cty.MapVal(map[string]cty.Value{"hello": cty.True}),
			cty.NilVal,
			"argument must be list, set, or tuple. Received map of bool",
		},
		{
			cty.UnknownVal(cty.Number),
			cty.UnknownVal(cty.Number),
			"",
		},
		{
			cty.UnknownVal(cty.List(cty.Number)),
			cty.UnknownVal(cty.Number),
			"",
		},
		{ // known list containing unknown values
			cty.ListVal([]cty.Value{cty.UnknownVal(cty.Number)}),
			cty.UnknownVal(cty.Number),
			"",
		},
		{ // numbers too large to represent as float64
			cty.ListVal([]cty.Value{
				cty.MustParseNumberVal("1e+500"),
				cty.MustParseNumberVal("1e+500"),
			}),
			cty.MustParseNumberVal("2e+500"),
			"",
		},
		{ // edge case we have a special error handler for
			cty.ListVal([]cty.Value{
				cty.NumberFloatVal(math.Inf(1)),
				cty.NumberFloatVal(math.Inf(-1)),
			}),
			cty.NilVal,
			"can't compute sum of opposing infinities",
		},
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("1"),
				cty.StringVal("2"),
				cty.StringVal("3"),
			}),
			cty.NumberIntVal(6),
			"",
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("sum(%#v)", test.List), func(t *testing.T) {
			got, err := Sum(test.List)

			if test.Err != "" {
				if err == nil {
					t.Fatal("succeeded; want error")
				} else if got, want := err.Error(), test.Err; got != want {
					t.Fatalf("wrong error\n got: %s\nwant: %s", got, want)
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestTranspose(t *testing.T) {
	tests := []struct {
		Values cty.Value
		Want   cty.Value
		Err    bool
	}{
		{
			cty.MapVal(map[string]cty.Value{
				"key1": cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("b"),
				}),
				"key2": cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("b"),
					cty.StringVal("c"),
				}),
				"key3": cty.ListVal([]cty.Value{
					cty.StringVal("c"),
				}),
				"key4": cty.ListValEmpty(cty.String),
			}),
			cty.MapVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"b": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"c": cty.ListVal([]cty.Value{
					cty.StringVal("key2"),
					cty.StringVal("key3"),
				}),
			}),
			false,
		},
		{ // map - unknown value
			cty.MapVal(map[string]cty.Value{
				"key1": cty.UnknownVal(cty.List(cty.String)),
			}),
			cty.UnknownVal(cty.Map(cty.List(cty.String))),
			false,
		},
		{ // bad map - empty value
			cty.MapVal(map[string]cty.Value{
				"key1": cty.ListValEmpty(cty.String),
			}),
			cty.MapValEmpty(cty.List(cty.String)),
			false,
		},
		{ // bad map - value not a list
			cty.MapVal(map[string]cty.Value{
				"key1": cty.StringVal("a"),
			}),
			cty.NilVal,
			true,
		},
		{ // marks (deep or shallow) on any elements will propegate to the entire return value
			cty.MapVal(map[string]cty.Value{
				"key1": cty.ListVal([]cty.Value{
					cty.StringVal("a").Mark("beep"), // mark on the inner list element
					cty.StringVal("b"),
				}),
				"key2": cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("b"),
					cty.StringVal("c"),
				}).Mark("boop"), // mark on the map element
				"key3": cty.ListVal([]cty.Value{
					cty.StringVal("c"),
				}),
				"key4": cty.ListValEmpty(cty.String),
			}),
			cty.MapVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"b": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"c": cty.ListVal([]cty.Value{
					cty.StringVal("key2"),
					cty.StringVal("key3")}),
			}).WithMarks(cty.NewValueMarks("beep", "boop")),
			false,
		},
		{ // Marks on the input value will be applied to the return value
			cty.MapVal(map[string]cty.Value{
				"key1": cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("b"),
				}),
				"key2": cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("b"),
					cty.StringVal("c"),
				}),
				"key3": cty.ListVal([]cty.Value{
					cty.StringVal("c"),
				}),
			}).Mark("beep"), // mark on the entire input value
			cty.MapVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"b": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"c": cty.ListVal([]cty.Value{
					cty.StringVal("key2"),
					cty.StringVal("key3"),
				}),
			}).Mark("beep"),
			false,
		},
		{ // Marks on the entire input value AND inner elements (deep or shallow) ALL apply to the return
			cty.MapVal(map[string]cty.Value{
				"key1": cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("b"),
				}).Mark("beep"), // mark on the map element
				"key2": cty.ListVal([]cty.Value{
					cty.StringVal("a"),
					cty.StringVal("b"),
					cty.StringVal("c"),
				}),
				"key3": cty.ListVal([]cty.Value{
					cty.StringVal("c").Mark("boop"), // mark on the inner list element
				}),
			}).Mark("bloop"), // mark on the entire input value
			cty.MapVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"b": cty.ListVal([]cty.Value{
					cty.StringVal("key1"),
					cty.StringVal("key2"),
				}),
				"c": cty.ListVal([]cty.Value{
					cty.StringVal("key2"),
					cty.StringVal("key3"),
				}),
			}).WithMarks(cty.NewValueMarks("beep", "boop", "bloop")),
			false,
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("transpose(%#v)", test.Values), func(t *testing.T) {
			got, err := Transpose(test.Values)

			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			if !got.RawEquals(test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}
