/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package diff_test

import (
	"reflect"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/r3labs/diff/v3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var currentTime = time.Now()

var struct1 = tmstruct{Bar: 1, Foo: "one"}
var struct2 = tmstruct{Bar: 2, Foo: "two"}

type tistruct struct {
	Name  string `diff:"name,identifier"`
	Value int    `diff:"value"`
}

type tuistruct struct {
	Value int `diff:"value"`
}

type tnstruct struct {
	Slice []tmstruct `diff:"slice"`
}

type tmstruct struct {
	Foo string `diff:"foo"`
	Bar int    `diff:"bar"`
}

type Embedded struct {
	Foo string `diff:"foo"`
	Bar int    `diff:"bar"`
}

type embedstruct struct {
	Embedded
	Baz bool `diff:"baz"`
}

type customTagStruct struct {
	Foo string `json:"foo"`
	Bar int    `json:"bar"`
}

type privateValueStruct struct {
	Public  string
	Private *sync.RWMutex
}

type privateMapStruct struct {
	set map[string]interface{}
}

type CustomStringType string
type CustomIntType int
type customTypeStruct struct {
	Foo CustomStringType `diff:"foo"`
	Bar CustomIntType    `diff:"bar"`
}

type tstruct struct {
	ID              string            `diff:"id,immutable"`
	Name            string            `diff:"name"`
	Value           int               `diff:"value"`
	Bool            bool              `diff:"bool"`
	Values          []string          `diff:"values"`
	Map             map[string]string `diff:"map"`
	Time            time.Time         `diff:"time"`
	Pointer         *string           `diff:"pointer"`
	Ignored         bool              `diff:"-"`
	Identifiables   []tistruct        `diff:"identifiables"`
	Unidentifiables []tuistruct       `diff:"unidentifiables"`
	Nested          tnstruct          `diff:"nested"`
	private         int               `diff:"private"`
}

func sptr(s string) *string {
	return &s
}

func TestDiff(t *testing.T) {
	cases := []struct {
		Name      string
		A, B      interface{}
		Changelog diff.Changelog
		Error     error
	}{
		{
			"uint-slice-insert", []uint{1, 2, 3}, []uint{1, 2, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)},
			},
			nil,
		},
		{
			"uint-array-insert", [3]uint{1, 2, 3}, [4]uint{1, 2, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: uint(4)},
			},
			nil,
		},
		{
			"int-slice-insert", []int{1, 2, 3}, []int{1, 2, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4},
			},
			nil,
		},
		{
			"int-array-insert", [3]int{1, 2, 3}, [4]int{1, 2, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4},
			},
			nil,
		},
		{
			"uint-slice-delete", []uint{1, 2, 3}, []uint{1, 3},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)},
			},
			nil,
		},
		{
			"uint-array-delete", [3]uint{1, 2, 3}, [2]uint{1, 3},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)},
			},
			nil,
		},
		{
			"int-slice-delete", []int{1, 2, 3}, []int{1, 3},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2},
			},
			nil,
		},
		{
			"uint-slice-insert-delete", []uint{1, 2, 3}, []uint{1, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)},
				diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)},
			},
			nil,
		},
		{
			"uint-slice-array-delete", [3]uint{1, 2, 3}, [3]uint{1, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: uint(2)},
				diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: uint(4)},
			},
			nil,
		},
		{
			"int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: 2},
				diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: 4},
			},
			nil,
		},
		{
			"string-slice-insert", []string{"1", "2", "3"}, []string{"1", "2", "3", "4"},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"},
			},
			nil,
		},
		{
			"string-array-insert", [3]string{"1", "2", "3"}, [4]string{"1", "2", "3", "4"},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: "4"},
			},
			nil,
		},
		{
			"string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"},
			},
			nil,
		},
		{
			"string-slice-delete", [3]string{"1", "2", "3"}, [2]string{"1", "3"},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"},
			},
			nil,
		},
		{
			"string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"},
				diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"},
			},
			nil,
		},
		{
			"string-array-insert-delete", [3]string{"1", "2", "3"}, [3]string{"1", "3", "4"},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"1"}, From: "2"},
				diff.Change{Type: diff.CREATE, Path: []string{"2"}, To: "4"},
			},
			nil,
		},
		{
			"comparable-slice-insert", []tistruct{{"one", 1}}, []tistruct{{"one", 1}, {"two", 2}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"},
				diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2},
			},
			nil,
		},
		{
			"comparable-array-insert", [1]tistruct{{"one", 1}}, [2]tistruct{{"one", 1}, {"two", 2}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"two", "name"}, To: "two"},
				diff.Change{Type: diff.CREATE, Path: []string{"two", "value"}, To: 2},
			},
			nil,
		},
		{
			"comparable-slice-delete", []tistruct{{"one", 1}, {"two", 2}}, []tistruct{{"one", 1}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"},
				diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2},
			},
			nil,
		},
		{
			"comparable-array-delete", [2]tistruct{{"one", 1}, {"two", 2}}, [1]tistruct{{"one", 1}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"two", "name"}, From: "two"},
				diff.Change{Type: diff.DELETE, Path: []string{"two", "value"}, From: 2},
			},
			nil,
		},
		{
			"comparable-slice-update", []tistruct{{"one", 1}}, []tistruct{{"one", 50}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50},
			},
			nil,
		},
		{
			"comparable-array-update", [1]tistruct{{"one", 1}}, [1]tistruct{{"one", 50}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"one", "value"}, From: 1, To: 50},
			},
			nil,
		},
		{
			"map-slice-insert", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "123", "tset": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"},
			},
			nil,
		},
		{
			"map-array-insert", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "123", "tset": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"0", "tset"}, To: "456"},
			},
			nil,
		},
		{
			"map-slice-update", []map[string]string{{"test": "123"}}, []map[string]string{{"test": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"},
			},
			nil,
		},
		{
			"map-array-update", [1]map[string]string{{"test": "123"}}, [1]map[string]string{{"test": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: "123", To: "456"},
			},
			nil,
		},
		{
			"map-slice-delete", []map[string]string{{"test": "123", "tset": "456"}}, []map[string]string{{"test": "123"}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"},
			},
			nil,
		},
		{
			"map-array-delete", [1]map[string]string{{"test": "123", "tset": "456"}}, [1]map[string]string{{"test": "123"}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"0", "tset"}, From: "456"},
			},
			nil,
		},
		{
			"map-interface-slice-update", []map[string]interface{}{{"test": nil}}, []map[string]interface{}{{"test": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"},
			},
			nil,
		},
		{
			"map-interface-array-update", [1]map[string]interface{}{{"test": nil}}, [1]map[string]interface{}{{"test": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"0", "test"}, From: nil, To: "456"},
			},
			nil,
		},
		{
			"map-nil", map[string]string{"one": "test"}, nil,
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"\xa3one"}, From: "test", To: nil},
			},
			nil,
		},
		{
			"nil-map", nil, map[string]string{"one": "test"},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"\xa3one"}, From: nil, To: "test"},
			},
			nil,
		},
		{
			"nested-map-insert", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "123", "tset": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"},
			},
			nil,
		},
		{
			"nested-map-interface-insert", map[string]map[string]interface{}{"a": {"test": "123"}}, map[string]map[string]interface{}{"a": {"test": "123", "tset": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"a", "tset"}, To: "456"},
			},
			nil,
		},
		{
			"nested-map-update", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {"test": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"a", "test"}, From: "123", To: "456"},
			},
			nil,
		},
		{
			"nested-map-delete", map[string]map[string]string{"a": {"test": "123"}}, map[string]map[string]string{"a": {}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"a", "test"}, From: "123", To: nil},
			},
			nil,
		},
		{
			"nested-slice-insert", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 2, 3, 4}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4},
			},
			nil,
		},
		{
			"nested-array-insert", map[string][3]int{"a": {1, 2, 3}}, map[string][4]int{"a": {1, 2, 3, 4}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"a", "3"}, To: 4},
			},
			nil,
		},
		{
			"nested-slice-update", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 4, 3}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4},
			},
			nil,
		},
		{
			"nested-array-update", map[string][3]int{"a": {1, 2, 3}}, map[string][3]int{"a": {1, 4, 3}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 4},
			},
			nil,
		},
		{
			"nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil},
			},
			nil,
		},
		{
			"nested-array-delete", map[string][3]int{"a": {1, 2, 3}}, map[string][2]int{"a": {1, 3}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"a", "1"}, From: 2, To: nil},
			},
			nil,
		},
		{
			"struct-string-update", tstruct{Name: "one"}, tstruct{Name: "two"},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"name"}, From: "one", To: "two"},
			},
			nil,
		},
		{
			"struct-int-update", tstruct{Value: 1}, tstruct{Value: 50},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"value"}, From: 1, To: 50},
			},
			nil,
		},
		{
			"struct-bool-update", tstruct{Bool: true}, tstruct{Bool: false},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"bool"}, From: true, To: false},
			},
			nil,
		},
		{
			"struct-time-update", tstruct{}, tstruct{Time: currentTime},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"time"}, From: time.Time{}, To: currentTime},
			},
			nil,
		},
		{
			"struct-map-update", tstruct{Map: map[string]string{"test": "123"}}, tstruct{Map: map[string]string{"test": "456"}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"map", "test"}, From: "123", To: "456"},
			},
			nil,
		},
		{
			"struct-string-pointer-update", tstruct{Pointer: sptr("test")}, tstruct{Pointer: sptr("test2")},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: "test", To: "test2"},
			},
			nil,
		},
		{
			"struct-nil-string-pointer-update", tstruct{Pointer: nil}, tstruct{Pointer: sptr("test")},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"pointer"}, From: nil, To: sptr("test")},
			},
			nil,
		},
		{
			"struct-generic-slice-insert", tstruct{Values: []string{"one"}}, tstruct{Values: []string{"one", "two"}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"},
			},
			nil,
		},
		{
			"struct-identifiable-slice-insert", tstruct{Identifiables: []tistruct{{"one", 1}}}, tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "name"}, From: nil, To: "two"},
				diff.Change{Type: diff.CREATE, Path: []string{"identifiables", "two", "value"}, From: nil, To: 2},
			},
			nil,
		},
		{
			"struct-generic-slice-delete", tstruct{Values: []string{"one", "two"}}, tstruct{Values: []string{"one"}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil},
			},
			nil,
		},
		{
			"struct-identifiable-slice-delete", tstruct{Identifiables: []tistruct{{"one", 1}, {"two", 2}}}, tstruct{Identifiables: []tistruct{{"one", 1}}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "name"}, From: "two", To: nil},
				diff.Change{Type: diff.DELETE, Path: []string{"identifiables", "two", "value"}, From: 2, To: nil},
			},
			nil,
		},
		{
			"struct-unidentifiable-slice-insert-delete", tstruct{Unidentifiables: []tuistruct{{1}, {2}, {3}}}, tstruct{Unidentifiables: []tuistruct{{5}, {2}, {3}, {4}}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"unidentifiables", "0", "value"}, From: 1, To: 5},
				diff.Change{Type: diff.CREATE, Path: []string{"unidentifiables", "3", "value"}, From: nil, To: 4},
			},
			nil,
		},
		{
			"struct-with-private-value", privateValueStruct{Public: "one", Private: new(sync.RWMutex)}, privateValueStruct{Public: "two", Private: new(sync.RWMutex)},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"Public"}, From: "one", To: "two"},
			},
			nil,
		},
		{
			"mismatched-values-struct-map", map[string]string{"test": "one"}, &tstruct{Identifiables: []tistruct{{"one", 1}}},
			diff.Changelog{},
			diff.ErrTypeMismatch,
		},
		{
			"omittable", tstruct{Ignored: false}, tstruct{Ignored: true},
			diff.Changelog{},
			nil,
		},
		{
			"slice", &tstruct{}, &tstruct{Nested: tnstruct{Slice: []tmstruct{{"one", 1}, {"two", 2}}}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "foo"}, From: nil, To: "one"},
				diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "0", "bar"}, From: nil, To: 1},
				diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "foo"}, From: nil, To: "two"},
				diff.Change{Type: diff.CREATE, Path: []string{"nested", "slice", "1", "bar"}, From: nil, To: 2},
			},
			nil,
		},
		{
			"slice-duplicate-items", []int{1}, []int{1, 1},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"1"}, From: nil, To: 1},
			},
			nil,
		},
		{
			"mixed-slice-map", []map[string]interface{}{{"name": "name1", "type": []string{"null", "string"}}}, []map[string]interface{}{{"name": "name1", "type": []string{"null", "int"}}, {"name": "name2", "type": []string{"null", "string"}}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"0", "type", "1"}, From: "string", To: "int"},
				diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4name"}, From: nil, To: "name2"},
				diff.Change{Type: diff.CREATE, Path: []string{"1", "\xa4type"}, From: nil, To: []string{"null", "string"}},
			},
			nil,
		},
		{
			"map-string-pointer-create",
			map[string]*tmstruct{"one": &struct1},
			map[string]*tmstruct{"one": &struct1, "two": &struct2},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"two", "foo"}, From: nil, To: "two"},
				diff.Change{Type: diff.CREATE, Path: []string{"two", "bar"}, From: nil, To: 2},
			},
			nil,
		},
		{
			"map-string-pointer-delete",
			map[string]*tmstruct{"one": &struct1, "two": &struct2},
			map[string]*tmstruct{"one": &struct1},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"two", "foo"}, From: "two", To: nil},
				diff.Change{Type: diff.DELETE, Path: []string{"two", "bar"}, From: 2, To: nil},
			},
			nil,
		},
		{
			"private-struct-field",
			tstruct{private: 1},
			tstruct{private: 4},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"private"}, From: int64(1), To: int64(4)},
			},
			nil,
		},
		{
			"embedded-struct-field",
			embedstruct{Embedded{Foo: "a", Bar: 2}, true},
			embedstruct{Embedded{Foo: "b", Bar: 3}, false},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "a", To: "b"},
				diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 2, To: 3},
				diff.Change{Type: diff.UPDATE, Path: []string{"baz"}, From: true, To: false},
			},
			nil,
		},
		{
			"custom-tags",
			customTagStruct{Foo: "abc", Bar: 3},
			customTagStruct{Foo: "def", Bar: 4},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: "abc", To: "def"},
				diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: 3, To: 4},
			},
			nil,
		},
		{
			"custom-types",
			customTypeStruct{Foo: "a", Bar: 1},
			customTypeStruct{Foo: "b", Bar: 2},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"foo"}, From: CustomStringType("a"), To: CustomStringType("b")},
				diff.Change{Type: diff.UPDATE, Path: []string{"bar"}, From: CustomIntType(1), To: CustomIntType(2)},
			},
			nil,
		},
		{
			"struct-private-map-create", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: struct{}{}},
			},
			nil,
		},
		{
			"struct-private-map-delete", privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}, "3": struct{}{}}}, privateMapStruct{set: map[string]interface{}{"1": struct{}{}, "2": struct{}{}}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"set", "3"}, From: struct{}{}, To: nil},
			},
			nil,
		},
		{
			"struct-private-map-nil-values", privateMapStruct{set: map[string]interface{}{"1": nil, "2": nil}}, privateMapStruct{set: map[string]interface{}{"1": nil, "2": nil, "3": nil}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"set", "3"}, From: nil, To: nil},
			},
			nil,
		},
		{
			"slice-of-struct-with-slice",
			[]tnstruct{{[]tmstruct{struct1, struct2}}, {[]tmstruct{struct2, struct2}}},
			[]tnstruct{{[]tmstruct{struct2, struct2}}, {[]tmstruct{struct2, struct1}}},
			diff.Changelog{},
			nil,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {

			var options []func(d *diff.Differ) error
			switch tc.Name {
			case "mixed-slice-map", "nil-map", "map-nil":
				options = append(options, diff.StructMapKeySupport())
			case "embedded-struct-field":
				options = append(options, diff.FlattenEmbeddedStructs())
			case "custom-tags":
				options = append(options, diff.TagName("json"))
			}
			cl, err := diff.Diff(tc.A, tc.B, options...)

			assert.Equal(t, tc.Error, err)
			require.Equal(t, len(tc.Changelog), len(cl))

			for i, c := range cl {
				assert.Equal(t, tc.Changelog[i].Type, c.Type)
				assert.Equal(t, tc.Changelog[i].Path, c.Path)
				assert.Equal(t, tc.Changelog[i].From, c.From)
				assert.Equal(t, tc.Changelog[i].To, c.To)
			}
		})
	}
}

