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

package hclwrite

import (
	"bytes"
	"math/big"
	"sort"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"
)

func TestTokensForValue(t *testing.T) {
	tests := []struct {
		Val  cty.Value
		Want Tokens
	}{
		{
			cty.NullVal(cty.DynamicPseudoType),
			Tokens{
				{
					Type:  hclsyntax.TokenIdent,
					Bytes: []byte(`null`),
				},
			},
		},
		{
			cty.True,
			Tokens{
				{
					Type:  hclsyntax.TokenIdent,
					Bytes: []byte(`true`),
				},
			},
		},
		{
			cty.False,
			Tokens{
				{
					Type:  hclsyntax.TokenIdent,
					Bytes: []byte(`false`),
				},
			},
		},
		{
			cty.NumberIntVal(0),
			Tokens{
				{
					Type:  hclsyntax.TokenNumberLit,
					Bytes: []byte(`0`),
				},
			},
		},
		{
			cty.NumberFloatVal(0.5),
			Tokens{
				{
					Type:  hclsyntax.TokenNumberLit,
					Bytes: []byte(`0.5`),
				},
			},
		},
		{
			cty.NumberVal(big.NewFloat(0).SetPrec(512).Mul(big.NewFloat(40000000), big.NewFloat(2000000))),
			Tokens{
				{
					Type:  hclsyntax.TokenNumberLit,
					Bytes: []byte(`80000000000000`),
				},
			},
		},
		{
			cty.StringVal(""),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.StringVal("foo"),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte(`foo`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.StringVal(`"foo"`),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte(`\"foo\"`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.StringVal("hello\nworld\n"),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte(`hello\nworld\n`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.StringVal("hello\r\nworld\r\n"),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte(`hello\r\nworld\r\n`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.StringVal(`what\what`),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte(`what\\what`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.StringVal("𝄞"),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte("𝄞"),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.StringVal("👩🏾"),
			Tokens{
				{
					Type:  hclsyntax.TokenOQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte(`👩🏾`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
			},
		},
		{
			cty.EmptyTupleVal,
			Tokens{
				{
					Type:  hclsyntax.TokenOBrack,
					Bytes: []byte(`[`),
				},
				{
					Type:  hclsyntax.TokenCBrack,
					Bytes: []byte(`]`),
				},
			},
		},
		{
			cty.TupleVal([]cty.Value{cty.EmptyTupleVal}),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrack,
					Bytes: []byte(`[`),
				},
				{
					Type:  hclsyntax.TokenOBrack,
					Bytes: []byte(`[`),
				},
				{
					Type:  hclsyntax.TokenCBrack,
					Bytes: []byte(`]`),
				},
				{
					Type:  hclsyntax.TokenCBrack,
					Bytes: []byte(`]`),
				},
			},
		},
		{
			cty.ListValEmpty(cty.String),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrack,
					Bytes: []byte(`[`),
				},
				{
					Type:  hclsyntax.TokenCBrack,
					Bytes: []byte(`]`),
				},
			},
		},
		{
			cty.SetValEmpty(cty.Bool),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrack,
					Bytes: []byte(`[`),
				},
				{
					Type:  hclsyntax.TokenCBrack,
					Bytes: []byte(`]`),
				},
			},
		},
		{
			cty.TupleVal([]cty.Value{cty.True}),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrack,
					Bytes: []byte(`[`),
				},
				{
					Type:  hclsyntax.TokenIdent,
					Bytes: []byte(`true`),
				},
				{
					Type:  hclsyntax.TokenCBrack,
					Bytes: []byte(`]`),
				},
			},
		},
		{
			cty.TupleVal([]cty.Value{cty.True, cty.NumberIntVal(0)}),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrack,
					Bytes: []byte(`[`),
				},
				{
					Type:  hclsyntax.TokenIdent,
					Bytes: []byte(`true`),
				},
				{
					Type:  hclsyntax.TokenComma,
					Bytes: []byte(`,`),
				},
				{
					Type:         hclsyntax.TokenNumberLit,
					Bytes:        []byte(`0`),
					SpacesBefore: 1,
				},
				{
					Type:  hclsyntax.TokenCBrack,
					Bytes: []byte(`]`),
				},
			},
		},
		{
			cty.EmptyObjectVal,
			Tokens{
				{
					Type:  hclsyntax.TokenOBrace,
					Bytes: []byte(`{`),
				},
				{
					Type:  hclsyntax.TokenCBrace,
					Bytes: []byte(`}`),
				},
			},
		},
		{
			cty.MapValEmpty(cty.Bool),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrace,
					Bytes: []byte(`{`),
				},
				{
					Type:  hclsyntax.TokenCBrace,
					Bytes: []byte(`}`),
				},
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.True,
			}),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrace,
					Bytes: []byte(`{`),
				},
				{
					Type:  hclsyntax.TokenNewline,
					Bytes: []byte("\n"),
				},
				{
					Type:         hclsyntax.TokenIdent,
					Bytes:        []byte(`foo`),
					SpacesBefore: 2,
				},
				{
					Type:         hclsyntax.TokenEqual,
					Bytes:        []byte(`=`),
					SpacesBefore: 1,
				},
				{
					Type:         hclsyntax.TokenIdent,
					Bytes:        []byte(`true`),
					SpacesBefore: 1,
				},
				{
					Type:  hclsyntax.TokenNewline,
					Bytes: []byte("\n"),
				},
				{
					Type:  hclsyntax.TokenCBrace,
					Bytes: []byte(`}`),
				},
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.True,
				"bar": cty.NumberIntVal(0),
			}),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrace,
					Bytes: []byte(`{`),
				},
				{
					Type:  hclsyntax.TokenNewline,
					Bytes: []byte("\n"),
				},
				{
					Type:         hclsyntax.TokenIdent,
					Bytes:        []byte(`bar`),
					SpacesBefore: 2,
				},
				{
					Type:         hclsyntax.TokenEqual,
					Bytes:        []byte(`=`),
					SpacesBefore: 1,
				},
				{
					Type:         hclsyntax.TokenNumberLit,
					Bytes:        []byte(`0`),
					SpacesBefore: 1,
				},
				{
					Type:  hclsyntax.TokenNewline,
					Bytes: []byte("\n"),
				},
				{
					Type:         hclsyntax.TokenIdent,
					Bytes:        []byte(`foo`),
					SpacesBefore: 2,
				},
				{
					Type:         hclsyntax.TokenEqual,
					Bytes:        []byte(`=`),
					SpacesBefore: 1,
				},
				{
					Type:         hclsyntax.TokenIdent,
					Bytes:        []byte(`true`),
					SpacesBefore: 1,
				},
				{
					Type:  hclsyntax.TokenNewline,
					Bytes: []byte("\n"),
				},
				{
					Type:  hclsyntax.TokenCBrace,
					Bytes: []byte(`}`),
				},
			},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo bar": cty.True,
			}),
			Tokens{
				{
					Type:  hclsyntax.TokenOBrace,
					Bytes: []byte(`{`),
				},
				{
					Type:  hclsyntax.TokenNewline,
					Bytes: []byte("\n"),
				},
				{
					Type:         hclsyntax.TokenOQuote,
					Bytes:        []byte(`"`),
					SpacesBefore: 2,
				},
				{
					Type:  hclsyntax.TokenQuotedLit,
					Bytes: []byte(`foo bar`),
				},
				{
					Type:  hclsyntax.TokenCQuote,
					Bytes: []byte(`"`),
				},
				{
					Type:         hclsyntax.TokenEqual,
					Bytes:        []byte(`=`),
					SpacesBefore: 1,
				},
				{
					Type:         hclsyntax.TokenIdent,
					Bytes:        []byte(`true`),
					SpacesBefore: 1,
				},
				{
					Type:  hclsyntax.TokenNewline,
					Bytes: []byte("\n"),
				},
				{
					Type:  hclsyntax.TokenCBrace,
					Bytes: []byte(`}`),
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.Val.GoString(), func(t *testing.T) {
			got := TokensForValue(test.Val)

			if !cmp.Equal(got, test.Want) {
				diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool {
					return bytes.Equal(a, b)
				}))
				var gotBuf, wantBuf bytes.Buffer
				got.WriteTo(&gotBuf)
				test.Want.WriteTo(&wantBuf)
				t.Errorf(
					"wrong result\nvalue: %#v\ngot:   %s\nwant:  %s\ndiff:  %s",
					test.Val, gotBuf.String(), wantBuf.String(), diff,
				)
			}
		})
	}
}

