// Copyright (c) 2019, Maxime Soulé
// All rights reserved.
//
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.

package td

import (
	"reflect"
	"sync"

	"github.com/maxatome/go-testdeep/internal/anchors"
	"github.com/maxatome/go-testdeep/internal/color"
)

// Anchors are stored globally by testing.TB.Name().
var allAnchors = map[string]*anchors.Info{}
var allAnchorsMu sync.Mutex

// AddAnchorableStructType declares a struct type as anchorable. fn
// is a function allowing to return a unique and identifiable instance
// of the struct type.
//
// fn has to have the following signature:
//
//	func (nextAnchor int) TYPE
//
// TYPE is the struct type to make anchorable and nextAnchor is an
// index to allow to differentiate several instances of the same type.
//
// For example, the [time.Time] type which is anchorable by default,
// could be declared as:
//
//	AddAnchorableStructType(func (nextAnchor int) time.Time {
//	  return time.Unix(int64(math.MaxInt64-1000424443-nextAnchor), 42)
//	})
//
// Just as a note, the 1000424443 constant allows to avoid to flirt
// with the math.MaxInt64 extreme limit and so avoid possible
// collision with real world values.
//
// It panics if the provided fn is not a function or if it has not the
// expected signature (see above).
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.DoAnchorsPersist], [T.ResetAnchors] and [T.SetAnchorsPersist].
func AddAnchorableStructType(fn any) {
	err := anchors.AddAnchorableStructType(fn)
	if err != nil {
		panic(color.Bad(err.Error()))
	}
}

// Anchor returns a typed value allowing to anchor the TestDeep
// operator operator in a go classic literal like a struct, slice,
// array or map value.
//
// If the TypeBehind method of operator returns non-nil, model can be
// omitted (like with [Between] operator in the example
// below). Otherwise, model should contain only one value
// corresponding to the returning type. It can be:
//   - a go value: returning type is the type of the value,
//     whatever the value is;
//   - a [reflect.Type].
//
// It returns a typed value ready to be embed in a go data structure to
// be compared using [T.Cmp] or [T.CmpLax]:
//
//	import (
//	  "testing"
//
//	  "github.com/maxatome/go-testdeep/td"
//	)
//
//	func TestFunc(tt *testing.T) {
//	  got := Func()
//
//	  t := td.NewT(tt)
//	  t.Cmp(got, &MyStruct{
//	    Name:    "Bob",
//	    Details: &MyDetails{
//	      Nick: t.Anchor(td.HasPrefix("Bobby"), "").(string),
//	      Age:  t.Anchor(td.Between(40, 50)).(int),
//	    },
//	  })
//	}
//
// In this example:
//
//   - [HasPrefix] operates on several input types (string,
//     [fmt.Stringer], error, …), so its TypeBehind method returns always
//     nil as it can not guess in advance on which type it operates. In
//     this case, we must pass "" as model parameter in order to tell it
//     to return the string type. Note that the .(string) type assertion
//     is then mandatory to conform to the strict type checking.
//   - [Between], on its side, knows the type on which it operates, as
//     it is the same as the one of its parameters. So its TypeBehind
//     method returns the right type, and so no need to pass it as model
//     parameter. Note that the .(int) type assertion is still mandatory
//     to conform to the strict type checking.
//
// Without operator anchoring feature, the previous example would have
// been:
//
//	import (
//	  "testing"
//
//	  "github.com/maxatome/go-testdeep/td"
//	)
//
//	func TestFunc(tt *testing.T) {
//	  got := Func()
//
//	  t := td.NewT(tt)
//	  t.Cmp(got, td.Struct(&MyStruct{Name: "Bob"},
//	    td.StructFields{
//	    "Details": td.Struct(&MyDetails{},
//	      td.StructFields{
//	        "Nick": td.HasPrefix("Bobby"),
//	        "Age":  td.Between(40, 50),
//	      }),
//	  }))
//	}
//
// using two times the [Struct] operator to work around the strict type
// checking of golang.
//
// By default, the value returned by Anchor can only be used in the
// next [T.Cmp] or [T.CmpLax] call. To make it persistent across calls,
// see [T.SetAnchorsPersist] and [T.AnchorsPersistTemporarily] methods.
//
// See [T.A] method for a shorter synonym of Anchor.
//
// See also [T.AnchorsPersistTemporarily], [T.DoAnchorsPersist],
// [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) Anchor(operator TestDeep, model ...any) any {
	if operator == nil {
		t.Helper()
		t.Fatal(color.Bad("Cannot anchor a nil TestDeep operator"))
	}

	var typ reflect.Type
	if len(model) > 0 {
		if len(model) != 1 {
			t.Helper()
			t.Fatal(color.TooManyParams("Anchor(OPERATOR[, MODEL])"))
		}
		var ok bool
		typ, ok = model[0].(reflect.Type)
		if !ok {
			typ = reflect.TypeOf(model[0])
			if typ == nil {
				t.Helper()
				t.Fatal(color.Bad("Untyped nil value is not valid as model for an anchor"))
			}
		}

		typeBehind := operator.TypeBehind()
		if typeBehind != nil && typeBehind != typ {
			t.Helper()
			t.Fatal(color.Bad("Operator %s TypeBehind() returned %s which differs from model type %s. Omit model or ensure its type is %[2]s",
				operator.GetLocation().Func, typeBehind, typ))
		}
	} else {
		typ = operator.TypeBehind()
		if typ == nil {
			t.Helper()
			t.Fatal(color.Bad("Cannot anchor operator %s as TypeBehind() returned nil. Use model parameter to specify the type to return",
				operator.GetLocation().Func))
		}
	}

	nvm, err := t.Config.anchors.AddAnchor(typ, reflect.ValueOf(operator))
	if err != nil {
		t.Helper()
		t.Fatal(color.Bad(err.Error()))
	}

	return nvm.Interface()
}