func TestDiffSliceOrdering(t *testing.T) {
	cases := []struct {
		Name      string
		A, B      interface{}
		Changelog diff.Changelog
		Error     error
	}{
		{
			"int-slice-insert-in-middle", []int{1, 2, 4}, []int{1, 2, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 4, To: 3},
				diff.Change{Type: diff.CREATE, Path: []string{"3"}, To: 4},
			},
			nil,
		},
		{
			"int-slice-delete", []int{1, 2, 3}, []int{1, 3},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3},
				diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: 3},
			},
			nil,
		},
		{
			"int-slice-insert-delete", []int{1, 2, 3}, []int{1, 3, 4},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3},
				diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 4},
			},
			nil,
		},
		{
			"int-slice-reorder", []int{1, 2, 3}, []int{1, 3, 2},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: 2, To: 3},
				diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: 3, To: 2},
			},
			nil,
		},
		{
			"string-slice-delete", []string{"1", "2", "3"}, []string{"1", "3"},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"},
				diff.Change{Type: diff.DELETE, Path: []string{"2"}, From: "3"},
			},
			nil,
		},
		{
			"string-slice-insert-delete", []string{"1", "2", "3"}, []string{"1", "3", "4"},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"},
				diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "4"},
			},
			nil,
		},
		{
			"string-slice-reorder", []string{"1", "2", "3"}, []string{"1", "3", "2"},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"1"}, From: "2", To: "3"},
				diff.Change{Type: diff.UPDATE, Path: []string{"2"}, From: "3", To: "2"},
			},
			nil,
		},
		{
			"nested-slice-delete", map[string][]int{"a": {1, 2, 3}}, map[string][]int{"a": {1, 3}},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"a", "1"}, From: 2, To: 3},
				diff.Change{Type: diff.DELETE, Path: []string{"a", "2"}, From: 3},
			},
			nil,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			d, err := diff.NewDiffer(diff.SliceOrdering(true))
			require.Nil(t, err)
			cl, err := d.Diff(tc.A, tc.B)

			assert.Equal(t, tc.Error, err)
			require.Equal(t, len(tc.Changelog), len(cl))

			for i, c := range cl {
				assert.Equal(t, tc.Changelog[i].Type, c.Type)
				assert.Equal(t, tc.Changelog[i].Path, c.Path)
				assert.Equal(t, tc.Changelog[i].From, c.From)
				assert.Equal(t, tc.Changelog[i].To, c.To)
			}
		})
	}

}