func TestTokensForTraversal(t *testing.T) {
	tests := []struct {
		Val  hcl.Traversal
		Want Tokens
	}{
		{
			hcl.Traversal{
				hcl.TraverseRoot{Name: "root"},
				hcl.TraverseAttr{Name: "attr"},
				hcl.TraverseIndex{Key: cty.StringVal("index")},
			},
			Tokens{
				{Type: hclsyntax.TokenIdent, Bytes: []byte("root")},
				{Type: hclsyntax.TokenDot, Bytes: []byte(".")},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("attr")},
				{Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}},
				{Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenQuotedLit, Bytes: []byte("index")},
				{Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}},
			},
		},
	}

	for _, test := range tests {
		got := TokensForTraversal(test.Val)

		if !cmp.Equal(got, test.Want) {
			diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool {
				return bytes.Equal(a, b)
			}))
			var gotBuf, wantBuf bytes.Buffer
			got.WriteTo(&gotBuf)
			test.Want.WriteTo(&wantBuf)
			t.Errorf(
				"wrong result\nvalue: %#v\ngot:   %s\nwant:  %s\ndiff:  %s",
				test.Val, gotBuf.String(), wantBuf.String(), diff,
			)
		}
	}
}

func TestTokensForTuple(t *testing.T) {
	tests := map[string]struct {
		Val  []Tokens
		Want Tokens
	}{
		"no elements": {
			nil,
			Tokens{
				{Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}},
				{Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}},
			},
		},
		"one element": {
			[]Tokens{
				TokensForValue(cty.StringVal("foo")),
			},
			Tokens{
				{Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}},
				{Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenQuotedLit, Bytes: []byte("foo")},
				{Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}},
			},
		},
		"two elements": {
			[]Tokens{
				TokensForTraversal(hcl.Traversal{
					hcl.TraverseRoot{Name: "root"},
					hcl.TraverseAttr{Name: "attr"},
				}),
				TokensForValue(cty.StringVal("foo")),
			},
			Tokens{
				{Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("root")},
				{Type: hclsyntax.TokenDot, Bytes: []byte(".")},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("attr")},
				{Type: hclsyntax.TokenComma, Bytes: []byte{','}},
				{Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 1},
				{Type: hclsyntax.TokenQuotedLit, Bytes: []byte("foo")},
				{Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got := TokensForTuple(test.Val)

			if !cmp.Equal(got, test.Want) {
				diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool {
					return bytes.Equal(a, b)
				}))
				var gotBuf, wantBuf bytes.Buffer
				got.WriteTo(&gotBuf)
				test.Want.WriteTo(&wantBuf)
				t.Errorf(
					"wrong result\nvalue: %#v\ngot:   %s\nwant:  %s\ndiff:  %s",
					test.Val, gotBuf.String(), wantBuf.String(), diff,
				)
			}
		})
	}
}

