// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package proptools

import (
	"bytes"
	"reflect"
	"testing"
	"text/scanner"

	"github.com/google/blueprint/parser"
)

var validUnpackTestCases = []struct {
	name   string
	input  string
	output []interface{}
	empty  []interface{}
	errs   []error
}{
	{
		name: "blank and unset",
		input: `
			m {
				s: "abc",
				blank: "",
			}
		`,
		output: []interface{}{
			&struct {
				S     *string
				Blank *string
				Unset *string
			}{
				S:     StringPtr("abc"),
				Blank: StringPtr(""),
				Unset: nil,
			},
		},
	},

	{
		name: "string",
		input: `
			m {
				s: "abc",
			}
		`,
		output: []interface{}{
			&struct {
				S string
			}{
				S: "abc",
			},
		},
	},

	{
		name: "bool",
		input: `
			m {
				isGood: true,
			}
		`,
		output: []interface{}{
			&struct {
				IsGood bool
			}{
				IsGood: true,
			},
		},
	},

	{
		name: "boolptr",
		input: `
			m {
				isGood: true,
				isBad: false,
			}
		`,
		output: []interface{}{
			&struct {
				IsGood *bool
				IsBad  *bool
				IsUgly *bool
			}{
				IsGood: BoolPtr(true),
				IsBad:  BoolPtr(false),
				IsUgly: nil,
			},
		},
	},

	{
		name: "slice",
		input: `
			m {
				stuff: ["asdf", "jkl;", "qwert",
					"uiop", "bnm,"],
				empty: []
			}
		`,
		output: []interface{}{
			&struct {
				Stuff     []string
				Empty     []string
				Nil       []string
				NonString []struct{ S string } `blueprint:"mutated"`
			}{
				Stuff:     []string{"asdf", "jkl;", "qwert", "uiop", "bnm,"},
				Empty:     []string{},
				Nil:       nil,
				NonString: nil,
			},
		},
	},

	{
		name: "double nested",
		input: `
			m {
				nested: {
					nested: {
						s: "abc",
					},
				},
			}
		`,
		output: []interface{}{
			&struct {
				Nested struct {
					Nested struct {
						S string
					}
				}
			}{
				Nested: struct{ Nested struct{ S string } }{
					Nested: struct{ S string }{
						S: "abc",
					},
				},
			},
		},
	},

	{
		name: "nested",
		input: `
			m {
				nested: {
					s: "abc",
				}
			}
		`,
		output: []interface{}{
			&struct {
				Nested struct {
					S string
				}
			}{
				Nested: struct{ S string }{
					S: "abc",
				},
			},
		},
	},

	{
		name: "nested interface",
		input: `
			m {
				nested: {
					s: "def",
				}
			}
		`,
		output: []interface{}{
			&struct {
				Nested interface{}
			}{
				Nested: &struct{ S string }{
					S: "def",
				},
			},
		},
	},

	{
		name: "mixed",
		input: `
			m {
				nested: {
					foo: "abc",
				},
				bar: false,
				baz: ["def", "ghi"],
			}
		`,
		output: []interface{}{
			&struct {
				Nested struct {
					Foo string
				}
				Bar bool
				Baz []string
			}{
				Nested: struct{ Foo string }{
					Foo: "abc",
				},
				Bar: false,
				Baz: []string{"def", "ghi"},
			},
		},
	},

	{
		name: "filter",
		input: `
			m {
				nested: {
					foo: "abc",
				},
				bar: false,
				baz: ["def", "ghi"],
			}
		`,
		output: []interface{}{
			&struct {
				Nested struct {
					Foo string `allowNested:"true"`
				} `blueprint:"filter(allowNested:\"true\")"`
				Bar bool
				Baz []string
			}{
				Nested: struct {
					Foo string `allowNested:"true"`
				}{
					Foo: "abc",
				},
				Bar: false,
				Baz: []string{"def", "ghi"},
			},
		},
	},

	// List of maps
	{
		name: "list of structs",
		input: `
			m {
				mapslist: [
					{
						foo: "abc",
						bar: true,
					},
					{
						foo: "def",
						bar: false,
					}
				],
			}
		`,
		output: []interface{}{
			&struct {
				Mapslist []struct {
					Foo string
					Bar bool
				}
			}{
				Mapslist: []struct {
					Foo string
					Bar bool
				}{
					{Foo: "abc", Bar: true},
					{Foo: "def", Bar: false},
				},
			},
		},
	},

	// List of pointers to structs
	{
		name: "list of pointers to structs",
		input: `
			m {
				mapslist: [
					{
						foo: "abc",
						bar: true,
					},
					{
						foo: "def",
						bar: false,
					}
				],
			}
		`,
		output: []interface{}{
			&struct {
				Mapslist []*struct {
					Foo string
					Bar bool
				}
			}{
				Mapslist: []*struct {
					Foo string
					Bar bool
				}{
					{Foo: "abc", Bar: true},
					{Foo: "def", Bar: false},
				},
			},
		},
	},

	// List of lists
	{
		name: "list of lists",
		input: `
			m {
				listoflists: [
					["abc",],
					["def",],
				],
			}
		`,
		output: []interface{}{
			&struct {
				Listoflists [][]string
			}{
				Listoflists: [][]string{
					[]string{"abc"},
					[]string{"def"},
				},
			},
		},
	},

	// Multilevel
	{
		name: "multilevel",
		input: `
			m {
				name: "mymodule",
				flag: true,
				settings: ["foo1", "foo2", "foo3",],
				perarch: {
					arm: "32",
					arm64: "64",
				},
				configvars: [
					{ var: "var1", values: ["1.1", "1.2", ], },
					{ var: "var2", values: ["2.1", ], },
				],
            }
        `,
		output: []interface{}{
			&struct {
				Name     string
				Flag     bool
				Settings []string
				Perarch  *struct {
					Arm   string
					Arm64 string
				}
				Configvars []struct {
					Var    string
					Values []string
				}
			}{
				Name:     "mymodule",
				Flag:     true,
				Settings: []string{"foo1", "foo2", "foo3"},
				Perarch: &struct {
					Arm   string
					Arm64 string
				}{Arm: "32", Arm64: "64"},
				Configvars: []struct {
					Var    string
					Values []string
				}{
					{Var: "var1", Values: []string{"1.1", "1.2"}},
					{Var: "var2", Values: []string{"2.1"}},
				},
			},
		},
	},
	// Anonymous struct
	{
		name: "embedded struct",
		input: `
			m {
				s: "abc",
				nested: {
					s: "def",
				},
			}
		`,
		output: []interface{}{
			&struct {
				EmbeddedStruct
				Nested struct {
					EmbeddedStruct
				}
			}{
				EmbeddedStruct: EmbeddedStruct{
					S: "abc",
				},
				Nested: struct {
					EmbeddedStruct
				}{
					EmbeddedStruct: EmbeddedStruct{
						S: "def",
					},
				},
			},
		},
	},

	// Anonymous interface
	{
		name: "embedded interface",
		input: `
			m {
				s: "abc",
				nested: {
					s: "def",
				},
			}
		`,
		output: []interface{}{
			&struct {
				EmbeddedInterface
				Nested struct {
					EmbeddedInterface
				}
			}{
				EmbeddedInterface: &struct{ S string }{
					S: "abc",
				},
				Nested: struct {
					EmbeddedInterface
				}{
					EmbeddedInterface: &struct{ S string }{
						S: "def",
					},
				},
			},
		},
	},

	// Anonymous struct with name collision
	{
		name: "embedded name collision",
		input: `
			m {
				s: "abc",
				nested: {
					s: "def",
				},
			}
		`,
		output: []interface{}{
			&struct {
				S string
				EmbeddedStruct
				Nested struct {
					S string
					EmbeddedStruct
				}
			}{
				S: "abc",
				EmbeddedStruct: EmbeddedStruct{
					S: "abc",
				},
				Nested: struct {
					S string
					EmbeddedStruct
				}{
					S: "def",
					EmbeddedStruct: EmbeddedStruct{
						S: "def",
					},
				},
			},
		},
	},

	// Anonymous interface with name collision
	{
		name: "embeded interface name collision",
		input: `
			m {
				s: "abc",
				nested: {
					s: "def",
				},
			}
		`,
		output: []interface{}{
			&struct {
				S string
				EmbeddedInterface
				Nested struct {
					S string
					EmbeddedInterface
				}
			}{
				S: "abc",
				EmbeddedInterface: &struct{ S string }{
					S: "abc",
				},
				Nested: struct {
					S string
					EmbeddedInterface
				}{
					S: "def",
					EmbeddedInterface: &struct{ S string }{
						S: "def",
					},
				},
			},
		},
	},

	// Variables
	{
		name: "variables",
		input: `
			list = ["abc"]
			string = "def"
			list_with_variable = [string]
			struct_value = { name: "foo" }
			m {
				s: string,
				list: list,
				list2: list_with_variable,
				structattr: struct_value,
			}
		`,
		output: []interface{}{
			&struct {
				S          string
				List       []string
				List2      []string
				Structattr struct {
					Name string
				}
			}{
				S:     "def",
				List:  []string{"abc"},
				List2: []string{"def"},
				Structattr: struct {
					Name string
				}{
					Name: "foo",
				},
			},
		},
	},

	// Multiple property structs
	{
		name: "multiple",
		input: `
			m {
				nested: {
					s: "abc",
				}
			}
		`,
		output: []interface{}{
			&struct {
				Nested struct {
					S string
				}
			}{
				Nested: struct{ S string }{
					S: "abc",
				},
			},
			&struct {
				Nested struct {
					S string
				}
			}{
				Nested: struct{ S string }{
					S: "abc",
				},
			},
			&struct {
			}{},
		},
	},

	// Nil pointer to struct
	{
		name: "nil struct pointer",
		input: `
			m {
				nested: {
					s: "abc",
				}
			}
		`,
		output: []interface{}{
			&struct {
				Nested *struct {
					S string
				}
			}{
				Nested: &struct{ S string }{
					S: "abc",
				},
			},
		},
		empty: []interface{}{
			&struct {
				Nested *struct {
					S string
				}
			}{},
		},
	},

	// Interface containing nil pointer to struct
	{
		name: "interface nil struct pointer",
		input: `
			m {
				nested: {
					s: "abc",
				}
			}
		`,
		output: []interface{}{
			&struct {
				Nested interface{}
			}{
				Nested: &EmbeddedStruct{
					S: "abc",
				},
			},
		},
		empty: []interface{}{
			&struct {
				Nested interface{}
			}{
				Nested: (*EmbeddedStruct)(nil),
			},
		},
	},

	// Factory set properties
	{
		name: "factory properties",
		input: `
			m {
				string: "abc",
				string_ptr: "abc",
				bool: false,
				bool_ptr: false,
				list: ["a", "b", "c"],
			}
		`,
		output: []interface{}{
			&struct {
				String     string
				String_ptr *string
				Bool       bool
				Bool_ptr   *bool
				List       []string
			}{
				String:     "012abc",
				String_ptr: StringPtr("abc"),
				Bool:       true,
				Bool_ptr:   BoolPtr(false),
				List:       []string{"0", "1", "2", "a", "b", "c"},
			},
		},
		empty: []interface{}{
			&struct {
				String     string
				String_ptr *string
				Bool       bool
				Bool_ptr   *bool
				List       []string
			}{
				String:     "012",
				String_ptr: StringPtr("012"),
				Bool:       true,
				Bool_ptr:   BoolPtr(true),
				List:       []string{"0", "1", "2"},
			},
		},
	},
	// Captitalized property
	{
		input: `
			m {
				CAPITALIZED: "foo",
			}
		`,
		output: []interface{}{
			&struct {
				CAPITALIZED string
			}{
				CAPITALIZED: "foo",
			},
		},
	},
	{
		name: "String configurable property that isn't configured",
		input: `
			m {
				foo: "bar"
			}
		`,
		output: []interface{}{
			&struct {
				Foo Configurable[string]
			}{
				Foo: newConfigurableWithPropertyName(
					"foo",
					nil,
					[]ConfigurableCase[string]{{
						value: &parser.String{
							LiteralPos: scanner.Position{
								Offset: 17,
								Line:   3,
								Column: 10,
							},
							Value: "bar",
						},
					}},
					false,
				),
			},
		},
	},
	{
		name: "Bool configurable property that isn't configured",
		input: `
			m {
				foo: true,
			}
		`,
		output: []interface{}{
			&struct {
				Foo Configurable[bool]
			}{
				Foo: newConfigurableWithPropertyName(
					"foo",
					nil,
					[]ConfigurableCase[bool]{{
						value: &parser.Bool{
							LiteralPos: scanner.Position{
								Offset: 17,
								Line:   3,
								Column: 10,
							},
							Value: true,
							Token: "true",
						},
					}},
					false,
				),
			},
		},
	},
	{
		name: "String list configurable property that isn't configured",
		input: `
			m {
				foo: ["a", "b"],
			}
		`,
		output: []interface{}{
			&struct {
				Foo Configurable[[]string]
			}{
				Foo: newConfigurableWithPropertyName(
					"foo",
					nil,
					[]ConfigurableCase[[]string]{{
						value: &parser.List{
							LBracePos: scanner.Position{
								Offset: 17,
								Line:   3,
								Column: 10,
							},
							RBracePos: scanner.Position{
								Offset: 26,
								Line:   3,
								Column: 19,
							},
							Values: []parser.Expression{
								&parser.String{
									LiteralPos: scanner.Position{
										Offset: 18,
										Line:   3,
										Column: 11,
									},
									Value: "a",
								},
								&parser.String{
									LiteralPos: scanner.Position{
										Offset: 23,
										Line:   3,
										Column: 16,
									},
									Value: "b",
								},
							},
						},
					}},
					false,
				),
			},
		},
	},
	{
		name: "Configurable property",
		input: `
			m {
				foo: select(soong_config_variable("my_namespace", "my_variable"), {
					"a": "a2",
					"b": "b2",
					default: "c2",
				})
			}
		`,
		output: []interface{}{
			&struct {
				Foo Configurable[string]
			}{
				Foo: newConfigurableWithPropertyName(
					"foo",
					[]ConfigurableCondition{{
						functionName: "soong_config_variable",
						args: []string{
							"my_namespace",
							"my_variable",
						},
					}},
					[]ConfigurableCase[string]{
						{
							patterns: []ConfigurablePattern{{
								typ:         configurablePatternTypeString,
								stringValue: "a",
							}},
							value: &parser.String{
								LiteralPos: scanner.Position{
									Offset: 90,
									Line:   4,
									Column: 11,
								},
								Value: "a2",
							},
						},
						{
							patterns: []ConfigurablePattern{{
								typ:         configurablePatternTypeString,
								stringValue: "b",
							}},
							value: &parser.String{
								LiteralPos: scanner.Position{
									Offset: 106,
									Line:   5,
									Column: 11,
								},
								Value: "b2",
							},
						},
						{
							patterns: []ConfigurablePattern{{
								typ: configurablePatternTypeDefault,
							}},
							value: &parser.String{
								LiteralPos: scanner.Position{
									Offset: 126,
									Line:   6,
									Column: 15,
								},
								Value: "c2",
							},
						},
					},
					true,
				),
			},
		},
	},
	{
		name: "Configurable property appending",
		input: `
			m {
				foo: select(soong_config_variable("my_namespace", "my_variable"), {
					"a": "a2",
					"b": "b2",
					default: "c2",
				}) + select(soong_config_variable("my_namespace", "my_2nd_variable"), {
					"d": "d2",
					"e": "e2",
					default: "f2",
				})
			}
		`,
		output: []interface{}{
			&struct {
				Foo Configurable[string]
			}{
				Foo: func() Configurable[string] {
					result := newConfigurableWithPropertyName(
						"foo",
						[]ConfigurableCondition{{
							functionName: "soong_config_variable",
							args: []string{
								"my_namespace",
								"my_variable",
							},
						}},
						[]ConfigurableCase[string]{
							{
								patterns: []ConfigurablePattern{{
									typ:         configurablePatternTypeString,
									stringValue: "a",
								}},
								value: &parser.String{
									LiteralPos: scanner.Position{
										Offset: 90,
										Line:   4,
										Column: 11,
									},
									Value: "a2",
								},
							},
							{
								patterns: []ConfigurablePattern{{
									typ:         configurablePatternTypeString,
									stringValue: "b",
								}},
								value: &parser.String{
									LiteralPos: scanner.Position{
										Offset: 106,
										Line:   5,
										Column: 11,
									},
									Value: "b2",
								},
							},
							{
								patterns: []ConfigurablePattern{{
									typ: configurablePatternTypeDefault,
								}},
								value: &parser.String{
									LiteralPos: scanner.Position{
										Offset: 126,
										Line:   6,
										Column: 15,
									},
									Value: "c2",
								},
							},
						},
						true,
					)
					result.Append(newConfigurableWithPropertyName(
						"",
						[]ConfigurableCondition{{
							functionName: "soong_config_variable",
							args: []string{
								"my_namespace",
								"my_2nd_variable",
							},
						}},
						[]ConfigurableCase[string]{
							{
								patterns: []ConfigurablePattern{{
									typ:         configurablePatternTypeString,
									stringValue: "d",
								}},
								value: &parser.String{
									LiteralPos: scanner.Position{
										Offset: 218,
										Line:   8,
										Column: 11,
									},
									Value: "d2",
								},
							},
							{
								patterns: []ConfigurablePattern{{
									typ:         configurablePatternTypeString,
									stringValue: "e",
								}},
								value: &parser.String{
									LiteralPos: scanner.Position{
										Offset: 234,
										Line:   9,
										Column: 11,
									},
									Value: "e2",
								},
							},
							{
								patterns: []ConfigurablePattern{{
									typ: configurablePatternTypeDefault,
								}},
								value: &parser.String{
									LiteralPos: scanner.Position{
										Offset: 254,
										Line:   10,
										Column: 15,
									},
									Value: "f2",
								},
							},
						},
						true,
					))
					return result
				}(),
			},
		},
	},
	{
		name: "Unpack variable to configurable property",
		input: `
			my_string_variable = "asdf"
			my_bool_variable = true
			m {
				foo: my_string_variable,
				bar: my_bool_variable,
			}
		`,
		output: []interface{}{
			&struct {
				Foo Configurable[string]
				Bar Configurable[bool]
			}{
				Foo: newConfigurableWithPropertyName(
					"foo",
					nil,
					[]ConfigurableCase[string]{{
						value: &parser.String{
							LiteralPos: scanner.Position{
								Offset: 25,
								Line:   2,
								Column: 25,
							},
							Value: "asdf",
						},
					}},
					false,
				),
				Bar: newConfigurableWithPropertyName(
					"bar",
					nil,
					[]ConfigurableCase[bool]{{
						value: &parser.Bool{
							LiteralPos: scanner.Position{
								Offset: 54,
								Line:   3,
								Column: 23,
							},
							Value: true,
							Token: "true",
						},
					}},
					false,
				),
			},
		},
	},
}