func TestFilter(t *testing.T) {
	cases := []struct {
		Name     string
		Filter   []string
		Expected [][]string
	}{
		{"simple", []string{"item-1", "subitem"}, [][]string{{"item-1", "subitem"}}},
		{"regex", []string{"item-*"}, [][]string{{"item-1", "subitem"}, {"item-2", "subitem"}}},
	}

	cl := diff.Changelog{
		{Path: []string{"item-1", "subitem"}},
		{Path: []string{"item-2", "subitem"}},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ncl := cl.Filter(tc.Filter)
			assert.Len(t, ncl, len(tc.Expected))
			for i, e := range tc.Expected {
				assert.Equal(t, e, ncl[i].Path)
			}
		})
	}
}

func TestFilterOut(t *testing.T) {
	cases := []struct {
		Name     string
		Filter   []string
		Expected [][]string
	}{
		{"simple", []string{"item-1", "subitem"}, [][]string{{"item-2", "subitem"}}},
		{"regex", []string{"item-*"}, [][]string{}},
	}

	cl := diff.Changelog{
		{Path: []string{"item-1", "subitem"}},
		{Path: []string{"item-2", "subitem"}},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			ncl := cl.FilterOut(tc.Filter)
			assert.Len(t, ncl, len(tc.Expected))
			for i, e := range tc.Expected {
				assert.Equal(t, e, ncl[i].Path)
			}
		})
	}
}