func TestTokensForObject(t *testing.T) {
	tests := map[string]struct {
		Val  []ObjectAttrTokens
		Want Tokens
	}{
		"no attributes": {
			nil,
			Tokens{
				{Type: hclsyntax.TokenOBrace, Bytes: []byte{'{'}},
				{Type: hclsyntax.TokenCBrace, Bytes: []byte{'}'}},
			},
		},
		"one attribute": {
			[]ObjectAttrTokens{
				{
					Name: TokensForTraversal(hcl.Traversal{
						hcl.TraverseRoot{Name: "bar"},
					}),
					Value: TokensForValue(cty.StringVal("baz")),
				},
			},
			Tokens{
				{Type: hclsyntax.TokenOBrace, Bytes: []byte{'{'}},
				{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("bar"), SpacesBefore: 2},
				{Type: hclsyntax.TokenEqual, Bytes: []byte{'='}, SpacesBefore: 1},
				{Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 1},
				{Type: hclsyntax.TokenQuotedLit, Bytes: []byte("baz")},
				{Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}},
				{Type: hclsyntax.TokenCBrace, Bytes: []byte{'}'}},
			},
		},
		"two attributes": {
			[]ObjectAttrTokens{
				{
					Name: TokensForTraversal(hcl.Traversal{
						hcl.TraverseRoot{Name: "foo"},
					}),
					Value: TokensForTraversal(hcl.Traversal{
						hcl.TraverseRoot{Name: "root"},
						hcl.TraverseAttr{Name: "attr"},
					}),
				},
				{
					Name: TokensForTraversal(hcl.Traversal{
						hcl.TraverseRoot{Name: "bar"},
					}),
					Value: TokensForValue(cty.StringVal("baz")),
				},
			},
			Tokens{
				{Type: hclsyntax.TokenOBrace, Bytes: []byte{'{'}},
				{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("foo"), SpacesBefore: 2},
				{Type: hclsyntax.TokenEqual, Bytes: []byte{'='}, SpacesBefore: 1},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("root"), SpacesBefore: 1},
				{Type: hclsyntax.TokenDot, Bytes: []byte(".")},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("attr")},
				{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("bar"), SpacesBefore: 2},
				{Type: hclsyntax.TokenEqual, Bytes: []byte{'='}, SpacesBefore: 1},
				{Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 1},
				{Type: hclsyntax.TokenQuotedLit, Bytes: []byte("baz")},
				{Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}},
				{Type: hclsyntax.TokenCBrace, Bytes: []byte{'}'}},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got := TokensForObject(test.Val)

			if !cmp.Equal(got, test.Want) {
				diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool {
					return bytes.Equal(a, b)
				}))
				var gotBuf, wantBuf bytes.Buffer
				got.WriteTo(&gotBuf)
				test.Want.WriteTo(&wantBuf)
				t.Errorf(
					"wrong result\nvalue: %#v\ngot:   %s\nwant:  %s\ndiff:  %s",
					test.Val, gotBuf.String(), wantBuf.String(), diff,
				)
			}
		})
	}
}

