// Copyright 2019 The Go Cloud Development Kit Authors
//
// 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 fields

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
)

type embed1 struct {
	Em1    int
	Dup    int // annihilates with embed2.Dup
	Shadow int
	embed3
}

type embed2 struct {
	Dup int
	embed3
	embed4
}

type embed3 struct {
	Em3 int // annihilated because embed3 is in both embed1 and embed2
	embed5
}

type embed4 struct {
	Em4     int
	Dup     int // annihilation of Dup in embed1, embed2 hides this Dup
	*embed1     // ignored because it occurs at a higher level
}

type embed5 struct {
	x int
}

type Anonymous int

type S1 struct {
	Exported   int
	unexported int
	Shadow     int // shadows S1.Shadow
	embed1
	*embed2
	Anonymous
}

type Time struct {
	time.Time
}

var intType = reflect.TypeOf(int(0))

func field(name string, tval interface{}, index ...int) *Field {
	return &Field{
		Name:      name,
		Type:      reflect.TypeOf(tval),
		Index:     index,
		ParsedTag: []string(nil),
	}
}

func tfield(name string, tval interface{}, index ...int) *Field {
	return &Field{
		Name:        name,
		Type:        reflect.TypeOf(tval),
		Index:       index,
		NameFromTag: true,
		ParsedTag:   []string(nil),
	}
}

func TestFieldsNoTags(t *testing.T) {
	c := NewCache(nil, nil, nil)
	got, err := c.Fields(reflect.TypeOf(S1{}))
	if err != nil {
		t.Fatal(err)
	}
	want := []*Field{
		field("Exported", int(0), 0),
		field("Shadow", int(0), 2),
		field("Em1", int(0), 3, 0),
		field("Em4", int(0), 4, 2, 0),
		field("Anonymous", Anonymous(0), 5),
	}
	for _, f := range want {
		f.ParsedTag = nil
	}
	if msg, ok := compareFields(got, want); !ok {
		t.Error(msg)
	}
}

func TestAgainstJSONEncodingNoTags(t *testing.T) {
	// Demonstrates that this package produces the same set of fields as encoding/json.
	s1 := S1{
		Exported:   1,
		unexported: 2,
		Shadow:     3,
		embed1: embed1{
			Em1:    4,
			Dup:    5,
			Shadow: 6,
			embed3: embed3{
				Em3:    7,
				embed5: embed5{x: 8},
			},
		},
		embed2: &embed2{
			Dup: 9,
			embed3: embed3{
				Em3:    10,
				embed5: embed5{x: 11},
			},
			embed4: embed4{
				Em4:    12,
				Dup:    13,
				embed1: &embed1{Em1: 14},
			},
		},
		Anonymous: Anonymous(15),
	}
	var want S1
	want.embed2 = &embed2{} // need this because reflection won't create it
	jsonRoundTrip(t, s1, &want)
	var got S1
	got.embed2 = &embed2{}
	fields, err := NewCache(nil, nil, nil).Fields(reflect.TypeOf(got))
	if err != nil {
		t.Fatal(err)
	}
	setFields(fields, &got, s1)
	if !cmp.Equal(got, want,
		cmp.AllowUnexported(S1{}, embed1{}, embed2{}, embed3{}, embed4{}, embed5{})) {
		t.Errorf("got\n%+v\nwant\n%+v", got, want)
	}
}

// Tests use of LeafTypes parameter to NewCache
func TestAgainstJSONEncodingEmbeddedTime(t *testing.T) {
	timeLeafFn := func(t reflect.Type) bool {
		return t == reflect.TypeOf(time.Time{})
	}
	// Demonstrates that this package can produce the same set of
	// fields as encoding/json for a struct with an embedded time.Time.
	now := time.Now().UTC()
	myt := Time{
		now,
	}
	var want Time
	jsonRoundTrip(t, myt, &want)
	var got Time
	fields, err := NewCache(nil, nil, timeLeafFn).Fields(reflect.TypeOf(got))
	if err != nil {
		t.Fatal(err)
	}
	setFields(fields, &got, myt)
	if !cmp.Equal(got, want) {
		t.Errorf("got\n%+v\nwant\n%+v", got, want)
	}
}