func TestStructValues(t *testing.T) {
	cases := []struct {
		Name       string
		ChangeType string
		X          interface{}
		Changelog  diff.Changelog
		Error      error
	}{
		{
			"struct-create", diff.CREATE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}},
			diff.Changelog{
				diff.Change{Type: diff.CREATE, Path: []string{"id"}, From: nil, To: "xxxxx"},
				diff.Change{Type: diff.CREATE, Path: []string{"name"}, From: nil, To: "something"},
				diff.Change{Type: diff.CREATE, Path: []string{"value"}, From: nil, To: 1},
				diff.Change{Type: diff.CREATE, Path: []string{"values", "0"}, From: nil, To: "one"},
				diff.Change{Type: diff.CREATE, Path: []string{"values", "1"}, From: nil, To: "two"},
				diff.Change{Type: diff.CREATE, Path: []string{"values", "2"}, From: nil, To: "three"},
			},
			nil,
		},
		{
			"struct-delete", diff.DELETE, tstruct{ID: "xxxxx", Name: "something", Value: 1, Values: []string{"one", "two", "three"}},
			diff.Changelog{
				diff.Change{Type: diff.DELETE, Path: []string{"id"}, From: "xxxxx", To: nil},
				diff.Change{Type: diff.DELETE, Path: []string{"name"}, From: "something", To: nil},
				diff.Change{Type: diff.DELETE, Path: []string{"value"}, From: 1, To: nil},
				diff.Change{Type: diff.DELETE, Path: []string{"values", "0"}, From: "one", To: nil},
				diff.Change{Type: diff.DELETE, Path: []string{"values", "1"}, From: "two", To: nil},
				diff.Change{Type: diff.DELETE, Path: []string{"values", "2"}, From: "three", To: nil},
			},
			nil,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			cl, err := diff.StructValues(tc.ChangeType, []string{}, tc.X)

			assert.Equal(t, tc.Error, err)
			assert.Equal(t, len(tc.Changelog), len(cl))

			for i, c := range cl {
				assert.Equal(t, tc.Changelog[i].Type, c.Type)
				assert.Equal(t, tc.Changelog[i].Path, c.Path)
				assert.Equal(t, tc.Changelog[i].From, c.From)
				assert.Equal(t, tc.Changelog[i].To, c.To)
			}
		})
	}
}