func TestTokensForFunctionCall(t *testing.T) {
	tests := map[string]struct {
		FuncName string
		Val      []Tokens
		Want     Tokens
	}{
		"no arguments": {
			"uuid",
			nil,
			Tokens{
				{Type: hclsyntax.TokenIdent, Bytes: []byte("uuid")},
				{Type: hclsyntax.TokenOParen, Bytes: []byte{'('}},
				{Type: hclsyntax.TokenCParen, Bytes: []byte(")")},
			},
		},
		"one argument": {
			"strlen",
			[]Tokens{
				TokensForValue(cty.StringVal("hello")),
			},
			Tokens{
				{Type: hclsyntax.TokenIdent, Bytes: []byte("strlen")},
				{Type: hclsyntax.TokenOParen, Bytes: []byte{'('}},
				{Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenQuotedLit, Bytes: []byte("hello")},
				{Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)},
				{Type: hclsyntax.TokenCParen, Bytes: []byte(")")},
			},
		},
		"two arguments": {
			"list",
			[]Tokens{
				TokensForIdentifier("string"),
				TokensForIdentifier("int"),
			},
			Tokens{
				{Type: hclsyntax.TokenIdent, Bytes: []byte("list")},
				{Type: hclsyntax.TokenOParen, Bytes: []byte{'('}},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("string")},
				{Type: hclsyntax.TokenComma, Bytes: []byte(",")},
				{Type: hclsyntax.TokenIdent, Bytes: []byte("int"), SpacesBefore: 1},
				{Type: hclsyntax.TokenCParen, Bytes: []byte(")")},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got := TokensForFunctionCall(test.FuncName, test.Val...)

			if !cmp.Equal(got, test.Want) {
				diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool {
					return bytes.Equal(a, b)
				}))
				var gotBuf, wantBuf bytes.Buffer
				got.WriteTo(&gotBuf)
				test.Want.WriteTo(&wantBuf)
				t.Errorf(
					"wrong result\nvalue: %#v\ngot:   %s\nwant:  %s\ndiff:  %s",
					test.Val, gotBuf.String(), wantBuf.String(), diff,
				)
			}
		})
	}
}

