package attributevalue

import (
	"fmt"
	"reflect"
	"testing"
)

type testUnionValues struct {
	Name  string
	Value interface{}
}

type unionSimple struct {
	A int
	B string
	C []string
}

type unionComplex struct {
	unionSimple
	A int
}

type unionTagged struct {
	A int `dynamodbav:"ddbav" json:"A" taga:"TagA" tagb:"TagB"`
}

type unionTaggedComplex struct {
	unionSimple
	unionTagged
	B string
}

func TestUnionStructFields(t *testing.T) {
	origFieldCache := fieldCache
	defer func() { fieldCache = origFieldCache }()

	fieldCache = &fieldCacher{}

	var cases = map[string]struct {
		in     interface{}
		opts   structFieldOptions
		expect []testUnionValues
	}{
		"simple input": {
			in:   unionSimple{1, "2", []string{"abc"}},
			opts: structFieldOptions{TagKey: "json"},
			expect: []testUnionValues{
				{"A", 1},
				{"B", "2"},
				{"C", []string{"abc"}},
			},
		},
		"nested struct": {
			in: unionComplex{
				unionSimple: unionSimple{1, "2", []string{"abc"}},
				A:           2,
			},
			opts: structFieldOptions{TagKey: "json"},
			expect: []testUnionValues{
				{"B", "2"},
				{"C", []string{"abc"}},
				{"A", 2},
			},
		},
		"with TagKey unset": {
			in: unionTaggedComplex{
				unionSimple: unionSimple{1, "2", []string{"abc"}},
				unionTagged: unionTagged{3},
				B:           "3",
			},
			expect: []testUnionValues{
				{"A", 1},
				{"C", []string{"abc"}},
				{"ddbav", 3},
				{"B", "3"},
			},
		},
		"with TagKey json": {
			in: unionTaggedComplex{
				unionSimple: unionSimple{1, "2", []string{"abc"}},
				unionTagged: unionTagged{3},
				B:           "3",
			},
			opts: structFieldOptions{TagKey: "json"},
			expect: []testUnionValues{
				{"C", []string{"abc"}},
				{"A", 3},
				{"B", "3"},
			},
		},
		"with TagKey taga": {
			in: unionTaggedComplex{
				unionSimple: unionSimple{1, "2", []string{"abc"}},
				unionTagged: unionTagged{3},
				B:           "3",
			},
			opts: structFieldOptions{TagKey: "taga"},
			expect: []testUnionValues{
				{"A", 1},
				{"C", []string{"abc"}},
				{"TagA", 3},
				{"B", "3"},
			},
		},
		"with TagKey tagb": {
			in: unionTaggedComplex{
				unionSimple: unionSimple{1, "2", []string{"abc"}},
				unionTagged: unionTagged{3},
				B:           "3",
			},
			opts: structFieldOptions{TagKey: "tagb"},
			expect: []testUnionValues{
				{"A", 1},
				{"C", []string{"abc"}},
				{"TagB", 3},
				{"B", "3"},
			},
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			v := reflect.ValueOf(c.in)

			fields := unionStructFields(v.Type(), c.opts)
			for i, f := range fields.All() {
				expected := c.expect[i]
				if e, a := expected.Name, f.Name; e != a {
					t.Errorf("%d expect %v, got %v, %v", i, e, a, f)
				}
				actual := v.FieldByIndex(f.Index).Interface()
				if e, a := expected.Value, actual; !reflect.DeepEqual(e, a) {
					t.Errorf("%d expect %v, got %v, %v", i, e, a, f)
				}
			}
		})
	}
}

func TestCachedFields(t *testing.T) {
	type myStruct struct {
		Dog  int `tag1:"rabbit" tag2:"cow" tag3:"horse"`
		CAT  string
		bird bool
	}

	cases := map[string][]struct {
		Name      string
		FieldName string
		Found     bool
	}{
		"": {
			{"Dog", "Dog", true},
			{"dog", "Dog", true},
			{"DOG", "Dog", true},
			{"Yorkie", "", false},
			{"Cat", "CAT", true},
			{"cat", "CAT", true},
			{"CAT", "CAT", true},
			{"tiger", "", false},
			{"bird", "", false},
		},
		"tag1": {
			{"rabbit", "rabbit", true},
			{"Rabbit", "rabbit", true},
			{"cow", "", false},
			{"Cow", "", false},
			{"horse", "", false},
			{"Horse", "", false},
			{"Dog", "", false},
			{"dog", "", false},
			{"DOG", "", false},
			{"Cat", "CAT", true},
			{"cat", "CAT", true},
			{"CAT", "CAT", true},
			{"tiger", "", false},
			{"bird", "", false},
		},
		"tag2": {
			{"rabbit", "", false},
			{"Rabbit", "", false},
			{"cow", "cow", true},
			{"Cow", "cow", true},
			{"horse", "", false},
			{"Horse", "", false},
			{"Dog", "", false},
			{"dog", "", false},
			{"DOG", "", false},
			{"Cat", "CAT", true},
			{"cat", "CAT", true},
			{"CAT", "CAT", true},
			{"tiger", "", false},
			{"bird", "", false},
		},
		"tag3": {
			{"rabbit", "", false},
			{"Rabbit", "", false},
			{"cow", "", false},
			{"Cow", "", false},
			{"horse", "horse", true},
			{"Horse", "horse", true},
			{"Dog", "", false},
			{"dog", "", false},
			{"DOG", "", false},
			{"Cat", "CAT", true},
			{"cat", "CAT", true},
			{"CAT", "CAT", true},
			{"tiger", "", false},
			{"bird", "", false},
		},
	}

	for tagKey, cs := range cases {
		tagKey := tagKey
		cs := cs
		for _, c := range cs {
			name := tagKey
			if name == "" {
				name = "none"
			}

			c := c
			t.Run(fmt.Sprintf("%s/%s", name, c.Name), func(t *testing.T) {
				t.Parallel()

				fields := unionStructFields(reflect.TypeOf(myStruct{}), structFieldOptions{
					TagKey: tagKey,
				})

				const expectedNumFields = 2
				if numFields := len(fields.All()); numFields != expectedNumFields {
					t.Errorf("expect %v fields, got %d", expectedNumFields, numFields)
				}

				f, found := fields.FieldByName(c.Name)
				if found != c.Found {
					t.Errorf("expect %v found, got %v", c.Found, found)
				}
				if found && f.Name != c.FieldName {
					t.Errorf("expect %v field name, got %s", c.FieldName, f.Name)
				}
			})
		}
	}
}