func TestUnpackProperties(t *testing.T) {
	for _, testCase := range validUnpackTestCases {
		t.Run(testCase.name, func(t *testing.T) {
			r := bytes.NewBufferString(testCase.input)
			file, errs := parser.ParseAndEval("", r, parser.NewScope(nil))
			if len(errs) != 0 {
				t.Errorf("test case: %s", testCase.input)
				t.Errorf("unexpected parse errors:")
				for _, err := range errs {
					t.Errorf("  %s", err)
				}
				t.FailNow()
			}

			for _, def := range file.Defs {
				module, ok := def.(*parser.Module)
				if !ok {
					continue
				}

				var output []interface{}
				if len(testCase.empty) > 0 {
					for _, p := range testCase.empty {
						output = append(output, CloneProperties(reflect.ValueOf(p)).Interface())
					}
				} else {
					for _, p := range testCase.output {
						output = append(output, CloneEmptyProperties(reflect.ValueOf(p)).Interface())
					}
				}

				_, errs = UnpackProperties(module.Properties, output...)
				if len(errs) != 0 && len(testCase.errs) == 0 {
					t.Errorf("test case: %s", testCase.input)
					t.Errorf("unexpected unpack errors:")
					for _, err := range errs {
						t.Errorf("  %s", err)
					}
					t.FailNow()
				} else if !reflect.DeepEqual(errs, testCase.errs) {
					t.Errorf("test case: %s", testCase.input)
					t.Errorf("incorrect errors:")
					t.Errorf("  expected: %+v", testCase.errs)
					t.Errorf("       got: %+v", errs)
				}

				if len(output) != len(testCase.output) {
					t.Fatalf("incorrect number of property structs, expected %d got %d",
						len(testCase.output), len(output))
				}

				for i := range output {
					got := reflect.ValueOf(output[i]).Interface()
					if !reflect.DeepEqual(got, testCase.output[i]) {
						t.Errorf("test case: %s", testCase.input)
						t.Errorf("incorrect output:")
						t.Errorf("  expected: %+v", testCase.output[i])
						t.Errorf("       got: %+v", got)
					}
				}
			}
		})
	}
}

