// Copyright 2020 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 soongconfig

import (
	"reflect"
	"testing"

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

func Test_CanonicalizeToProperty(t *testing.T) {
	tests := []struct {
		name string
		arg  string
		want string
	}{
		{
			name: "lowercase",
			arg:  "board",
			want: "board",
		},
		{
			name: "uppercase",
			arg:  "BOARD",
			want: "BOARD",
		},
		{
			name: "numbers",
			arg:  "BOARD123",
			want: "BOARD123",
		},
		{
			name: "underscore",
			arg:  "TARGET_BOARD",
			want: "TARGET_BOARD",
		},
		{
			name: "dash",
			arg:  "TARGET-BOARD",
			want: "TARGET_BOARD",
		},
		{
			name: "unicode",
			arg:  "boardλ",
			want: "board_",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := CanonicalizeToProperty(tt.arg); got != tt.want {
				t.Errorf("canonicalizeToProperty() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_typeForPropertyFromPropertyStruct(t *testing.T) {
	tests := []struct {
		name     string
		ps       interface{}
		property string
		want     string
	}{
		{
			name: "string",
			ps: struct {
				A string
			}{},
			property: "a",
			want:     "string",
		},
		{
			name: "list",
			ps: struct {
				A []string
			}{},
			property: "a",
			want:     "[]string",
		},
		{
			name: "missing",
			ps: struct {
				A []string
			}{},
			property: "b",
			want:     "",
		},
		{
			name: "nested",
			ps: struct {
				A struct {
					B string
				}
			}{},
			property: "a.b",
			want:     "string",
		},
		{
			name: "missing nested",
			ps: struct {
				A struct {
					B string
				}
			}{},
			property: "a.c",
			want:     "",
		},
		{
			name: "not a struct",
			ps: struct {
				A string
			}{},
			property: "a.b",
			want:     "",
		},
		{
			name: "nested pointer",
			ps: struct {
				A *struct {
					B string
				}
			}{},
			property: "a.b",
			want:     "string",
		},
		{
			name: "nested interface",
			ps: struct {
				A interface{}
			}{
				A: struct {
					B string
				}{},
			},
			property: "a.b",
			want:     "string",
		},
		{
			name: "nested interface pointer",
			ps: struct {
				A interface{}
			}{
				A: &struct {
					B string
				}{},
			},
			property: "a.b",
			want:     "string",
		},
		{
			name: "nested interface nil pointer",
			ps: struct {
				A interface{}
			}{
				A: (*struct {
					B string
				})(nil),
			},
			property: "a.b",
			want:     "string",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			typ := typeForPropertyFromPropertyStruct(tt.ps, tt.property)
			got := ""
			if typ != nil {
				got = typ.String()
			}
			if got != tt.want {
				t.Errorf("typeForPropertyFromPropertyStruct() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_createAffectablePropertiesType(t *testing.T) {
	tests := []struct {
		name                 string
		affectableProperties []string
		factoryProps         interface{}
		want                 string
	}{
		{
			name:                 "string",
			affectableProperties: []string{"cflags"},
			factoryProps: struct {
				Cflags string
			}{},
			want: "*struct { Cflags string }",
		},
		{
			name:                 "list",
			affectableProperties: []string{"cflags"},
			factoryProps: struct {
				Cflags []string
			}{},
			want: "*struct { Cflags []string }",
		},
		{
			name:                 "string pointer",
			affectableProperties: []string{"cflags"},
			factoryProps: struct {
				Cflags *string
			}{},
			want: "*struct { Cflags *string }",
		},
		{
			name:                 "subset",
			affectableProperties: []string{"cflags"},
			factoryProps: struct {
				Cflags  string
				Ldflags string
			}{},
			want: "*struct { Cflags string }",
		},
		{
			name:                 "none",
			affectableProperties: []string{"cflags"},
			factoryProps: struct {
				Ldflags string
			}{},
			want: "",
		},
		{
			name:                 "nested",
			affectableProperties: []string{"multilib.lib32.cflags"},
			factoryProps: struct {
				Multilib struct {
					Lib32 struct {
						Cflags string
					}
				}
			}{},
			want: "*struct { Multilib struct { Lib32 struct { Cflags string } } }",
		},
		{
			name: "complex",
			affectableProperties: []string{
				"cflags",
				"multilib.lib32.cflags",
				"multilib.lib32.ldflags",
				"multilib.lib64.cflags",
				"multilib.lib64.ldflags",
				"zflags",
			},
			factoryProps: struct {
				Cflags   string
				Multilib struct {
					Lib32 struct {
						Cflags  string
						Ldflags string
					}
					Lib64 struct {
						Cflags  string
						Ldflags string
					}
				}
				Zflags string
			}{},
			want: "*struct { Cflags string; Multilib struct { Lib32 struct { Cflags string; Ldflags string }; Lib64 struct { Cflags string; Ldflags string } }; Zflags string }",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			typ := createAffectablePropertiesType(tt.affectableProperties, []interface{}{tt.factoryProps})
			got := ""
			if typ != nil {
				got = typ.String()
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("createAffectablePropertiesType() = %v, want %v", got, tt.want)
			}
		})
	}
}

type properties struct {
	A *string
	B bool
	C []string
}

type varProps struct {
	A                  *string
	B                  bool
	C                  []string
	Conditions_default *properties
}

type boolSoongConfigVars struct {
	Bool_var interface{}
}

type stringSoongConfigVars struct {
	String_var interface{}
}

type valueSoongConfigVars struct {
	My_value_var interface{}
}

type listProperties struct {
	C []string
}

type listVarProps struct {
	C                  []string
	Conditions_default *listProperties
}

type listSoongConfigVars struct {
	List_var interface{}
}

func Test_PropertiesToApply_Bool(t *testing.T) {
	mt, _ := newModuleType(&ModuleTypeProperties{
		Module_type:      "foo",
		Config_namespace: "bar",
		Bool_variables:   []string{"bool_var"},
		Properties:       []string{"a", "b"},
	})
	boolVarPositive := &properties{
		A: proptools.StringPtr("A"),
		B: true,
	}
	conditionsDefault := &properties{
		A: proptools.StringPtr("default"),
		B: false,
	}
	actualProps := &struct {
		Soong_config_variables boolSoongConfigVars
	}{
		Soong_config_variables: boolSoongConfigVars{
			Bool_var: &varProps{
				A:                  boolVarPositive.A,
				B:                  boolVarPositive.B,
				Conditions_default: conditionsDefault,
			},
		},
	}
	props := reflect.ValueOf(actualProps)

	testCases := []struct {
		name      string
		config    SoongConfig
		wantProps []interface{}
	}{
		{
			name:      "no_vendor_config",
			config:    Config(map[string]string{}),
			wantProps: []interface{}{conditionsDefault},
		},
		{
			name:      "vendor_config_false",
			config:    Config(map[string]string{"bool_var": "n"}),
			wantProps: []interface{}{conditionsDefault},
		},
		{
			name:      "bool_var_true",
			config:    Config(map[string]string{"bool_var": "y"}),
			wantProps: []interface{}{boolVarPositive},
		},
	}

	for _, tc := range testCases {
		gotProps, err := PropertiesToApply(mt, props, tc.config)
		if err != nil {
			t.Errorf("%s: Unexpected error in PropertiesToApply: %s", tc.name, err)
		}

		if !reflect.DeepEqual(gotProps, tc.wantProps) {
			t.Errorf("%s: Expected %s, got %s", tc.name, tc.wantProps, gotProps)
		}
	}
}

func Test_PropertiesToApply_List(t *testing.T) {
	mt, _ := newModuleType(&ModuleTypeProperties{
		Module_type:      "foo",
		Config_namespace: "bar",
		List_variables:   []string{"my_list_var"},
		Properties:       []string{"c"},
	})
	conditionsDefault := &listProperties{
		C: []string{"default"},
	}
	actualProps := &struct {
		Soong_config_variables listSoongConfigVars
	}{
		Soong_config_variables: listSoongConfigVars{
			List_var: &listVarProps{
				C:                  []string{"A=%s", "B=%s"},
				Conditions_default: conditionsDefault,
			},
		},
	}
	props := reflect.ValueOf(actualProps)

	testCases := []struct {
		name      string
		config    SoongConfig
		wantProps []interface{}
	}{
		{
			name:      "no_vendor_config",
			config:    Config(map[string]string{}),
			wantProps: []interface{}{conditionsDefault},
		},
		{
			name:   "value_var_set",
			config: Config(map[string]string{"my_list_var": "hello there"}),
			wantProps: []interface{}{&listProperties{
				C: []string{"A=hello", "A=there", "B=hello", "B=there"},
			}},
		},
	}

	for _, tc := range testCases {
		gotProps, err := PropertiesToApply(mt, props, tc.config)
		if err != nil {
			t.Errorf("%s: Unexpected error in PropertiesToApply: %s", tc.name, err)
		}

		if !reflect.DeepEqual(gotProps, tc.wantProps) {
			t.Errorf("%s: Expected %s, got %s", tc.name, tc.wantProps, gotProps)
		}
	}
}

func Test_PropertiesToApply_Value(t *testing.T) {
	mt, _ := newModuleType(&ModuleTypeProperties{
		Module_type:      "foo",
		Config_namespace: "bar",
		Value_variables:  []string{"my_value_var"},
		Properties:       []string{"a", "b"},
	})
	conditionsDefault := &properties{
		A: proptools.StringPtr("default"),
		B: false,
	}
	actualProps := &struct {
		Soong_config_variables valueSoongConfigVars
	}{
		Soong_config_variables: valueSoongConfigVars{
			My_value_var: &varProps{
				A:                  proptools.StringPtr("A=%s"),
				B:                  true,
				Conditions_default: conditionsDefault,
			},
		},
	}
	props := reflect.ValueOf(actualProps)

	testCases := []struct {
		name      string
		config    SoongConfig
		wantProps []interface{}
	}{
		{
			name:      "no_vendor_config",
			config:    Config(map[string]string{}),
			wantProps: []interface{}{conditionsDefault},
		},
		{
			name:   "value_var_set",
			config: Config(map[string]string{"my_value_var": "Hello"}),
			wantProps: []interface{}{&properties{
				A: proptools.StringPtr("A=Hello"),
				B: true,
			}},
		},
	}

	for _, tc := range testCases {
		gotProps, err := PropertiesToApply(mt, props, tc.config)
		if err != nil {
			t.Errorf("%s: Unexpected error in PropertiesToApply: %s", tc.name, err)
		}

		if !reflect.DeepEqual(gotProps, tc.wantProps) {
			t.Errorf("%s: Expected %s, got %s", tc.name, tc.wantProps, gotProps)
		}
	}
}

func Test_PropertiesToApply_Value_Nested(t *testing.T) {
	mt, _ := newModuleType(&ModuleTypeProperties{
		Module_type:      "foo",
		Config_namespace: "bar",
		Value_variables:  []string{"my_value_var"},
		Properties:       []string{"a.b"},
	})
	type properties struct {
		A struct {
			B string
		}
	}
	conditionsDefault := &properties{
		A: struct{ B string }{
			B: "default",
		},
	}
	type valueVarProps struct {
		A struct {
			B string
		}
		Conditions_default *properties
	}
	actualProps := &struct {
		Soong_config_variables valueSoongConfigVars
	}{
		Soong_config_variables: valueSoongConfigVars{
			My_value_var: &valueVarProps{
				A: struct{ B string }{
					B: "A.B=%s",
				},
				Conditions_default: conditionsDefault,
			},
		},
	}
	props := reflect.ValueOf(actualProps)

	testCases := []struct {
		name      string
		config    SoongConfig
		wantProps []interface{}
	}{
		{
			name:      "no_vendor_config",
			config:    Config(map[string]string{}),
			wantProps: []interface{}{conditionsDefault},
		},
		{
			name:   "value_var_set",
			config: Config(map[string]string{"my_value_var": "Hello"}),
			wantProps: []interface{}{&properties{
				A: struct{ B string }{
					B: "A.B=Hello",
				},
			}},
		},
	}

	for _, tc := range testCases {
		gotProps, err := PropertiesToApply(mt, props, tc.config)
		if err != nil {
			t.Errorf("%s: Unexpected error in PropertiesToApply: %s", tc.name, err)
		}

		if !reflect.DeepEqual(gotProps, tc.wantProps) {
			t.Errorf("%s: Expected %s, got %s", tc.name, tc.wantProps, gotProps)
		}
	}
}

func Test_PropertiesToApply_String_Error(t *testing.T) {
	mt, _ := newModuleType(&ModuleTypeProperties{
		Module_type:      "foo",
		Config_namespace: "bar",
		Variables:        []string{"string_var"},
		Properties:       []string{"a", "b"},
	})
	mt.Variables = append(mt.Variables, &stringVariable{
		baseVariable: baseVariable{
			variable: "string_var",
		},
		values: []string{"a", "b", "c"},
	})
	stringVarPositive := &properties{
		A: proptools.StringPtr("A"),
		B: true,
	}
	conditionsDefault := &properties{
		A: proptools.StringPtr("default"),
		B: false,
	}
	actualProps := &struct {
		Soong_config_variables stringSoongConfigVars
	}{
		Soong_config_variables: stringSoongConfigVars{
			String_var: &varProps{
				A:                  stringVarPositive.A,
				B:                  stringVarPositive.B,
				Conditions_default: conditionsDefault,
			},
		},
	}
	props := reflect.ValueOf(actualProps)

	_, err := PropertiesToApply(mt, props, Config(map[string]string{
		"string_var": "x",
	}))
	expected := `Soong config property "string_var" must be one of [a b c], found "x"`
	if err == nil {
		t.Fatalf("Expected an error, got nil")
	} else if err.Error() != expected {
		t.Fatalf("Error message was not correct, expected %q, got %q", expected, err.Error())
	}
}