func TestTokenGenerateConsistency(t *testing.T) {

	bytesComparer := cmp.Comparer(func(a, b []byte) bool {
		return bytes.Equal(a, b)
	})

	// This test verifies that different ways of generating equivalent token
	// sequences all generate identical tokens, to help us keep them all in
	// sync under future maintanence.

	t.Run("tuple constructor", func(t *testing.T) {
		tests := map[string]struct {
			elems []cty.Value
		}{
			"no elements": {
				nil,
			},
			"one element": {
				[]cty.Value{
					cty.StringVal("hello"),
				},
			},
			"two elements": {
				[]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("world"),
				},
			},
		}

		for name, test := range tests {
			t.Run(name, func(t *testing.T) {
				var listVal cty.Value
				if len(test.elems) > 0 {
					listVal = cty.ListVal(test.elems)
				} else {
					listVal = cty.ListValEmpty(cty.DynamicPseudoType)
				}
				fromListValue := TokensForValue(listVal)
				fromTupleValue := TokensForValue(cty.TupleVal(test.elems))
				elemTokens := make([]Tokens, len(test.elems))
				for i, v := range test.elems {
					elemTokens[i] = TokensForValue(v)
				}
				fromTupleTokens := TokensForTuple(elemTokens)

				if diff := cmp.Diff(fromListValue, fromTupleTokens, bytesComparer); diff != "" {
					t.Errorf("inconsistency between TokensForValue(list) and TokensForTuple\n%s", diff)
				}
				if diff := cmp.Diff(fromTupleValue, fromTupleTokens, bytesComparer); diff != "" {
					t.Errorf("inconsistency between TokensForValue(tuple) and TokensForTuple\n%s", diff)
				}

			})
		}
	})

	t.Run("object constructor", func(t *testing.T) {
		tests := map[string]struct {
			attrs map[string]cty.Value
		}{
			"no elements": {
				nil,
			},
			"one element": {
				map[string]cty.Value{
					"greeting": cty.StringVal("hello"),
				},
			},
			"two elements": {
				map[string]cty.Value{
					"greeting1": cty.StringVal("hello"),
					"greeting2": cty.StringVal("world"),
				},
			},
		}

		for name, test := range tests {
			t.Run(name, func(t *testing.T) {
				var mapVal cty.Value
				if len(test.attrs) > 0 {
					mapVal = cty.MapVal(test.attrs)
				} else {
					mapVal = cty.MapValEmpty(cty.DynamicPseudoType)
				}
				fromMapValue := TokensForValue(mapVal)
				fromObjectValue := TokensForValue(cty.ObjectVal(test.attrs))
				attrTokens := make([]ObjectAttrTokens, 0, len(test.attrs))

				// TokensForValue always writes the keys/attributes in cty's
				// standard iteration order, but TokensForObject gives the
				// caller direct control of the ordering. The result is
				// therefore consistent only if the given attributes are
				// pre-sorted into the same iteration order, which is a lexical
				// sort by attribute name.
				keys := make([]string, 0, len(test.attrs))
				for k := range test.attrs {
					keys = append(keys, k)
				}
				sort.Strings(keys)
				for _, k := range keys {
					v := test.attrs[k]
					attrTokens = append(attrTokens, ObjectAttrTokens{
						Name:  TokensForIdentifier(k),
						Value: TokensForValue(v),
					})
				}
				fromObjectTokens := TokensForObject(attrTokens)

				if diff := cmp.Diff(fromMapValue, fromObjectTokens, bytesComparer); diff != "" {
					t.Errorf("inconsistency between TokensForValue(map) and TokensForObject\n%s", diff)
				}
				if diff := cmp.Diff(fromObjectValue, fromObjectTokens, bytesComparer); diff != "" {
					t.Errorf("inconsistency between TokensForValue(object) and TokensForObject\n%s", diff)
				}
			})
		}
	})
}
