// Copyright 2017 Google LLC
//
// 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 firestore

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

	ts "github.com/golang/protobuf/ptypes/timestamp"
	pb "google.golang.org/genproto/googleapis/firestore/v1beta1"

	"google.golang.org/genproto/googleapis/type/latlng"
)

type testStruct1 struct {
	B  bool
	I  int
	U  uint32
	F  float64
	S  string
	Y  []byte
	T  time.Time
	Ts *ts.Timestamp
	G  *latlng.LatLng
	L  []int
	M  map[string]int
	P  *int
}

var (
	p = new(int)

	testVal1 = testStruct1{
		B:  true,
		I:  1,
		U:  2,
		F:  3.0,
		S:  "four",
		Y:  []byte{5},
		T:  tm,
		Ts: ptm,
		G:  ll,
		L:  []int{6},
		M:  map[string]int{"a": 7},
		P:  p,
	}

	mapVal1 = mapval(map[string]*pb.Value{
		"B":  boolval(true),
		"I":  intval(1),
		"U":  intval(2),
		"F":  floatval(3),
		"S":  {ValueType: &pb.Value_StringValue{"four"}},
		"Y":  bytesval([]byte{5}),
		"T":  tsval(tm),
		"Ts": {ValueType: &pb.Value_TimestampValue{ptm}},
		"G":  geoval(ll),
		"L":  arrayval(intval(6)),
		"M":  mapval(map[string]*pb.Value{"a": intval(7)}),
		"P":  intval(8),
	})
)

func TestToProtoValue(t *testing.T) {
	*p = 8
	for _, test := range []struct {
		in   interface{}
		want *pb.Value
	}{
		{nil, nullValue},
		{[]int(nil), nullValue},
		{map[string]int(nil), nullValue},
		{(*testStruct1)(nil), nullValue},
		{(*ts.Timestamp)(nil), nullValue},
		{(*latlng.LatLng)(nil), nullValue},
		{(*DocumentRef)(nil), nullValue},
		{true, boolval(true)},
		{3, intval(3)},
		{uint32(3), intval(3)},
		{1.5, floatval(1.5)},
		{"str", strval("str")},
		{[]byte{1, 2}, bytesval([]byte{1, 2})},
		{tm, tsval(tm)},
		{ptm, &pb.Value{ValueType: &pb.Value_TimestampValue{ptm}}},
		{ll, geoval(ll)},
		{[]int{1, 2}, arrayval(intval(1), intval(2))},
		{&[]int{1, 2}, arrayval(intval(1), intval(2))},
		{[]int{}, arrayval()},
		{map[string]int{"a": 1, "b": 2},
			mapval(map[string]*pb.Value{"a": intval(1), "b": intval(2)})},
		{map[string]int{}, mapval(map[string]*pb.Value{})},
		{p, intval(8)},
		{&p, intval(8)},
		{map[string]interface{}{"a": 1, "p": p, "s": "str"},
			mapval(map[string]*pb.Value{"a": intval(1), "p": intval(8), "s": strval("str")})},
		{map[string]fmt.Stringer{"a": tm},
			mapval(map[string]*pb.Value{"a": tsval(tm)})},
		{testVal1, mapVal1},
		{
			&DocumentRef{
				ID:   "d",
				Path: "projects/P/databases/D/documents/c/d",
				Parent: &CollectionRef{
					ID:         "c",
					parentPath: "projects/P/databases/D",
					Path:       "projects/P/databases/D/documents/c",
					Query:      Query{collectionID: "c", parentPath: "projects/P/databases/D"},
				},
			},
			refval("projects/P/databases/D/documents/c/d"),
		},
		// ServerTimestamps are removed, possibly leaving nil.
		{map[string]interface{}{"a": ServerTimestamp}, nil},
		{
			map[string]interface{}{
				"a": map[string]interface{}{
					"b": map[string]interface{}{
						"c": ServerTimestamp,
					},
				},
			},
			nil,
		},
		{
			map[string]interface{}{
				"a": map[string]interface{}{
					"b": map[string]interface{}{
						"c": ServerTimestamp,
						"d": ServerTimestamp,
					},
				},
			},
			nil,
		},
		{
			map[string]interface{}{
				"a": map[string]interface{}{
					"b": map[string]interface{}{
						"c": ServerTimestamp,
						"d": ServerTimestamp,
						"e": 1,
					},
				},
			},
			mapval(map[string]*pb.Value{
				"a": mapval(map[string]*pb.Value{
					"b": mapval(map[string]*pb.Value{"e": intval(1)}),
				}),
			}),
		},
	} {
		got, _, err := toProtoValue(reflect.ValueOf(test.in))
		if err != nil {
			t.Errorf("%v (%T): %v", test.in, test.in, err)
			continue
		}
		if !testEqual(got, test.want) {
			t.Errorf("%+v (%T):\ngot\n%+v\nwant\n%+v", test.in, test.in, got, test.want)
		}
	}
}

