package dynamodbattribute

import (
	"fmt"
	"reflect"
	"strconv"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/stretchr/testify/assert"
)

func TestUnmarshalErrorTypes(t *testing.T) {
	var _ awserr.Error = (*UnmarshalTypeError)(nil)
	var _ awserr.Error = (*InvalidUnmarshalError)(nil)
}

func TestUnmarshalShared(t *testing.T) {
	for i, c := range sharedTestCases {
		err := Unmarshal(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

func TestUnmarshal(t *testing.T) {
	cases := []struct {
		in               *dynamodb.AttributeValue
		actual, expected interface{}
		err              error
	}{
		//------------
		// Sets
		//------------
		{
			in: &dynamodb.AttributeValue{BS: [][]byte{
				{48, 49}, {50, 51},
			}},
			actual:   &[][]byte{},
			expected: [][]byte{{48, 49}, {50, 51}},
		},
		{
			in: &dynamodb.AttributeValue{NS: []*string{
				aws.String("123"), aws.String("321"),
			}},
			actual:   &[]int{},
			expected: []int{123, 321},
		},
		{
			in: &dynamodb.AttributeValue{NS: []*string{
				aws.String("123"), aws.String("321"),
			}},
			actual:   &[]interface{}{},
			expected: []interface{}{123., 321.},
		},
		{
			in: &dynamodb.AttributeValue{SS: []*string{
				aws.String("abc"), aws.String("123"),
			}},
			actual:   &[]string{},
			expected: &[]string{"abc", "123"},
		},
		{
			in: &dynamodb.AttributeValue{SS: []*string{
				aws.String("abc"), aws.String("123"),
			}},
			actual:   &[]*string{},
			expected: &[]*string{aws.String("abc"), aws.String("123")},
		},
		//------------
		// Interfaces
		//------------
		{
			in: &dynamodb.AttributeValue{B: []byte{48, 49}},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: []byte{48, 49},
		},
		{
			in: &dynamodb.AttributeValue{BS: [][]byte{
				{48, 49}, {50, 51},
			}},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: [][]byte{{48, 49}, {50, 51}},
		},
		{
			in: &dynamodb.AttributeValue{BOOL: aws.Bool(true)},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: bool(true),
		},
		{
			in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
				{S: aws.String("abc")}, {S: aws.String("123")},
			}},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: []interface{}{"abc", "123"},
		},
		{
			in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
				"123": {S: aws.String("abc")},
				"abc": {S: aws.String("123")},
			}},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: map[string]interface{}{"123": "abc", "abc": "123"},
		},
		{
			in: &dynamodb.AttributeValue{N: aws.String("123")},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: float64(123),
		},
		{
			in: &dynamodb.AttributeValue{NS: []*string{
				aws.String("123"), aws.String("321"),
			}},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: []float64{123., 321.},
		},
		{
			in: &dynamodb.AttributeValue{S: aws.String("123")},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: "123",
		},
		{
			in: &dynamodb.AttributeValue{SS: []*string{
				aws.String("123"), aws.String("321"),
			}},
			actual: func() interface{} {
				var v interface{}
				return &v
			}(),
			expected: []string{"123", "321"},
		},
		{
			in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
				"abc": {S: aws.String("123")},
				"Cba": {S: aws.String("321")},
			}},
			actual:   &struct{ Abc, Cba string }{},
			expected: struct{ Abc, Cba string }{Abc: "123", Cba: "321"},
		},
		{
			in:     &dynamodb.AttributeValue{N: aws.String("512")},
			actual: new(uint8),
			err: &UnmarshalTypeError{
				Value: fmt.Sprintf("number overflow, 512"),
				Type:  reflect.TypeOf(uint8(0)),
			},
		},
	}

	for i, c := range cases {
		err := Unmarshal(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

func TestInterfaceInput(t *testing.T) {
	var v interface{}
	expected := []interface{}{"abc", "123"}
	err := Unmarshal(&dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
		{S: aws.String("abc")}, {S: aws.String("123")},
	}}, &v)
	assertConvertTest(t, 0, v, expected, err, nil)
}