func TestUnpackErrors(t *testing.T) {
	testCases := []struct {
		name   string
		input  string
		output []interface{}
		errors []string
	}{
		{
			name: "missing",
			input: `
				m {
					missing: true,
				}
			`,
			output: []interface{}{},
			errors: []string{`<input>:3:13: unrecognized property "missing"`},
		},
		{
			name: "missing nested",
			input: `
				m {
					nested: {
						missing: true,
					},
				}
			`,
			output: []interface{}{
				&struct {
					Nested struct{}
				}{},
			},
			errors: []string{`<input>:4:14: unrecognized property "nested.missing"`},
		},
		{
			name: "mutated",
			input: `
				m {
					mutated: true,
				}
			`,
			output: []interface{}{
				&struct {
					Mutated bool `blueprint:"mutated"`
				}{},
			},
			errors: []string{`<input>:3:13: mutated field mutated cannot be set in a Blueprint file`},
		},
		{
			name: "nested mutated",
			input: `
				m {
					nested: {
						mutated: true,
					},
				}
			`,
			output: []interface{}{
				&struct {
					Nested struct {
						Mutated bool `blueprint:"mutated"`
					}
				}{},
			},
			errors: []string{`<input>:4:14: mutated field nested.mutated cannot be set in a Blueprint file`},
		},
		{
			name: "duplicate",
			input: `
				m {
					exists: true,
					exists: true,
				}
			`,
			output: []interface{}{
				&struct {
					Exists bool
				}{},
			},
			errors: []string{
				`<input>:4:12: property "exists" already defined`,
				`<input>:3:12: <-- previous definition here`,
			},
		},
		{
			name: "nested duplicate",
			input: `
				m {
					nested: {
						exists: true,
						exists: true,
					},
				}
			`,
			output: []interface{}{
				&struct {
					Nested struct {
						Exists bool
					}
				}{},
			},
			errors: []string{
				`<input>:5:13: property "nested.exists" already defined`,
				`<input>:4:13: <-- previous definition here`,
			},
		},
		{
			name: "wrong type",
			input: `
				m {
					int: "foo",
				}
			`,
			output: []interface{}{
				&struct {
					Int *int64
				}{},
			},
			errors: []string{
				`<input>:3:11: can't assign string value to int64 property "int"`,
			},
		},
		{
			name: "wrong type for map",
			input: `
				m {
					map: "foo",
				}
			`,
			output: []interface{}{
				&struct {
					Map struct {
						S string
					}
				}{},
			},
			errors: []string{
				`<input>:3:11: can't assign string value to map property "map"`,
			},
		},
		{
			name: "wrong type for list",
			input: `
				m {
					list: "foo",
				}
			`,
			output: []interface{}{
				&struct {
					List []string
				}{},
			},
			errors: []string{
				`<input>:3:12: can't assign string value to list property "list"`,
			},
		},
		{
			name: "wrong type for list of maps",
			input: `
				m {
					map_list: "foo",
				}
			`,
			output: []interface{}{
				&struct {
					Map_list []struct {
						S string
					}
				}{},
			},
			errors: []string{
				`<input>:3:16: can't assign string value to list property "map_list"`,
			},
		},
		{
			name: "non-existent property",
			input: `
				m {
					foo: {
						foo_prop1: true,
						foo_prop2: false,
						foo_prop3: true,
					},
					bar: {
						bar_prop: false,
					},
					baz: true,
					exist: false,
				}
			`,
			output: []interface{}{
				&struct {
					Foo struct {
						Foo_prop1 bool
					}
					Exist bool
				}{},
			},
			errors: []string{
				`<input>:5:16: unrecognized property "foo.foo_prop2"`,
				`<input>:6:16: unrecognized property "foo.foo_prop3"`,
				`<input>:9:15: unrecognized property "bar.bar_prop"`,
				`<input>:11:9: unrecognized property "baz"`,
			},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			r := bytes.NewBufferString(testCase.input)
			file, errs := parser.ParseAndEval("", r, parser.NewScope(nil))
			if len(errs) != 0 {
				t.Errorf("test case: %s", testCase.input)
				t.Errorf("unexpected parse errors:")
				for _, err := range errs {
					t.Errorf("  %s", err)
				}
				t.FailNow()
			}

			for _, def := range file.Defs {
				module, ok := def.(*parser.Module)
				if !ok {
					continue
				}

				var output []interface{}
				for _, p := range testCase.output {
					output = append(output, CloneEmptyProperties(reflect.ValueOf(p)).Interface())
				}

				_, errs = UnpackProperties(module.Properties, output...)

				printErrors := false
				for _, expectedErr := range testCase.errors {
					foundError := false
					for _, err := range errs {
						if err.Error() == expectedErr {
							foundError = true
						}
					}
					if !foundError {
						t.Errorf("expected error %s", expectedErr)
						printErrors = true
					}
				}
				if printErrors {
					t.Errorf("got errors:")
					for _, err := range errs {
						t.Errorf("   %s", err.Error())
					}
				}
			}
		})
	}
}