func TestDifferReuse(t *testing.T) {
	d, err := diff.NewDiffer()
	require.Nil(t, err)

	cl, err := d.Diff([]string{"1", "2", "3"}, []string{"1"})
	require.Nil(t, err)

	require.Len(t, cl, 2)

	assert.Equal(t, "2", cl[0].From)
	assert.Equal(t, nil, cl[0].To)
	assert.Equal(t, "3", cl[1].From)
	assert.Equal(t, nil, cl[1].To)

	cl, err = d.Diff([]string{"a", "b"}, []string{"a", "c"})
	require.Nil(t, err)

	require.Len(t, cl, 1)

	assert.Equal(t, "b", cl[0].From)
	assert.Equal(t, "c", cl[0].To)
}

func TestDiffingOptions(t *testing.T) {
	d, err := diff.NewDiffer(diff.SliceOrdering(false))
	require.Nil(t, err)

	assert.False(t, d.SliceOrdering)

	cl, err := d.Diff([]int{1, 2, 3}, []int{1, 3, 2})
	require.Nil(t, err)

	assert.Len(t, cl, 0)

	d, err = diff.NewDiffer(diff.SliceOrdering(true))
	require.Nil(t, err)

	assert.True(t, d.SliceOrdering)

	cl, err = d.Diff([]int{1, 2, 3}, []int{1, 3, 2})
	require.Nil(t, err)

	assert.Len(t, cl, 2)

	// some other options..
}

