// Copyright (c) 2015 Arista Networks, Inc.
// Use of this source code is governed by the Apache License 2.0
// that can be found in the COPYING file.

package test

import (
	"errors"
	"fmt"
	"reflect"
	"runtime"
	"testing"
	"time"

	"github.com/aristanetworks/goarista/key"
	pb "google.golang.org/protobuf/types/known/durationpb"
)

type builtinCompare struct {
	a uint32
	b string
}

type complexCompare struct {
	m map[builtinCompare]int8
	p *complexCompare
}

type embedder struct {
	builtinCompare
}

type partialCompare struct {
	a uint32
	b string `deepequal:"ignore"`
}

type deepEqualTestCase struct {
	a, b interface{}
	diff string
}

type code int32

type message string

type myError struct {
	e string
}

type myEmbeddedErrorPtr struct {
	e *myError
}

func (e *myError) Error() string {
	return e.e
}

type myStringError string

func (e myStringError) Error() string {
	return string(e)
}

type hasFunc struct {
	f func()
}

func someFunction()      {}
func someOtherFunction() {}

func generateFunc() func() {
	return func() {}
}

// both of these functions are generated, but will both will be assigned distinct anonymous names,
// for example:
// generatedFuncA would be github.com/aristanetworks/goarista/test.init.func1
// generatedFuncB would be github.com/aristanetworks/goarista/test.init.func2
var generatedFuncA = generateFunc()
var generatedFuncB = generateFunc()