type S2 struct {
	NoTag      int
	XXX        int           `json:"tag"` // tag name takes precedence
	Anonymous  `json:"anon"` // anonymous non-structs also get their name from the tag
	unexported int           `json:"tag"`
	Embed      `json:"em"`   // embedded structs with tags become fields
	Tag        int
	YYY        int `json:"Tag"` // tag takes precedence over untagged field of the same name
	Empty      int `json:""`    // empty tag is noop
	tEmbed1
	tEmbed2
}

type Embed struct {
	Em int
}

type tEmbed1 struct {
	Dup int
	X   int `json:"Dup2"`
}

type tEmbed2 struct {
	Y int `json:"Dup"`  // takes precedence over tEmbed1.Dup because it is tagged
	Z int `json:"Dup2"` // same name as tEmbed1.X and both tagged, so ignored
}

func jsonTagParser(t reflect.StructTag) (name string, keep bool, other interface{}, err error) {
	n, k, o := ParseStandardTag("json", t)
	return n, k, o, nil
}

func validateFunc(t reflect.Type) (err error) {
	if t.Kind() != reflect.Struct {
		return errors.New("non-struct type used")
	}

	for i := 0; i < t.NumField(); i++ {
		if t.Field(i).Type.Kind() == reflect.Slice {
			return fmt.Errorf("slice field found at field %s on struct %s", t.Field(i).Name, t.Name())
		}
	}

	return nil
}

func TestFieldsWithTags(t *testing.T) {
	got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S2{}))
	if err != nil {
		t.Fatal(err)
	}
	want := []*Field{
		field("NoTag", int(0), 0),
		tfield("tag", int(0), 1),
		tfield("anon", Anonymous(0), 2),
		tfield("em", Embed{}, 4),
		tfield("Tag", int(0), 6),
		field("Empty", int(0), 7),
		tfield("Dup", int(0), 8, 0),
	}
	if msg, ok := compareFields(got, want); !ok {
		t.Error(msg)
	}
}

func TestAgainstJSONEncodingWithTags(t *testing.T) {
	// Demonstrates that this package produces the same set of fields as encoding/json.
	s2 := S2{
		NoTag:     1,
		XXX:       2,
		Anonymous: 3,
		Embed: Embed{
			Em: 4,
		},
		tEmbed1: tEmbed1{
			Dup: 5,
			X:   6,
		},
		tEmbed2: tEmbed2{
			Y: 7,
			Z: 8,
		},
	}
	var want S2
	jsonRoundTrip(t, s2, &want)
	var got S2
	fields, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(got))
	if err != nil {
		t.Fatal(err)
	}
	setFields(fields, &got, s2)
	if !cmp.Equal(got, want, cmp.AllowUnexported(S2{})) {
		t.Errorf("got\n%+v\nwant\n%+v", got, want)
	}
}

func TestUnexportedAnonymousNonStruct(t *testing.T) {
	// An unexported anonymous non-struct field should not be recorded.
	// This is currently a bug in encoding/json.
	// https://github.com/golang/go/issues/18009
	type (
		u int
		v int
		S struct {
			u
			v `json:"x"`
			int
		}
	)

	got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S{}))
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 0 {
		t.Errorf("got %d fields, want 0", len(got))
	}
}

func TestUnexportedAnonymousStruct(t *testing.T) {
	// An unexported anonymous struct with a tag is ignored.
	// This is currently a bug in encoding/json.
	// https://github.com/golang/go/issues/18009
	type (
		s1 struct{ X int }
		S2 struct {
			s1 `json:"Y"`
		}
	)
	got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S2{}))
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 0 {
		t.Errorf("got %d fields, want 0", len(got))
	}
}

