/*
Copyright 2017 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 spanner

import (
	"math"
	"reflect"
	"testing"
	"time"

	"cloud.google.com/go/civil"
	proto3 "github.com/golang/protobuf/ptypes/struct"
	sppb "google.golang.org/genproto/googleapis/spanner/v1"
)

var (
	t1 = mustParseTime("2016-11-15T15:04:05.999999999Z")
	// Boundaries
	t2 = mustParseTime("0000-01-01T00:00:00.000000000Z")
	t3 = mustParseTime("9999-12-31T23:59:59.999999999Z")
	// Local timezone
	t4 = time.Now()
	d1 = mustParseDate("2016-11-15")
	d2 = mustParseDate("1678-01-01")
)

func mustParseTime(s string) time.Time {
	t, err := time.Parse(time.RFC3339Nano, s)
	if err != nil {
		panic(err)
	}
	return t
}

func mustParseDate(s string) civil.Date {
	d, err := civil.ParseDate(s)
	if err != nil {
		panic(err)
	}
	return d
}

// Test encoding Values.
func TestEncodeValue(t *testing.T) {
	var (
		tString = stringType()
		tInt    = intType()
		tBool   = boolType()
		tFloat  = floatType()
		tBytes  = bytesType()
		tTime   = timeType()
		tDate   = dateType()
	)
	for i, test := range []struct {
		in       interface{}
		want     *proto3.Value
		wantType *sppb.Type
	}{
		// STRING / STRING ARRAY
		{"abc", stringProto("abc"), tString},
		{NullString{"abc", true}, stringProto("abc"), tString},
		{NullString{"abc", false}, nullProto(), tString},
		{[]string(nil), nullProto(), listType(tString)},
		{[]string{"abc", "bcd"}, listProto(stringProto("abc"), stringProto("bcd")), listType(tString)},
		{[]NullString{{"abcd", true}, {"xyz", false}}, listProto(stringProto("abcd"), nullProto()), listType(tString)},
		// BYTES / BYTES ARRAY
		{[]byte("foo"), bytesProto([]byte("foo")), tBytes},
		{[]byte(nil), nullProto(), tBytes},
		{[][]byte{nil, []byte("ab")}, listProto(nullProto(), bytesProto([]byte("ab"))), listType(tBytes)},
		{[][]byte(nil), nullProto(), listType(tBytes)},
		// INT64 / INT64 ARRAY
		{7, intProto(7), tInt},
		{[]int(nil), nullProto(), listType(tInt)},
		{[]int{31, 127}, listProto(intProto(31), intProto(127)), listType(tInt)},
		{int64(81), intProto(81), tInt},
		{[]int64(nil), nullProto(), listType(tInt)},
		{[]int64{33, 129}, listProto(intProto(33), intProto(129)), listType(tInt)},
		{NullInt64{11, true}, intProto(11), tInt},
		{NullInt64{11, false}, nullProto(), tInt},
		{[]NullInt64{{35, true}, {131, false}}, listProto(intProto(35), nullProto()), listType(tInt)},
		// BOOL / BOOL ARRAY
		{true, boolProto(true), tBool},
		{NullBool{true, true}, boolProto(true), tBool},
		{NullBool{true, false}, nullProto(), tBool},
		{[]bool{true, false}, listProto(boolProto(true), boolProto(false)), listType(tBool)},
		{[]NullBool{{true, true}, {true, false}}, listProto(boolProto(true), nullProto()), listType(tBool)},
		// FLOAT64 / FLOAT64 ARRAY
		{3.14, floatProto(3.14), tFloat},
		{NullFloat64{3.1415, true}, floatProto(3.1415), tFloat},
		{NullFloat64{math.Inf(1), true}, floatProto(math.Inf(1)), tFloat},
		{NullFloat64{3.14159, false}, nullProto(), tFloat},
		{[]float64(nil), nullProto(), listType(tFloat)},
		{[]float64{3.141, 0.618, math.Inf(-1)}, listProto(floatProto(3.141), floatProto(0.618), floatProto(math.Inf(-1))), listType(tFloat)},
		{[]NullFloat64{{3.141, true}, {0.618, false}}, listProto(floatProto(3.141), nullProto()), listType(tFloat)},
		// TIMESTAMP / TIMESTAMP ARRAY
		{t1, timeProto(t1), tTime},
		{NullTime{t1, true}, timeProto(t1), tTime},
		{NullTime{t1, false}, nullProto(), tTime},
		{[]time.Time(nil), nullProto(), listType(tTime)},
		{[]time.Time{t1, t2, t3, t4}, listProto(timeProto(t1), timeProto(t2), timeProto(t3), timeProto(t4)), listType(tTime)},
		{[]NullTime{{t1, true}, {t1, false}}, listProto(timeProto(t1), nullProto()), listType(tTime)},
		// DATE / DATE ARRAY
		{d1, dateProto(d1), tDate},
		{NullDate{d1, true}, dateProto(d1), tDate},
		{NullDate{civil.Date{}, false}, nullProto(), tDate},
		{[]civil.Date(nil), nullProto(), listType(tDate)},
		{[]civil.Date{d1, d2}, listProto(dateProto(d1), dateProto(d2)), listType(tDate)},
		{[]NullDate{{d1, true}, {civil.Date{}, false}}, listProto(dateProto(d1), nullProto()), listType(tDate)},
		// GenericColumnValue
		{GenericColumnValue{tString, stringProto("abc")}, stringProto("abc"), tString},
		{GenericColumnValue{tString, nullProto()}, nullProto(), tString},
		// not actually valid (stringProto inside int list), but demonstrates pass-through.
		{
			GenericColumnValue{
				Type:  listType(tInt),
				Value: listProto(intProto(5), nullProto(), stringProto("bcd")),
			},
			listProto(intProto(5), nullProto(), stringProto("bcd")),
			listType(tInt),
		},
		// placeholder
		{CommitTimestamp, stringProto(commitTimestampPlaceholderString), tTime},
	} {
		got, gotType, err := encodeValue(test.in)
		if err != nil {
			t.Fatalf("#%d: got error during encoding: %v, want nil", i, err)
		}
		if !testEqual(got, test.want) {
			t.Errorf("#%d: got encode result: %v, want %v", i, got, test.want)
		}
		if !testEqual(gotType, test.wantType) {
			t.Errorf("#%d: got encode type: %v, want %v", i, gotType, test.wantType)
		}
	}
}

// Test decoding Values.
func TestDecodeValue(t *testing.T) {
	for i, test := range []struct {
		in   *proto3.Value
		t    *sppb.Type
		want interface{}
		fail bool
	}{
		// STRING
		{stringProto("abc"), stringType(), "abc", false},
		{nullProto(), stringType(), "abc", true},
		{stringProto("abc"), stringType(), NullString{"abc", true}, false},
		{nullProto(), stringType(), NullString{}, false},
		// STRING ARRAY with []NullString
		{
			listProto(stringProto("abc"), nullProto(), stringProto("bcd")),
			listType(stringType()),
			[]NullString{{"abc", true}, {}, {"bcd", true}},
			false,
		},
		{nullProto(), listType(stringType()), []NullString(nil), false},
		// STRING ARRAY with []string
		{
			listProto(stringProto("abc"), stringProto("bcd")),
			listType(stringType()),
			[]string{"abc", "bcd"},
			false,
		},
		// BYTES
		{bytesProto([]byte("ab")), bytesType(), []byte("ab"), false},
		{nullProto(), bytesType(), []byte(nil), false},
		// BYTES ARRAY
		{listProto(bytesProto([]byte("ab")), nullProto()), listType(bytesType()), [][]byte{[]byte("ab"), nil}, false},
		{nullProto(), listType(bytesType()), [][]byte(nil), false},
		//INT64
		{intProto(15), intType(), int64(15), false},
		{nullProto(), intType(), int64(0), true},
		{intProto(15), intType(), NullInt64{15, true}, false},
		{nullProto(), intType(), NullInt64{}, false},
		// INT64 ARRAY with []NullInt64
		{listProto(intProto(91), nullProto(), intProto(87)), listType(intType()), []NullInt64{{91, true}, {}, {87, true}}, false},
		{nullProto(), listType(intType()), []NullInt64(nil), false},
		// INT64 ARRAY with []int64
		{listProto(intProto(91), intProto(87)), listType(intType()), []int64{91, 87}, false},
		// BOOL
		{boolProto(true), boolType(), true, false},
		{nullProto(), boolType(), true, true},
		{boolProto(true), boolType(), NullBool{true, true}, false},
		{nullProto(), boolType(), NullBool{}, false},
		// BOOL ARRAY with []NullBool
		{listProto(boolProto(true), boolProto(false), nullProto()), listType(boolType()), []NullBool{{true, true}, {false, true}, {}}, false},
		{nullProto(), listType(boolType()), []NullBool(nil), false},
		// BOOL ARRAY with []bool
		{listProto(boolProto(true), boolProto(false)), listType(boolType()), []bool{true, false}, false},
		// FLOAT64
		{floatProto(3.14), floatType(), 3.14, false},
		{nullProto(), floatType(), 0.00, true},
		{floatProto(3.14), floatType(), NullFloat64{3.14, true}, false},
		{nullProto(), floatType(), NullFloat64{}, false},
		// FLOAT64 ARRAY with []NullFloat64
		{
			listProto(floatProto(math.Inf(1)), floatProto(math.Inf(-1)), nullProto(), floatProto(3.1)),
			listType(floatType()),
			[]NullFloat64{{math.Inf(1), true}, {math.Inf(-1), true}, {}, {3.1, true}},
			false,
		},
		{nullProto(), listType(floatType()), []NullFloat64(nil), false},
		// FLOAT64 ARRAY with []float64
		{
			listProto(floatProto(math.Inf(1)), floatProto(math.Inf(-1)), floatProto(3.1)),
			listType(floatType()),
			[]float64{math.Inf(1), math.Inf(-1), 3.1},
			false,
		},
		// TIMESTAMP
		{timeProto(t1), timeType(), t1, false},
		{timeProto(t1), timeType(), NullTime{t1, true}, false},
		{nullProto(), timeType(), NullTime{}, false},
		// TIMESTAMP ARRAY with []NullTime
		{listProto(timeProto(t1), timeProto(t2), timeProto(t3), nullProto()), listType(timeType()), []NullTime{{t1, true}, {t2, true}, {t3, true}, {}}, false},
		{nullProto(), listType(timeType()), []NullTime(nil), false},
		// TIMESTAMP ARRAY with []time.Time
		{listProto(timeProto(t1), timeProto(t2), timeProto(t3)), listType(timeType()), []time.Time{t1, t2, t3}, false},
		// DATE
		{dateProto(d1), dateType(), d1, false},
		{dateProto(d1), dateType(), NullDate{d1, true}, false},
		{nullProto(), dateType(), NullDate{}, false},
		// DATE ARRAY with []NullDate
		{listProto(dateProto(d1), dateProto(d2), nullProto()), listType(dateType()), []NullDate{{d1, true}, {d2, true}, {}}, false},
		{nullProto(), listType(dateType()), []NullDate(nil), false},
		// DATE ARRAY with []civil.Date
		{listProto(dateProto(d1), dateProto(d2)), listType(dateType()), []civil.Date{d1, d2}, false},
		// STRUCT ARRAY
		// STRUCT schema is equal to the following Go struct:
		// type s struct {
		//     Col1 NullInt64
		//     Col2 []struct {
		//         SubCol1 float64
		//         SubCol2 string
		//     }
		// }
		{
			in: listProto(
				listProto(
					intProto(3),
					listProto(
						listProto(floatProto(3.14), stringProto("this")),
						listProto(floatProto(0.57), stringProto("siht")),
					),
				),
				listProto(
					nullProto(),
					nullProto(),
				),
				nullProto(),
			),
			t: listType(
				structType(
					mkField("Col1", intType()),
					mkField(
						"Col2",
						listType(
							structType(
								mkField("SubCol1", floatType()),
								mkField("SubCol2", stringType()),
							),
						),
					),
				),
			),
			want: []NullRow{
				{
					Row: Row{
						fields: []*sppb.StructType_Field{
							mkField("Col1", intType()),
							mkField(
								"Col2",
								listType(
									structType(
										mkField("SubCol1", floatType()),
										mkField("SubCol2", stringType()),
									),
								),
							),
						},
						vals: []*proto3.Value{
							intProto(3),
							listProto(
								listProto(floatProto(3.14), stringProto("this")),
								listProto(floatProto(0.57), stringProto("siht")),
							),
						},
					},
					Valid: true,
				},
				{
					Row: Row{
						fields: []*sppb.StructType_Field{
							mkField("Col1", intType()),
							mkField(
								"Col2",
								listType(
									structType(
										mkField("SubCol1", floatType()),
										mkField("SubCol2", stringType()),
									),
								),
							),
						},
						vals: []*proto3.Value{
							nullProto(),
							nullProto(),
						},
					},
					Valid: true,
				},
				{},
			},
			fail: false,
		},
		{
			in: listProto(
				listProto(
					intProto(3),
					listProto(
						listProto(floatProto(3.14), stringProto("this")),
						listProto(floatProto(0.57), stringProto("siht")),
					),
				),
				listProto(
					nullProto(),
					nullProto(),
				),
				nullProto(),
			),
			t: listType(
				structType(
					mkField("Col1", intType()),
					mkField(
						"Col2",
						listType(
							structType(
								mkField("SubCol1", floatType()),
								mkField("SubCol2", stringType()),
							),
						),
					),
				),
			),
			want: []*struct {
				Col1      NullInt64
				StructCol []*struct {
					SubCol1 NullFloat64
					SubCol2 string
				} `spanner:"Col2"`
			}{
				{
					Col1: NullInt64{3, true},
					StructCol: []*struct {
						SubCol1 NullFloat64
						SubCol2 string
					}{
						{
							SubCol1: NullFloat64{3.14, true},
							SubCol2: "this",
						},
						{
							SubCol1: NullFloat64{0.57, true},
							SubCol2: "siht",
						},
					},
				},
				{
					Col1: NullInt64{},
					StructCol: []*struct {
						SubCol1 NullFloat64
						SubCol2 string
					}(nil),
				},
				nil,
			},
			fail: false,
		},
		// GenericColumnValue
		{stringProto("abc"), stringType(), GenericColumnValue{stringType(), stringProto("abc")}, false},
		{nullProto(), stringType(), GenericColumnValue{stringType(), nullProto()}, false},
		// not actually valid (stringProto inside int list), but demonstrates pass-through.
		{
			in: listProto(intProto(5), nullProto(), stringProto("bcd")),
			t:  listType(intType()),
			want: GenericColumnValue{
				Type:  listType(intType()),
				Value: listProto(intProto(5), nullProto(), stringProto("bcd")),
			},
			fail: false,
		},
	} {
		gotp := reflect.New(reflect.TypeOf(test.want))
		if err := decodeValue(test.in, test.t, gotp.Interface()); err != nil {
			if !test.fail {
				t.Errorf("%d: cannot decode %v(%v): %v", i, test.in, test.t, err)
			}
			continue
		}
		if test.fail {
			t.Errorf("%d: decoding %v(%v) succeeds unexpectedly, want error", i, test.in, test.t)
			continue
		}
		got := reflect.Indirect(gotp).Interface()
		if !testEqual(got, test.want) {
			t.Errorf("%d: unexpected decoding result - got %v, want %v", i, got, test.want)
			continue
		}
	}
}

// Test error cases for decodeValue.
func TestDecodeValueErrors(t *testing.T) {
	for i, test := range []struct {
		in *proto3.Value
		t  *sppb.Type
		v  interface{}
	}{
		{nullProto(), stringType(), nil},
		{nullProto(), stringType(), 1},
	} {
		err := decodeValue(test.in, test.t, test.v)
		if err == nil {
			t.Errorf("#%d: want error, got nil", i)
		}
	}
}

// Test NaN encoding/decoding.
func TestNaN(t *testing.T) {
	// Decode NaN value.
	f := 0.0
	nf := NullFloat64{}
	// To float64
	if err := decodeValue(floatProto(math.NaN()), floatType(), &f); err != nil {
		t.Errorf("decodeValue returns %q for %v, want nil", err, floatProto(math.NaN()))
	}
	if !math.IsNaN(f) {
		t.Errorf("f = %v, want %v", f, math.NaN())
	}
	// To NullFloat64
	if err := decodeValue(floatProto(math.NaN()), floatType(), &nf); err != nil {
		t.Errorf("decodeValue returns %q for %v, want nil", err, floatProto(math.NaN()))
	}
	if !math.IsNaN(nf.Float64) || !nf.Valid {
		t.Errorf("f = %v, want %v", f, NullFloat64{math.NaN(), true})
	}
	// Encode NaN value
	// From float64
	v, _, err := encodeValue(math.NaN())
	if err != nil {
		t.Errorf("encodeValue returns %q for NaN, want nil", err)
	}
	x, ok := v.GetKind().(*proto3.Value_NumberValue)
	if !ok {
		t.Errorf("incorrect type for v.GetKind(): %T, want *proto3.Value_NumberValue", v.GetKind())
	}
	if !math.IsNaN(x.NumberValue) {
		t.Errorf("x.NumberValue = %v, want %v", x.NumberValue, math.NaN())
	}
	// From NullFloat64
	v, _, err = encodeValue(NullFloat64{math.NaN(), true})
	if err != nil {
		t.Errorf("encodeValue returns %q for NaN, want nil", err)
	}
	x, ok = v.GetKind().(*proto3.Value_NumberValue)
	if !ok {
		t.Errorf("incorrect type for v.GetKind(): %T, want *proto3.Value_NumberValue", v.GetKind())
	}
	if !math.IsNaN(x.NumberValue) {
		t.Errorf("x.NumberValue = %v, want %v", x.NumberValue, math.NaN())
	}
}

func TestGenericColumnValue(t *testing.T) {
	for _, test := range []struct {
		in   GenericColumnValue
		want interface{}
		fail bool
	}{
		{GenericColumnValue{stringType(), stringProto("abc")}, "abc", false},
		{GenericColumnValue{stringType(), stringProto("abc")}, 5, true},
		{GenericColumnValue{listType(intType()), listProto(intProto(91), nullProto(), intProto(87))}, []NullInt64{{91, true}, {}, {87, true}}, false},
		{GenericColumnValue{intType(), intProto(42)}, GenericColumnValue{intType(), intProto(42)}, false}, // trippy! :-)
	} {
		gotp := reflect.New(reflect.TypeOf(test.want))
		if err := test.in.Decode(gotp.Interface()); err != nil {
			if !test.fail {
				t.Errorf("cannot decode %v to %v: %v", test.in, test.want, err)
			}
			continue
		}
		if test.fail {
			t.Errorf("decoding %v to %v succeeds unexpectedly", test.in, test.want)
		}

		// Test we can go backwards as well.
		v, err := newGenericColumnValue(test.want)
		if err != nil {
			t.Errorf("NewGenericColumnValue failed: %v", err)
			continue
		}
		if !testEqual(*v, test.in) {
			t.Errorf("unexpected encode result - got %v, want %v", v, test.in)
		}
	}
}