// A is a synonym for [T.Anchor].
//
//	import (
//	  "testing"
//
//	  "github.com/maxatome/go-testdeep/td"
//	)
//
//	func TestFunc(tt *testing.T) {
//	  got := Func()
//
//	  t := td.NewT(tt)
//	  t.Cmp(got, &MyStruct{
//	    Name:    "Bob",
//	    Details: &MyDetails{
//	      Nick: t.A(td.HasPrefix("Bobby"), "").(string),
//	      Age:  t.A(td.Between(40, 50)).(int),
//	    },
//	  })
//	}
//
// See also [T.AnchorsPersistTemporarily], [T.DoAnchorsPersist],
// [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) A(operator TestDeep, model ...any) any {
	t.Helper()
	return t.Anchor(operator, model...)
}

func (t *T) resetNonPersistentAnchors() {
	t.Config.anchors.ResetAnchors(false)
}

// ResetAnchors frees all operators anchored with [T.Anchor]
// method. Unless operators anchoring persistence has been enabled
// with [T.SetAnchorsPersist], there is no need to call this
// method. Anchored operators are automatically freed after each [Cmp],
// [CmpDeeply] and [CmpPanic] call (or others methods calling them behind
// the scene).
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.DoAnchorsPersist], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) ResetAnchors() {
	t.Config.anchors.ResetAnchors(true)
}

// AnchorsPersistTemporarily is used by helpers to temporarily enable
// anchors persistence. See [tdhttp] package for an example of use. It
// returns a function to be deferred, to restore the normal behavior
// (clear anchored operators if persistence was false, do nothing
// otherwise).
//
// Typically used as:
//
//	defer t.AnchorsPersistTemporarily()()
//	// or
//	t.Cleanup(t.AnchorsPersistTemporarily())
//
// See also [T.Anchor], [T.DoAnchorsPersist], [T.ResetAnchors],
// [T.SetAnchorsPersist] and [AddAnchorableStructType].
//
// [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp
func (t *T) AnchorsPersistTemporarily() func() {
	// If already persistent, do nothing on defer
	if t.DoAnchorsPersist() {
		return func() {}
	}

	t.SetAnchorsPersist(true)
	return func() {
		t.SetAnchorsPersist(false)
		t.Config.anchors.ResetAnchors(true)
	}
}

// DoAnchorsPersist returns true if anchors persistence is enabled,
// false otherwise.
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType].
func (t *T) DoAnchorsPersist() bool {
	return t.Config.anchors.DoAnchorsPersist()
}

// SetAnchorsPersist allows to enable or disable anchors persistence.
//
// See also [T.Anchor], [T.AnchorsPersistTemporarily],
// [T.DoAnchorsPersist], [T.ResetAnchors] and [AddAnchorableStructType].
func (t *T) SetAnchorsPersist(persist bool) {
	t.Config.anchors.SetAnchorsPersist(persist)
}

func (t *T) initAnchors() {
	if t.Config.anchors != nil {
		return
	}

	name := t.Name()

	allAnchorsMu.Lock()
	defer allAnchorsMu.Unlock()

	t.Config.anchors = allAnchors[name]
	if t.Config.anchors == nil {
		t.Config.anchors = anchors.NewInfo()
		allAnchors[name] = t.Config.anchors

		// Do not record a finalizer if no name (should not happen
		// except perhaps in tests)
		if name != "" {
			t.Cleanup(func() {
				allAnchorsMu.Lock()
				defer allAnchorsMu.Unlock()
				delete(allAnchors, name)
			})
		}
	}
}