func TestDiffPrivateField(t *testing.T) {
	cl, err := diff.Diff(tstruct{private: 1}, tstruct{private: 3})
	require.Nil(t, err)
	assert.Len(t, cl, 1)
}

type testType string
type testTypeDiffer struct {
	DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error)
}

func (o *testTypeDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) {
	o.DiffFunc = dfunc
}

func (o *testTypeDiffer) Match(a, b reflect.Value) bool {
	return diff.AreType(a, b, reflect.TypeOf(testType("")))
}
func (o *testTypeDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error {
	if a.String() != "custom" && b.String() != "match" {
		cl.Add(diff.UPDATE, path, a.Interface(), b.Interface())
	}
	return nil
}

func TestCustomDiffer(t *testing.T) {
	type custom struct {
		T testType
	}

	d, err := diff.NewDiffer(
		diff.CustomValueDiffers(
			&testTypeDiffer{},
		),
	)
	require.Nil(t, err)

	cl, err := d.Diff(custom{"custom"}, custom{"match"})
	require.Nil(t, err)

	assert.Len(t, cl, 0)

	d, err = diff.NewDiffer(
		diff.CustomValueDiffers(
			&testTypeDiffer{},
		),
	)
	require.Nil(t, err)

	cl, err = d.Diff(custom{"same"}, custom{"same"})
	require.Nil(t, err)

	assert.Len(t, cl, 1)
}

type testStringInterceptorDiffer struct {
	DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error)
}

