package funcs

import (
	"fmt"
	"testing"

	"github.com/zclconf/go-cty/cty"
)

func TestDefaults(t *testing.T) {
	tests := []struct {
		Input, Defaults cty.Value
		Want            cty.Value
		WantErr         string
	}{
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hey"),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hey"),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
			}),
			WantErr: `.a: target type does not expect an attribute named "a"`,
		},

		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.NullVal(cty.String),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
				}),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.NullVal(cty.String),
					cty.StringVal("hey"),
					cty.NullVal(cty.String),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("hey"),
					cty.StringVal("hello"),
				}),
			}),
		},
		{
			// Using defaults with single set elements is a pretty
			// odd thing to do, but this behavior is just here because
			// it generalizes from how we handle collections. It's
			// tested only to ensure it doesn't change accidentally
			// in future.
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.SetVal([]cty.Value{
					cty.NullVal(cty.String),
					cty.StringVal("hey"),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.SetVal([]cty.Value{
					cty.StringVal("hey"),
					cty.StringVal("hello"),
				}),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.MapVal(map[string]cty.Value{
					"x": cty.NullVal(cty.String),
					"y": cty.StringVal("hey"),
					"z": cty.NullVal(cty.String),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.MapVal(map[string]cty.Value{
					"x": cty.StringVal("hello"),
					"y": cty.StringVal("hey"),
					"z": cty.StringVal("hello"),
				}),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.NullVal(cty.String),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hello"),
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hello"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
				}),
			}),
		},
		{
			Input: cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hey"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"b": cty.NullVal(cty.String),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hey"),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"b": cty.StringVal("hello"),
			}),
			Want: cty.ListVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hey"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hello"),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hey"),
				}),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("boop"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.NullVal(cty.String),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hello"),
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("boop"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hello"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
				}),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hello"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.NullVal(cty.String),
					}),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hello"),
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.SetVal([]cty.Value{
					// After applying defaults, the one with a null value
					// coalesced with the one with a non-null value,
					// and so there's only one left.
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hello"),
					}),
				}),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.MapVal(map[string]cty.Value{
					"boop": cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
					"beep": cty.ObjectVal(map[string]cty.Value{
						"b": cty.NullVal(cty.String),
					}),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ObjectVal(map[string]cty.Value{
					"b": cty.StringVal("hello"),
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.MapVal(map[string]cty.Value{
					"boop": cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
					"beep": cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hello"),
					}),
				}),
			}),
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.NullVal(cty.String),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"b": cty.StringVal("hey"),
					}),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			WantErr: `.a: the default value for a collection of an object type must itself be an object type, not string`,
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.NullVal(cty.String),
					cty.StringVal("hey"),
					cty.NullVal(cty.String),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				// The default value for a list must be a single value
				// of the list's element type which provides defaults
				// for each element separately, so the default for a
				// list of string should be just a single string, not
				// a list of string.
				"a": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
				}),
			}),
			WantErr: `.a: invalid default value for string: string required`,
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.TupleVal([]cty.Value{
					cty.NullVal(cty.String),
					cty.StringVal("hey"),
					cty.NullVal(cty.String),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
			}),
			WantErr: `.a: the default value for a tuple type must itself be a tuple type, not string`,
		},
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.TupleVal([]cty.Value{
					cty.NullVal(cty.String),
					cty.StringVal("hey"),
					cty.NullVal(cty.String),
				}),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.TupleVal([]cty.Value{
					cty.StringVal("hello 0"),
					cty.StringVal("hello 1"),
					cty.StringVal("hello 2"),
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.TupleVal([]cty.Value{
					cty.StringVal("hello 0"),
					cty.StringVal("hey"),
					cty.StringVal("hello 2"),
				}),
			}),
		},
		{
			// There's no reason to use this function for plain primitive
			// types, because the "default" argument in a variable definition
			// already has the equivalent behavior. This function is only
			// to deal with the situation of a complex-typed variable where
			// only parts of the data structure are optional.
			Input:    cty.NullVal(cty.String),
			Defaults: cty.StringVal("hello"),
			WantErr:  `only object types and collections of object types can have defaults applied`,
		},
		// When applying default values to structural types, null objects or
		// tuples in the input should be passed through.
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.Object(map[string]cty.Type{
					"x": cty.String,
					"y": cty.String,
				})),
				"b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ObjectVal(map[string]cty.Value{
					"x": cty.StringVal("hello"),
					"y": cty.StringVal("there"),
				}),
				"b": cty.TupleVal([]cty.Value{
					cty.StringVal("how are"),
					cty.StringVal("you?"),
				}),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.Object(map[string]cty.Type{
					"x": cty.String,
					"y": cty.String,
				})),
				"b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
			}),
		},
		// When applying default values to structural types, we permit null
		// values in the defaults, and just pass through the input value.
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"p": cty.StringVal("xyz"),
						"q": cty.StringVal("xyz"),
					}),
				}),
				"b": cty.SetVal([]cty.Value{
					cty.TupleVal([]cty.Value{
						cty.NumberIntVal(0),
						cty.NumberIntVal(2),
					}),
					cty.TupleVal([]cty.Value{
						cty.NumberIntVal(1),
						cty.NumberIntVal(3),
					}),
				}),
				"c": cty.NullVal(cty.String),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"c": cty.StringVal("tada"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"p": cty.StringVal("xyz"),
						"q": cty.StringVal("xyz"),
					}),
				}),
				"b": cty.SetVal([]cty.Value{
					cty.TupleVal([]cty.Value{
						cty.NumberIntVal(0),
						cty.NumberIntVal(2),
					}),
					cty.TupleVal([]cty.Value{
						cty.NumberIntVal(1),
						cty.NumberIntVal(3),
					}),
				}),
				"c": cty.StringVal("tada"),
			}),
		},
		// When applying default values to collection types, null collections in the
		// input should result in empty collections in the output.
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.List(cty.String)),
				"b": cty.NullVal(cty.Map(cty.String)),
				"c": cty.NullVal(cty.Set(cty.String)),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("hello"),
				"b": cty.StringVal("hi"),
				"c": cty.StringVal("greetings"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.ListValEmpty(cty.String),
				"b": cty.MapValEmpty(cty.String),
				"c": cty.SetValEmpty(cty.String),
			}),
		},
		// When specifying fallbacks, we allow mismatched primitive attribute
		// types so long as a safe conversion is possible. This means that we
		// can accept number or boolean values for string attributes.
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.String),
				"b": cty.NullVal(cty.String),
				"c": cty.NullVal(cty.String),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NumberIntVal(5),
				"b": cty.True,
				"c": cty.StringVal("greetings"),
			}),
			Want: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("5"),
				"b": cty.StringVal("true"),
				"c": cty.StringVal("greetings"),
			}),
		},
		// Fallbacks with mismatched primitive attribute types which do not
		// have safe conversions must not pass the suitable fallback check,
		// even if unsafe conversion would be possible.
		{
			Input: cty.ObjectVal(map[string]cty.Value{
				"a": cty.NullVal(cty.Bool),
			}),
			Defaults: cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("5"),
			}),
			WantErr: ".a: invalid default value for bool: bool required",
		},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("defaults(%#v, %#v)", test.Input, test.Defaults), func(t *testing.T) {
			got, gotErr := Defaults(test.Input, test.Defaults)

			if test.WantErr != "" {
				if gotErr == nil {
					t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
				}
				if got, want := gotErr.Error(), test.WantErr; got != want {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				return
			} else if gotErr != nil {
				t.Fatalf("unexpected error\ngot:  %s", gotErr.Error())
			}

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