func TestUnmarshalError(t *testing.T) {
	cases := []struct {
		in               *dynamodb.AttributeValue
		actual, expected interface{}
		err              error
	}{
		{
			in:       &dynamodb.AttributeValue{},
			actual:   int(0),
			expected: nil,
			err:      &InvalidUnmarshalError{Type: reflect.TypeOf(int(0))},
		},
	}

	for i, c := range cases {
		err := Unmarshal(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

func TestUnmarshalListShared(t *testing.T) {
	for i, c := range sharedListTestCases {
		err := UnmarshalList(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

func TestUnmarshalListError(t *testing.T) {
	cases := []struct {
		in               []*dynamodb.AttributeValue
		actual, expected interface{}
		err              error
	}{
		{
			in:       []*dynamodb.AttributeValue{},
			actual:   []interface{}{},
			expected: nil,
			err:      &InvalidUnmarshalError{Type: reflect.TypeOf([]interface{}{})},
		},
	}

	for i, c := range cases {
		err := UnmarshalList(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

func TestUnmarshalMapShared(t *testing.T) {
	for i, c := range sharedMapTestCases {
		err := UnmarshalMap(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

func TestUnmarshalMapError(t *testing.T) {
	cases := []struct {
		in               map[string]*dynamodb.AttributeValue
		actual, expected interface{}
		err              error
	}{
		{
			in:       map[string]*dynamodb.AttributeValue{},
			actual:   map[string]interface{}{},
			expected: nil,
			err:      &InvalidUnmarshalError{Type: reflect.TypeOf(map[string]interface{}{})},
		},
		{
			in: map[string]*dynamodb.AttributeValue{
				"BOOL": {BOOL: aws.Bool(true)},
			},
			actual:   &map[int]interface{}{},
			expected: nil,
			err:      &UnmarshalTypeError{Value: "map string key", Type: reflect.TypeOf(int(0))},
		},
	}

	for i, c := range cases {
		err := UnmarshalMap(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

func TestUnmarshalListOfMaps(t *testing.T) {
	type testItem struct {
		Value  string
		Value2 int
	}

	cases := []struct {
		in               []map[string]*dynamodb.AttributeValue
		actual, expected interface{}
		err              error
	}{
		{ // Simple map conversion.
			in: []map[string]*dynamodb.AttributeValue{
				{
					"Value": &dynamodb.AttributeValue{
						BOOL: aws.Bool(true),
					},
				},
			},
			actual: &[]map[string]interface{}{},
			expected: []map[string]interface{}{
				{
					"Value": true,
				},
			},
		},
		{ // attribute to struct.
			in: []map[string]*dynamodb.AttributeValue{
				{
					"Value": &dynamodb.AttributeValue{
						S: aws.String("abc"),
					},
					"Value2": &dynamodb.AttributeValue{
						N: aws.String("123"),
					},
				},
			},
			actual: &[]testItem{},
			expected: []testItem{
				{
					Value:  "abc",
					Value2: 123,
				},
			},
		},
	}

	for i, c := range cases {
		err := UnmarshalListOfMaps(c.in, c.actual)
		assertConvertTest(t, i, c.actual, c.expected, err, c.err)
	}
}

type unmarshalUnmarshaler struct {
	Value  string
	Value2 int
	Value3 bool
	Value4 time.Time
}

func (u *unmarshalUnmarshaler) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
	if av.M == nil {
		return fmt.Errorf("expected AttributeValue to be map")
	}

	if v, ok := av.M["abc"]; !ok {
		return fmt.Errorf("expected `abc` map key")
	} else if v.S == nil {
		return fmt.Errorf("expected `abc` map value string")
	} else {
		u.Value = *v.S
	}

	if v, ok := av.M["def"]; !ok {
		return fmt.Errorf("expected `def` map key")
	} else if v.N == nil {
		return fmt.Errorf("expected `def` map value number")
	} else {
		n, err := strconv.ParseInt(*v.N, 10, 64)
		if err != nil {
			return err
		}
		u.Value2 = int(n)
	}

	if v, ok := av.M["ghi"]; !ok {
		return fmt.Errorf("expected `ghi` map key")
	} else if v.BOOL == nil {
		return fmt.Errorf("expected `ghi` map value number")
	} else {
		u.Value3 = *v.BOOL
	}

	if v, ok := av.M["jkl"]; !ok {
		return fmt.Errorf("expected `jkl` map key")
	} else if v.S == nil {
		return fmt.Errorf("expected `jkl` map value string")
	} else {
		t, err := time.Parse(time.RFC3339, *v.S)
		if err != nil {
			return err
		}
		u.Value4 = t
	}

	return nil
}

func TestUnmarshalUnmashaler(t *testing.T) {
	u := &unmarshalUnmarshaler{}
	av := &dynamodb.AttributeValue{
		M: map[string]*dynamodb.AttributeValue{
			"abc": {S: aws.String("value")},
			"def": {N: aws.String("123")},
			"ghi": {BOOL: aws.Bool(true)},
			"jkl": {S: aws.String("2016-05-03T17:06:26.209072Z")},
		},
	}

	err := Unmarshal(av, u)
	assert.NoError(t, err)

	assert.Equal(t, "value", u.Value)
	assert.Equal(t, 123, u.Value2)
	assert.Equal(t, true, u.Value3)
	assert.Equal(t, testDate, u.Value4)
}

func TestDecodeUseNumber(t *testing.T) {
	u := map[string]interface{}{}
	av := &dynamodb.AttributeValue{
		M: map[string]*dynamodb.AttributeValue{
			"abc": {S: aws.String("value")},
			"def": {N: aws.String("123")},
			"ghi": {BOOL: aws.Bool(true)},
		},
	}

	decoder := NewDecoder(func(d *Decoder) {
		d.UseNumber = true
	})
	err := decoder.Decode(av, &u)
	assert.NoError(t, err)

	assert.Equal(t, "value", u["abc"])
	n, ok := u["def"].(Number)
	assert.True(t, ok)
	assert.Equal(t, "123", n.String())
	assert.Equal(t, true, u["ghi"])
}

func TestDecodeUseNumberNumberSet(t *testing.T) {
	u := map[string]interface{}{}
	av := &dynamodb.AttributeValue{
		M: map[string]*dynamodb.AttributeValue{
			"ns": {
				NS: []*string{
					aws.String("123"), aws.String("321"),
				},
			},
		},
	}

	decoder := NewDecoder(func(d *Decoder) {
		d.UseNumber = true
	})
	err := decoder.Decode(av, &u)
	assert.NoError(t, err)

	ns, ok := u["ns"].([]Number)
	assert.True(t, ok)

	assert.Equal(t, "123", ns[0].String())
	assert.Equal(t, "321", ns[1].String())
}

func TestDecodeEmbeddedPointerStruct(t *testing.T) {
	type B struct {
		Bint int
	}
	type C struct {
		Cint int
	}
	type A struct {
		Aint int
		*B
		*C
	}
	av := &dynamodb.AttributeValue{
		M: map[string]*dynamodb.AttributeValue{
			"Aint": {
				N: aws.String("321"),
			},
			"Bint": {
				N: aws.String("123"),
			},
		},
	}
	decoder := NewDecoder()
	a := A{}
	err := decoder.Decode(av, &a)
	assert.NoError(t, err)
	assert.Equal(t, 321, a.Aint)
	// Embedded pointer struct can be created automatically.
	assert.Equal(t, 123, a.Bint)
	// But not for absent fields.
	assert.Nil(t, a.C)
}

func TestDecodeBooleanOverlay(t *testing.T) {
	type BooleanOverlay bool

	av := &dynamodb.AttributeValue{
		BOOL: aws.Bool(true),
	}

	decoder := NewDecoder()

	var v BooleanOverlay

	err := decoder.Decode(av, &v)
	assert.NoError(t, err)
	assert.Equal(t, BooleanOverlay(true), v)
}

func TestDecodeUnixTime(t *testing.T) {
	type A struct {
		Normal time.Time
		Tagged time.Time `dynamodbav:",unixtime"`
		Typed  UnixTime
	}

	expect := A{
		Normal: time.Unix(123, 0).UTC(),
		Tagged: time.Unix(456, 0),
		Typed:  UnixTime(time.Unix(789, 0)),
	}

	input := &dynamodb.AttributeValue{
		M: map[string]*dynamodb.AttributeValue{
			"Normal": {
				S: aws.String("1970-01-01T00:02:03Z"),
			},
			"Tagged": {
				N: aws.String("456"),
			},
			"Typed": {
				N: aws.String("789"),
			},
		},
	}
	actual := A{}

	err := Unmarshal(input, &actual)
	assert.NoError(t, err)
	assert.Equal(t, expect, actual)
}

func TestDecodeAliasedUnixTime(t *testing.T) {
	type A struct {
		Normal AliasedTime
		Tagged AliasedTime `dynamodbav:",unixtime"`
	}

	expect := A{
		Normal: AliasedTime(time.Unix(123, 0).UTC()),
		Tagged: AliasedTime(time.Unix(456, 0)),
	}

	input := &dynamodb.AttributeValue{
		M: map[string]*dynamodb.AttributeValue{
			"Normal": {
				S: aws.String("1970-01-01T00:02:03Z"),
			},
			"Tagged": {
				N: aws.String("456"),
			},
		},
	}
	actual := A{}

	err := Unmarshal(input, &actual)
	assert.NoError(t, err)
	assert.Equal(t, expect, actual)
}