func (o *testStringInterceptorDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) {
	o.DiffFunc = dfunc
}

func (o *testStringInterceptorDiffer) Match(a, b reflect.Value) bool {
	return diff.AreType(a, b, reflect.TypeOf(testType("")))
}
func (o *testStringInterceptorDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error {
	if dt.String() == "STRING" {
		// intercept the data
		aValue, aOk := a.Interface().(testType)
		bValue, bOk := b.Interface().(testType)

		if aOk && bOk {
			if aValue == "avalue" {
				aValue = testType(strings.ToUpper(string(aValue)))
				a = reflect.ValueOf(aValue)
			}

			if bValue == "bvalue" {
				bValue = testType(strings.ToUpper(string(aValue)))
				b = reflect.ValueOf(bValue)
			}
		}
	}

	// continue the diff logic passing the updated a/b values
	return df(path, a, b, parent)
}

func TestStringInterceptorDiffer(t *testing.T) {
	d, err := diff.NewDiffer(
		diff.CustomValueDiffers(
			&testStringInterceptorDiffer{},
		),
	)
	require.Nil(t, err)

	cl, err := d.Diff(testType("avalue"), testType("bvalue"))
	require.Nil(t, err)

	assert.Len(t, cl, 0)
}

type RecursiveTestStruct struct {
	Id       int
	Children []RecursiveTestStruct
}

type recursiveTestStructDiffer struct {
	DiffFunc (func(path []string, a, b reflect.Value, p interface{}) error)
}

func (o *recursiveTestStructDiffer) InsertParentDiffer(dfunc func(path []string, a, b reflect.Value, p interface{}) error) {
	o.DiffFunc = dfunc
}

func (o *recursiveTestStructDiffer) Match(a, b reflect.Value) bool {
	return diff.AreType(a, b, reflect.TypeOf(RecursiveTestStruct{}))
}

func (o *recursiveTestStructDiffer) Diff(dt diff.DiffType, df diff.DiffFunc, cl *diff.Changelog, path []string, a, b reflect.Value, parent interface{}) error {
	if a.Kind() == reflect.Invalid {
		cl.Add(diff.CREATE, path, nil, b.Interface())
		return nil
	}
	if b.Kind() == reflect.Invalid {
		cl.Add(diff.DELETE, path, a.Interface(), nil)
		return nil
	}
	var awt, bwt RecursiveTestStruct
	awt, _ = a.Interface().(RecursiveTestStruct)
	bwt, _ = b.Interface().(RecursiveTestStruct)
	if awt.Id != bwt.Id {
		cl.Add(diff.UPDATE, path, a.Interface(), b.Interface())
	}
	for i := 0; i < a.NumField(); i++ {
		field := a.Type().Field(i)
		tname := field.Name
		if tname != "Children" {
			continue
		}
		af := a.Field(i)
		bf := b.FieldByName(field.Name)
		fpath := copyAppend(path, tname)
		err := o.DiffFunc(fpath, af, bf, nil)
		if err != nil {
			return err
		}
	}
	return nil
}

func TestRecursiveCustomDiffer(t *testing.T) {
	treeA := RecursiveTestStruct{
		Id:       1,
		Children: []RecursiveTestStruct{},
	}

	treeB := RecursiveTestStruct{
		Id: 1,
		Children: []RecursiveTestStruct{
			{
				Id:       4,
				Children: []RecursiveTestStruct{},
			},
		},
	}
	d, err := diff.NewDiffer(
		diff.CustomValueDiffers(
			&recursiveTestStructDiffer{},
		),
	)
	require.Nil(t, err)
	cl, err := d.Diff(treeA, treeB)
	require.Nil(t, err)
	assert.Len(t, cl, 1)
}

func TestHandleDifferentTypes(t *testing.T) {
	cases := []struct {
		Name               string
		A, B               interface{}
		Changelog          diff.Changelog
		Error              error
		HandleTypeMismatch bool
	}{
		{
			"type-change-not-allowed-error",
			1, "1",
			nil,
			diff.ErrTypeMismatch,
			false,
		},
		{
			"type-change-not-allowed-error-struct",
			struct {
				p1 string
				p2 int
			}{"1", 1},
			struct {
				p1 string
				p2 string
			}{"1", "1"},
			nil,
			diff.ErrTypeMismatch,
			false,
		},
		{
			"type-change-allowed",
			1, "1",
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{}, From: 1, To: "1"},
			},
			nil,
			true,
		},
		{
			"type-change-allowed-struct",
			struct {
				P1 string
				P2 int
				P3 map[string]string
			}{"1", 1, map[string]string{"1": "1"}},
			struct {
				P1 string
				P2 string
				P3 string
			}{"1", "1", "1"},
			diff.Changelog{
				diff.Change{Type: diff.UPDATE, Path: []string{"P2"}, From: 1, To: "1"},
				diff.Change{Type: diff.UPDATE, Path: []string{"P3"}, From: map[string]string{"1": "1"}, To: "1"},
			},
			nil,
			true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			d, err := diff.NewDiffer(diff.AllowTypeMismatch(tc.HandleTypeMismatch))
			require.Nil(t, err)
			cl, err := d.Diff(tc.A, tc.B)

			assert.Equal(t, tc.Error, err)
			require.Equal(t, len(tc.Changelog), len(cl))

			for i, c := range cl {
				assert.Equal(t, tc.Changelog[i].Type, c.Type)
				assert.Equal(t, tc.Changelog[i].Path, c.Path)
				assert.Equal(t, tc.Changelog[i].From, c.From)
				assert.Equal(t, tc.Changelog[i].To, c.To)
			}
		})
	}
}

func copyAppend(src []string, elems ...string) []string {
	dst := make([]string, len(src)+len(elems))
	copy(dst, src)
	for i := len(src); i < len(src)+len(elems); i++ {
		dst[i] = elems[i-len(src)]
	}
	return dst
}