type stringy struct{}

func (stringy) String() string { return "stringy" }

func TestToProtoValueErrors(t *testing.T) {
	for _, in := range []interface{}{
		uint64(0),                               // a bad fit for int64
		map[int]bool{},                          // map key type is not string
		make(chan int),                          // can't handle type
		map[string]fmt.Stringer{"a": stringy{}}, // only empty interfaces
		ServerTimestamp,                         // ServerTimestamp can only be a field value
		[]interface{}{ServerTimestamp},
		map[string]interface{}{"a": []interface{}{ServerTimestamp}},
		map[string]interface{}{"a": []interface{}{
			map[string]interface{}{"b": ServerTimestamp},
		}},
		Delete, // Delete should never appear
		[]interface{}{Delete},
		map[string]interface{}{"a": Delete},
		map[string]interface{}{"a": []interface{}{Delete}},
	} {
		_, _, err := toProtoValue(reflect.ValueOf(in))
		if err == nil {
			t.Errorf("%v: got nil, want error", in)
		}
	}
}

type testStruct2 struct {
	Ignore        int       `firestore:"-"`
	Rename        int       `firestore:"a"`
	OmitEmpty     int       `firestore:",omitempty"`
	OmitEmptyTime time.Time `firestore:",omitempty"`
}

func TestToProtoValueTags(t *testing.T) {
	in := &testStruct2{
		Ignore:        1,
		Rename:        2,
		OmitEmpty:     3,
		OmitEmptyTime: aTime,
	}
	got, _, err := toProtoValue(reflect.ValueOf(in))
	if err != nil {
		t.Fatal(err)
	}
	want := mapval(map[string]*pb.Value{
		"a":             intval(2),
		"OmitEmpty":     intval(3),
		"OmitEmptyTime": tsval(aTime),
	})
	if !testEqual(got, want) {
		t.Errorf("got %+v, want %+v", got, want)
	}

	got, _, err = toProtoValue(reflect.ValueOf(testStruct2{}))
	if err != nil {
		t.Fatal(err)
	}
	want = mapval(map[string]*pb.Value{"a": intval(0)})
	if !testEqual(got, want) {
		t.Errorf("got\n%+v\nwant\n%+v", got, want)
	}
}

func TestToProtoValueEmbedded(t *testing.T) {
	// Embedded time.Time, LatLng, or Timestamp should behave like non-embedded.
	type embed struct {
		time.Time
		*latlng.LatLng
		*ts.Timestamp
	}

	got, _, err := toProtoValue(reflect.ValueOf(embed{tm, ll, ptm}))
	if err != nil {
		t.Fatal(err)
	}
	want := mapval(map[string]*pb.Value{
		"Time":      tsval(tm),
		"LatLng":    geoval(ll),
		"Timestamp": {ValueType: &pb.Value_TimestampValue{ptm}},
	})
	if !testEqual(got, want) {
		t.Errorf("got %+v, want %+v", got, want)
	}
}

func TestIsEmpty(t *testing.T) {
	for _, e := range []interface{}{int(0), float32(0), false, "", []int{}, []int(nil), (*int)(nil)} {
		if !isEmptyValue(reflect.ValueOf(e)) {
			t.Errorf("%v (%T): want true, got false", e, e)
		}
	}
	i := 3
	for _, n := range []interface{}{int(1), float32(1), true, "x", []int{1}, &i} {
		if isEmptyValue(reflect.ValueOf(n)) {
			t.Errorf("%v (%T): want false, got true", n, n)
		}
	}
}