func TestDominantField(t *testing.T) {
	// With fields sorted by index length and then by tag presence,
	// the dominant field is always the first. Make sure all error
	// cases are caught.
	for _, test := range []struct {
		fields []Field
		wantOK bool
	}{
		// A single field is OK.
		{[]Field{{Index: []int{0}}}, true},
		{[]Field{{Index: []int{0}, NameFromTag: true}}, true},
		// A single field at top level is OK.
		{[]Field{{Index: []int{0}}, {Index: []int{1, 0}}}, true},
		{[]Field{{Index: []int{0}}, {Index: []int{1, 0}, NameFromTag: true}}, true},
		{[]Field{{Index: []int{0}, NameFromTag: true}, {Index: []int{1, 0}, NameFromTag: true}}, true},
		// A single tagged field is OK.
		{[]Field{{Index: []int{0}, NameFromTag: true}, {Index: []int{1}}}, true},
		// Two untagged fields at the same level is an error.
		{[]Field{{Index: []int{0}}, {Index: []int{1}}}, false},
		// Two tagged fields at the same level is an error.
		{[]Field{{Index: []int{0}, NameFromTag: true}, {Index: []int{1}, NameFromTag: true}}, false},
	} {
		_, gotOK := dominantField(test.fields)
		if gotOK != test.wantOK {
			t.Errorf("%v: got %t, want %t", test.fields, gotOK, test.wantOK)
		}
	}
}

func TestIgnore(t *testing.T) {
	type S struct {
		X int `json:"-"`
		Y int `json:"-,"` // field with name "-"
	}
	got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S{}))
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 1 {
		t.Errorf("got %d fields, want 1", len(got))
	}
}

func TestParsedTag(t *testing.T) {
	type S struct {
		X int `json:"name,omitempty"`
	}
	got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S{}))
	if err != nil {
		t.Fatal(err)
	}
	want := []*Field{
		{
			Name: "name", NameFromTag: true, Type: intType,
			Index: []int{0}, ParsedTag: []string{"omitempty"},
		},
	}
	if msg, ok := compareFields(got, want); !ok {
		t.Error(msg)
	}
}

func TestValidateFunc(t *testing.T) {
	type MyInvalidStruct struct {
		A string
		B []int
	}

	_, err := NewCache(nil, validateFunc, nil).Fields(reflect.TypeOf(MyInvalidStruct{}))
	if err == nil {
		t.Fatal("expected error, got nil")
	}

	type MyValidStruct struct {
		A string
		B int
	}
	_, err = NewCache(nil, validateFunc, nil).Fields(reflect.TypeOf(MyValidStruct{}))
	if err != nil {
		t.Fatalf("expected nil, got error: %s\n", err)
	}
}

func compareFields(got []Field, want []*Field) (msg string, ok bool) {
	if len(got) != len(want) {
		return fmt.Sprintf("got %d fields, want %d", len(got), len(want)), false
	}
	for i, g := range got {
		w := *want[i]
		if !fieldsEqual(&g, &w) {
			return fmt.Sprintf("got\n%+v\nwant\n%+v", g, w), false
		}
	}
	return "", true
}

// Need this because Field contains a function, which cannot be compared.
func fieldsEqual(f1, f2 *Field) bool {
	if f1 == nil || f2 == nil {
		return f1 == f2
	}
	return f1.Name == f2.Name &&
		f1.NameFromTag == f2.NameFromTag &&
		f1.Type == f2.Type &&
		cmp.Equal(f1.ParsedTag, f2.ParsedTag)
}

// Set the fields of dst from those of src.
// dst must be a pointer to a struct value.
// src must be a struct value.
func setFields(fields []Field, dst, src interface{}) {
	vsrc := reflect.ValueOf(src)
	vdst := reflect.ValueOf(dst).Elem()
	for _, f := range fields {
		fdst := vdst.FieldByIndex(f.Index)
		fsrc := vsrc.FieldByIndex(f.Index)
		fdst.Set(fsrc)
	}
}

func jsonRoundTrip(t *testing.T, in, out interface{}) {
	t.Helper()

	bytes, err := json.Marshal(in)
	if err != nil {
		t.Fatal(err)
	}
	if err := json.Unmarshal(bytes, out); err != nil {
		t.Fatal(err)
	}
}