func getDeepEqualTests(t *testing.T) []deepEqualTestCase {
	var deepEqualNullMapString map[string]interface{}
	recursive := &complexCompare{}
	recursive.p = recursive
	time1 := time.Now()
	time2 := time1.Add(time.Second)
	time1a := time.Unix(0, time1.UnixNano())
	return []deepEqualTestCase{{
		a: nil,
		b: nil,
	}, {
		a: uint8(5),
		b: uint8(5),
	}, {
		a:    nil,
		b:    uint8(5),
		diff: "expected nil but got a uint8: 0x5",
	}, {
		a:    uint8(5),
		b:    nil,
		diff: "expected a uint8 (0x5) but got nil",
	}, {
		a:    uint16(1),
		b:    uint16(2),
		diff: "uint16(1) != uint16(2)",
	}, {
		a:    int8(1),
		b:    int16(1),
		diff: "expected a int8 but got a int16",
	}, {
		a: true,
		b: true,
	}, {
		a: float32(3.1415),
		b: float32(3.1415),
	}, {
		a:    float32(3.1415),
		b:    float32(3.1416),
		diff: "float32(3.1415) != float32(3.1416)",
	}, {
		a: float64(3.14159265),
		b: float64(3.14159265),
	}, {
		a:    float64(3.14159265),
		b:    float64(3.14159266),
		diff: "float64(3.14159265) != float64(3.14159266)",
	}, {
		a: deepEqualNullMapString,
		b: deepEqualNullMapString,
	}, {
		a: &deepEqualNullMapString,
		b: &deepEqualNullMapString,
	}, {
		a:    deepEqualNullMapString,
		b:    &deepEqualNullMapString,
		diff: "expected a map[string]interface {} but got a *map[string]interface {}",
	}, {
		a:    &deepEqualNullMapString,
		b:    deepEqualNullMapString,
		diff: "expected a *map[string]interface {} but got a map[string]interface {}",
	}, {
		a: map[string]interface{}{"a": uint32(42)},
		b: map[string]interface{}{"a": uint32(42)},
	}, {
		a:    map[string]interface{}{"a": int32(42)},
		b:    map[string]interface{}{"a": int32(51)},
		diff: `for key "a" in map, values are different: int32(42) != int32(51)`,
	}, {
		a:    map[string]interface{}{"a": uint32(42)},
		b:    map[string]interface{}{},
		diff: `Maps have different size: 1 != 0 (missing key: "a")`,
	}, {
		a:    map[string]interface{}{},
		b:    map[string]interface{}{"a": uint32(42)},
		diff: `Maps have different size: 0 != 1 (extra key: "a")`,
	}, {
		a:    map[string]interface{}{"a": uint64(42), "b": "extra"},
		b:    map[string]interface{}{"a": uint64(42)},
		diff: `Maps have different size: 2 != 1 (missing key: "b")`,
	}, {
		a:    map[string]interface{}{"a": uint64(42)},
		b:    map[string]interface{}{"a": uint64(42), "b": "extra"},
		diff: `Maps have different size: 1 != 2 (extra key: "b")`,
	}, {
		a: map[uint64]interface{}{uint64(42): "foo"},
		b: map[uint64]interface{}{uint64(42): "foo"},
	}, {
		a:    map[uint64]interface{}{uint64(42): "foo"},
		b:    map[uint64]interface{}{uint64(51): "foo"},
		diff: "key uint64(42) in map is missing in the actual map",
	}, {
		a:    map[uint64]interface{}{uint64(42): "foo"},
		b:    map[uint64]interface{}{uint64(42): "foo", uint64(51): "bar"},
		diff: `Maps have different size: 1 != 2 (extra key: uint64(51))`,
	}, {
		a:    map[uint64]interface{}{uint64(42): "foo"},
		b:    map[interface{}]interface{}{uint32(42): "foo"},
		diff: "expected a map[uint64]interface {} but got a map[interface {}]interface {}",
	}, {
		a:    map[interface{}]interface{}{"a": uint32(42)},
		b:    map[string]interface{}{"a": uint32(42)},
		diff: "expected a map[interface {}]interface {} but got a map[string]interface {}",
	}, {
		a: map[interface{}]interface{}{},
		b: map[interface{}]interface{}{},
	}, {
		a: &map[interface{}]interface{}{},
		b: &map[interface{}]interface{}{},
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo"},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo"},
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": uint32(8)}: "foo"},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": uint32(8)}: "fox"},
		diff: `for complex key *map[string]interface {}{"a":"foo", "b":uint32(8)}` +
			` in map, values are different: string(foo) != string(fox)`,
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": uint32(8)}: "foo"},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": uint32(5)}: "foo"},
		diff: `complex key *map[string]interface {}{"a":"foo", "b":uint32(8)}` +
			` in map is missing in the actual map`,
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": uint32(8)}: "foo"},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo"}: "foo"},
		diff: `complex key *map[string]interface {}{"a":"foo", "b":uint32(8)}` +
			` in map is missing in the actual map`,
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int8(8)}:  "foo",
		},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int8(8)}:  "foo",
		},
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int8(8)}:  "foo",
		},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int8(5)}:  "foo",
		},
		diff: `complex key *map[string]interface {}{"a":"foo", "b":int8(8)}` +
			` in map is missing in the actual map`,
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int8(8)}:  "foo",
		},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int32(8)}: "foo",
		},
		diff: `complex key *map[string]interface {}{"a":"foo", "b":int8(8)}` +
			` in map is missing in the actual map`,
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int8(8)}:  "foo",
		},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
		},
		diff: `Maps have different size: 2 != 1` +
			` (extra key: *map[string]interface {}{"a":"foo", "b":int16(8)},` +
			` missing key: *map[string]interface {}{"a":"foo", "b":int16(8)},` +
			` missing key: *map[string]interface {}{"a":"foo", "b":int8(8)})`,
	}, {
		a: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
		},
		b: map[interface{}]interface{}{
			&map[string]interface{}{"a": "foo", "b": int16(8)}: "foo",
			&map[string]interface{}{"a": "foo", "b": int8(8)}:  "foo",
		},
		diff: `Maps have different size: 1 != 2` +
			` (extra key: *map[string]interface {}{"a":"foo", "b":int16(8)},` +
			` extra key: *map[string]interface {}{"a":"foo", "b":int8(8)},` +
			` missing key: *map[string]interface {}{"a":"foo", "b":int16(8)})`,
	}, {
		a: []string{},
		b: []string{},
	}, {
		a: []string{"foo", "bar"},
		b: []string{"foo", "bar"},
	}, {
		a:    []string{"foo", "bar"},
		b:    []string{"foo"},
		diff: "Expected an array of size 2 but got 1",
	}, {
		a:    []string{"foo"},
		b:    []string{"foo", "bar"},
		diff: "Expected an array of size 1 but got 2",
	}, {
		a: []string{"foo", "bar"},
		b: []string{"bar", "foo"},
		diff: `In arrays, values are different at index 0:` +
			` string(foo) != string(bar)`,
	}, {
		a:    &[]string{},
		b:    []string{},
		diff: "expected a *[]string but got a []string",
	}, {
		a: &[]string{},
		b: &[]string{},
	}, {
		a: &[]string{"foo", "bar"},
		b: &[]string{"foo", "bar"},
	}, {
		a:    &[]string{"foo", "bar"},
		b:    &[]string{"foo"},
		diff: "Expected an array of size 2 but got 1",
	}, {
		a:    &[]string{"foo"},
		b:    &[]string{"foo", "bar"},
		diff: "Expected an array of size 1 but got 2",
	}, {
		a: &[]string{"foo", "bar"},
		b: &[]string{"bar", "foo"},
		diff: `In arrays, values are different at index 0:` +
			` string(foo) != string(bar)`,
	}, {
		a: []uint32{42, 51},
		b: []uint32{42, 51},
	}, {
		a:    []uint32{42, 51},
		b:    []uint32{42, 88},
		diff: "In arrays, values are different at index 1: uint32(51) != uint32(88)",
	}, {
		a:    []uint32{42, 51},
		b:    []uint32{42},
		diff: "Expected an array of size 2 but got 1",
	}, {
		a:    []uint32{42, 51},
		b:    []uint64{42, 51},
		diff: "expected a []uint32 but got a []uint64",
	}, {
		a:    []uint64{42, 51},
		b:    []uint32{42, 51},
		diff: "expected a []uint64 but got a []uint32",
	}, {
		a: []uint64{42, 51},
		b: []uint64{42, 51},
	}, {
		a:    []uint64{42, 51},
		b:    []uint64{42},
		diff: "Expected an array of size 2 but got 1",
	}, {
		a:    []uint64{42, 51},
		b:    []uint64{42, 88},
		diff: "In arrays, values are different at index 1: uint64(51) != uint64(88)",
	}, {
		a: []interface{}{"foo", uint32(42)},
		b: []interface{}{"foo", uint32(42)},
	}, {
		a:    []interface{}{"foo", uint32(42)},
		b:    []interface{}{"foo"},
		diff: "Expected an array of size 2 but got 1",
	}, {
		a:    []interface{}{"foo"},
		b:    []interface{}{"foo", uint32(42)},
		diff: "Expected an array of size 1 but got 2",
	}, {
		a: []interface{}{"foo", uint32(42)},
		b: []interface{}{"foo", uint8(42)},
		diff: "In arrays, values are different at index 1:" +
			" expected a uint32 but got a uint8",
	}, {
		a:    []interface{}{"foo", "bar"},
		b:    []string{"foo", "bar"},
		diff: "expected a []interface {} but got a []string",
	}, {
		a: &[]interface{}{"foo", uint32(42)},
		b: &[]interface{}{"foo", uint32(42)},
	}, {
		a:    &[]interface{}{"foo", uint32(42)},
		b:    []interface{}{"foo", uint32(42)},
		diff: "expected a *[]interface {} but got a []interface {}",
	}, {
		a: comparableStruct{a: 42},
		b: comparableStruct{a: 42},
	}, {
		a: comparableStruct{a: 42, t: t},
		b: comparableStruct{a: 42},
	}, {
		a: comparableStruct{a: 42},
		b: comparableStruct{a: 42, t: t},
	}, {
		a: comparableStruct{a: 42},
		b: comparableStruct{a: 51},
		diff: "Comparable types are different: test.comparableStruct{a:" +
			"uint32(42), t:*nil} vs test.comparableStruct{a:uint32(51), t:*nil}",
	}, {
		a: builtinCompare{a: 42, b: "foo"},
		b: builtinCompare{a: 42, b: "foo"},
	}, {
		a:    builtinCompare{a: 42, b: "foo"},
		b:    builtinCompare{a: 42, b: "bar"},
		diff: `attributes "b" are different: string(foo) != string(bar)`,
	}, {
		a: map[int8]int8{2: 3, 3: 4},
		b: map[int8]int8{2: 3, 3: 4},
	}, {
		a:    map[int8]int8{2: 3, 3: 4},
		b:    map[int8]int8{2: 3, 3: 5},
		diff: "for key int8(3) in map, values are different: int8(4) != int8(5)",
	}, {
		a: complexCompare{},
		b: complexCompare{},
	}, {
		a: complexCompare{
			m: map[builtinCompare]int8{{1, "foo"}: 42}},
		b: complexCompare{
			m: map[builtinCompare]int8{{1, "foo"}: 42}},
	}, {
		a: complexCompare{
			m: map[builtinCompare]int8{{1, "foo"}: 42}},
		b: complexCompare{
			m: map[builtinCompare]int8{{1, "foo"}: 51}},
		diff: `attributes "m" are different: for key test.builtinCompare{a:uint32(1),` +
			` b:"foo"} in map, values are different: int8(42) != int8(51)`,
	}, {
		a: complexCompare{
			m: map[builtinCompare]int8{{1, "foo"}: 42}},
		b: complexCompare{
			m: map[builtinCompare]int8{{1, "bar"}: 42}},
		diff: `attributes "m" are different: key test.builtinCompare{a:uint32(1),` +
			` b:"foo"} in map is missing in the actual map`,
	}, {
		a: recursive,
		b: recursive,
	}, {
		a: complexCompare{p: recursive},
		b: complexCompare{p: recursive},
	}, {
		a: complexCompare{p: &complexCompare{p: recursive}},
		b: complexCompare{p: &complexCompare{p: recursive}},
	}, {
		a: []complexCompare{{p: &complexCompare{p: recursive}}},
		b: []complexCompare{{p: &complexCompare{p: recursive}}},
	}, {
		a: []complexCompare{{p: &complexCompare{p: recursive}}},
		b: []complexCompare{{p: &complexCompare{p: nil}}},
		diff: `In arrays, values are different at index 0: attributes "p" are` +
			` different: attributes "p" are different: got nil instead of ` +
			`*test.complexCompare{m:map[test.builtinCompare]int8{},` +
			` p:*test.complexCompare{<circular dependency>}}`,
	}, {
		a: partialCompare{a: 42},
		b: partialCompare{a: 42},
	}, {
		a:    partialCompare{a: 42},
		b:    partialCompare{a: 51},
		diff: `attributes "a" are different: uint32(42) != uint32(51)`,
	}, {
		a: partialCompare{a: 42, b: "foo"},
		b: partialCompare{a: 42, b: "bar"},
	}, {
		a: map[*builtinCompare]uint32{{1, "foo"}: 42},
		b: map[*builtinCompare]uint32{{1, "foo"}: 42},
	}, {
		a: map[*builtinCompare]uint32{{1, "foo"}: 42},
		b: map[*builtinCompare]uint32{{2, "foo"}: 42},
		diff: `complex key *test.builtinCompare{a:uint32(1), b:"foo"}` +
			` in map is missing in the actual map`,
	}, {
		a: map[*builtinCompare]uint32{{1, "foo"}: 42},
		b: map[*builtinCompare]uint32{{1, "foo"}: 51},
		diff: `for complex key *test.builtinCompare{a:uint32(1), b:"foo"}` +
			` in map, values are different: uint32(42) != uint32(51)`,
	}, {
		a: key.New("a"),
		b: key.New("a"),
	}, {
		a: key.NewMap(key.New("a"), "b"),
		b: key.NewMap(key.New("a"), "b"),
	}, {
		a: key.NewMap(key.New(map[string]interface{}{"a": true}), "b"),
		b: key.NewMap(key.New(map[string]interface{}{"a": true}), "b"),
	}, {
		a: key.New(map[string]interface{}{
			"a": key.NewMap(key.New(map[string]interface{}{"k": 42}), true)}),
		b: key.New(map[string]interface{}{
			"a": key.NewMap(key.New(map[string]interface{}{"k": 42}), true)}),
	}, {
		a: key.New(map[string]interface{}{
			"a": key.NewMap(key.New(map[string]interface{}{"k": 42}), true)}),
		b: key.New(map[string]interface{}{
			"a": key.NewMap(key.New(map[string]interface{}{"k": 51}), true)}),
		diff: `Comparable types are different: ` +
			`key.mapKey{"a":*key.Map{normal:map[interface {}]interface {}{}, ` +
			`custom:map[uint64]key.entry{<max_depth>:<max_depth>}, length:int(1)}} vs ` +
			`key.mapKey{"a":*key.Map{normal:map[interface {}]interface {}{}, ` +
			`custom:map[uint64]key.entry{<max_depth>:<max_depth>}, length:int(1)}}`,
	}, {
		a: fmt.Errorf("This is a %d error", 42),
		b: errors.New("This is a 42 error"),
	}, {
		a: fmt.Errorf("This is a %d error", 42),
		b: &myError{e: "This is a 42 error"},
	}, {
		a:    &myError{e: "This is a 42 error"},
		diff: `expected a *test.myError (&test.myError{e:"This is a 42 error"}) but got nil`,
	}, {
		a: myStringError("This is a 42 error"),
		b: fmt.Errorf("This is a %d error", 42),
	}, {
		a: &myEmbeddedErrorPtr{},
		b: &myEmbeddedErrorPtr{},
	}, {
		a: code(42),
		b: code(42),
	}, {
		a:    code(42),
		b:    code(51),
		diff: "code(42) != code(51)",
	}, {
		a: message("foo"),
		b: message("foo"),
	}, {
		a:    message("foo"),
		b:    message("bar"),
		diff: `message("foo") != message("bar")`,
	}, {
		a: []byte("foo"),
		b: []byte("foo"),
	}, {
		a:    []byte("foo"),
		b:    []byte("bar"),
		diff: `[]byte("foo") != []byte("bar")`,
	}, {
		a: time1,
		b: time1a,
	}, {
		a: time1,
		b: time1.UTC(),
	}, {
		a:    time1,
		b:    time2,
		diff: fmt.Sprintf("time.Time values are different: %s vs %s", time1, time2),
	}, {
		a: embedder{builtinCompare: builtinCompare{}},
		b: embedder{builtinCompare: builtinCompare{}},
	}, {
		a: pb.Duration{Seconds: 1},
		b: pb.Duration{Seconds: 1},
	}, {
		a:    pb.Duration{Seconds: 1},
		b:    pb.Duration{Seconds: 2},
		diff: "attributes \"Seconds\" are different: int64(1) != int64(2)",
	}, {
		a:    []interface{}{"foo", uint32(42)},
		b:    pb.Duration{Seconds: 1},
		diff: "expected a []interface {} but got a durationpb.Duration",
	}, {
		a:    pb.Duration{Seconds: 1},
		b:    []interface{}{"foo", uint32(42)},
		diff: "expected a durationpb.Duration but got a []interface {}",
	}, {
		a: &hasFunc{},
		b: &hasFunc{},
	}, {
		a: &hasFunc{f: someFunction},
		b: &hasFunc{f: someFunction},
	}, {
		a: &hasFunc{},
		b: &hasFunc{f: someFunction},
		diff: fmt.Sprintf("attributes \"f\" are different: type func():"+
			" (func())(nil) with name \"\" cannot be compared to %#v with name %q,"+
			" functions must be exactly equal or nil",
			reflect.ValueOf(someFunction),
			runtime.FuncForPC(reflect.ValueOf(someFunction).Pointer()).Name()),
	}, {
		a: &hasFunc{f: someFunction},
		b: &hasFunc{f: someOtherFunction},
		diff: fmt.Sprintf("attributes \"f\" are different: type func(): %#v with name %q"+
			" cannot be compared to %#v with name %q, functions must be exactly equal or nil",
			reflect.ValueOf(someFunction),
			runtime.FuncForPC(reflect.ValueOf(someFunction).Pointer()).Name(),
			reflect.ValueOf(someOtherFunction),
			runtime.FuncForPC(reflect.ValueOf(someOtherFunction).Pointer()).Name()),
	}, {
		a: &hasFunc{f: generatedFuncA},
		b: &hasFunc{f: generatedFuncB},
		diff: fmt.Sprintf("attributes \"f\" are different: type func(): %#v with name %q"+
			" cannot be compared to %#v with name %q, functions must be exactly equal or nil",
			reflect.ValueOf(generatedFuncA),
			runtime.FuncForPC(reflect.ValueOf(generatedFuncA).Pointer()).Name(),
			reflect.ValueOf(generatedFuncB),
			runtime.FuncForPC(reflect.ValueOf(generatedFuncB).Pointer()).Name()),
	}}
}