func BenchmarkUnpackProperties(b *testing.B) {
	run := func(b *testing.B, props []interface{}, input string) {
		b.ReportAllocs()
		b.StopTimer()
		r := bytes.NewBufferString(input)
		file, errs := parser.ParseAndEval("", r, parser.NewScope(nil))
		if len(errs) != 0 {
			b.Errorf("test case: %s", input)
			b.Errorf("unexpected parse errors:")
			for _, err := range errs {
				b.Errorf("  %s", err)
			}
			b.FailNow()
		}

		for i := 0; i < b.N; i++ {
			for _, def := range file.Defs {
				module, ok := def.(*parser.Module)
				if !ok {
					continue
				}

				var output []interface{}
				for _, p := range props {
					output = append(output, CloneProperties(reflect.ValueOf(p)).Interface())
				}

				b.StartTimer()
				_, errs = UnpackProperties(module.Properties, output...)
				b.StopTimer()
				if len(errs) > 0 {
					b.Errorf("unexpected unpack errors:")
					for _, err := range errs {
						b.Errorf("  %s", err)
					}
				}
			}
		}
	}

	b.Run("basic", func(b *testing.B) {
		props := []interface{}{
			&struct {
				Nested struct {
					S string
				}
			}{},
		}
		bp := `
			m {
				nested: {
					s: "abc",
				},
			}
		`
		run(b, props, bp)
	})

	b.Run("interface", func(b *testing.B) {
		props := []interface{}{
			&struct {
				Nested interface{}
			}{
				Nested: (*struct {
					S string
				})(nil),
			},
		}
		bp := `
			m {
				nested: {
					s: "abc",
				},
			}
		`
		run(b, props, bp)
	})

	b.Run("many", func(b *testing.B) {
		props := []interface{}{
			&struct {
				A *string
				B *string
				C *string
				D *string
				E *string
				F *string
				G *string
				H *string
				I *string
				J *string
			}{},
		}
		bp := `
			m {
				a: "a",
				b: "b",
				c: "c",
				d: "d",
				e: "e",
				f: "f",
				g: "g",
				h: "h",
				i: "i",
				j: "j",
			}
		`
		run(b, props, bp)
	})

	b.Run("deep", func(b *testing.B) {
		props := []interface{}{
			&struct {
				Nested struct {
					Nested struct {
						Nested struct {
							Nested struct {
								Nested struct {
									Nested struct {
										Nested struct {
											Nested struct {
												Nested struct {
													Nested struct {
														S string
													}
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}{},
		}
		bp := `
			m {
				nested: { nested: { nested: { nested: { nested: {
					nested: { nested: { nested: { nested: { nested: {
						s: "abc",
					}, }, }, }, },
				}, }, }, }, },
			}
		`
		run(b, props, bp)
	})

	b.Run("mix", func(b *testing.B) {
		props := []interface{}{
			&struct {
				Name     string
				Flag     bool
				Settings []string
				Perarch  *struct {
					Arm   string
					Arm64 string
				}
				Configvars []struct {
					Name   string
					Values []string
				}
			}{},
		}
		bp := `
			m {
				name: "mymodule",
				flag: true,
				settings: ["foo1", "foo2", "foo3",],
				perarch: {
					arm: "32",
					arm64: "64",
				},
				configvars: [
					{ name: "var1", values: ["var1:1", "var1:2", ], },
					{ name: "var2", values: ["var2:1", "var2:2", ], },
				],
            }
        `
		run(b, props, bp)
	})
}

func TestRemoveUnnecessaryUnusedNames(t *testing.T) {
	testCases := []struct {
		name   string
		input  []string
		output []string
	}{
		{
			name:   "no unused names",
			input:  []string{},
			output: []string{},
		},
		{
			name:   "only one unused name",
			input:  []string{"a.b.c"},
			output: []string{"a.b.c"},
		},
		{
			name:   "unused names in a chain",
			input:  []string{"a", "a.b", "a.b.c"},
			output: []string{"a.b.c"},
		},
		{
			name:   "unused names unrelated",
			input:  []string{"a.b.c", "s.t", "x.y"},
			output: []string{"a.b.c", "s.t", "x.y"},
		},
		{
			name:   "unused names partially related one",
			input:  []string{"a.b", "a.b.c", "a.b.d"},
			output: []string{"a.b.c", "a.b.d"},
		},
		{
			name:   "unused names partially related two",
			input:  []string{"a", "a.b.c", "a.c"},
			output: []string{"a.b.c", "a.c"},
		},
		{
			name:   "unused names partially related three",
			input:  []string{"a.b.c", "b.c", "c"},
			output: []string{"a.b.c", "b.c", "c"},
		},
	}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			simplifiedNames := removeUnnecessaryUnusedNames(testCase.input)
			if !reflect.DeepEqual(simplifiedNames, testCase.output) {
				t.Errorf("test case: %s", testCase.name)
				t.Errorf("  input: %s", testCase.input)
				t.Errorf("  expect: %s", testCase.output)
				t.Errorf("  got: %s", simplifiedNames)
			}
		})
	}
}