type S3 struct {
	S4
	Abc        int
	AbC        int
	Tag        int
	X          int `json:"Tag"`
	unexported int
}

type S4 struct {
	ABc int
	Y   int `json:"Abc"` // ignored because of top-level Abc
}

func TestMatchExact(t *testing.T) {
	fields, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S3{}))
	if err != nil {
		t.Fatal(err)
	}
	for _, test := range []struct {
		name string
		want *Field
	}{
		// Exact match wins.
		{"Abc", field("Abc", int(0), 1)},
		{"AbC", field("AbC", int(0), 2)},
		{"ABc", field("ABc", int(0), 0, 0)},
		// Matches must be exact.
		{"abc", nil},
		// Tag name takes precedence over untagged field of the same name.
		{"Tag", tfield("Tag", int(0), 4)},
		// Unexported fields disappear.
		{"unexported", nil},
		// Untagged embedded structs disappear.
		{"S4", nil},
	} {
		if got := fields.MatchExact(test.name); !fieldsEqual(got, test.want) {
			t.Errorf("match %q:\ngot  %+v\nwant %+v", test.name, got, test.want)
		}
	}
}

func TestMatchFold(t *testing.T) {
	fields, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S3{}))
	if err != nil {
		t.Fatal(err)
	}
	for _, test := range []struct {
		name string
		want *Field
	}{
		// Exact match wins.
		{"Abc", field("Abc", int(0), 1)},
		{"AbC", field("AbC", int(0), 2)},
		{"ABc", field("ABc", int(0), 0, 0)},
		// If there are multiple matches but no exact match or tag,
		// the first field wins, lexicographically by index.
		// Here, "ABc" is at a deeper embedding level, but since S4 appears
		// first in S3, its index precedes the other fields of S3.
		{"abc", field("ABc", int(0), 0, 0)},
		// Tag name takes precedence over untagged field of the same name.
		{"Tag", tfield("Tag", int(0), 4)},
		// Unexported fields disappear.
		{"unexported", nil},
		// Untagged embedded structs disappear.
		{"S4", nil},
	} {
		if got := fields.MatchFold(test.name); !fieldsEqual(got, test.want) {
			t.Errorf("match %q:\ngot  %+v\nwant %+v", test.name, got, test.want)
		}
	}
}

func TestAgainstJSONMatchingField(t *testing.T) {
	s3 := S3{
		S4:         S4{ABc: 1, Y: 2},
		Abc:        3,
		AbC:        4,
		Tag:        5,
		X:          6,
		unexported: 7,
	}
	var want S3
	jsonRoundTrip(t, s3, &want)
	v := reflect.ValueOf(want)
	fields, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S3{}))
	if err != nil {
		t.Fatal(err)
	}
	for _, test := range []struct {
		name string
		got  int
	}{
		{"Abc", 3},
		{"AbC", 4},
		{"ABc", 1},
		{"abc", 1},
		{"Tag", 6},
	} {
		f := fields.MatchFold(test.name)
		if f == nil {
			t.Fatalf("%s: no match", test.name)
		}
		w := v.FieldByIndex(f.Index).Interface()
		if test.got != w {
			t.Errorf("%s: got %d, want %d", test.name, test.got, w)
		}
	}
}

func TestTagErrors(t *testing.T) {
	called := false
	c := NewCache(func(t reflect.StructTag) (string, bool, interface{}, error) {
		called = true
		s := t.Get("f")
		if s == "bad" {
			return "", false, nil, errors.New("error")
		}
		return s, true, nil, nil
	}, nil, nil)

	type T struct {
		X int `f:"ok"`
		Y int `f:"bad"`
	}

	_, err := c.Fields(reflect.TypeOf(T{}))
	if !called {
		t.Fatal("tag parser not called")
	}
	if err == nil {
		t.Error("want error, got nil")
	}
	// Second time, we should cache the error.
	called = false
	_, err = c.Fields(reflect.TypeOf(T{}))
	if called {
		t.Fatal("tag parser called on second time")
	}
	if err == nil {
		t.Error("want error, got nil")
	}
}
