package matchers

import (
	"fmt"
	"reflect"

	"github.com/onsi/gomega/format"
)

type SatisfyMatcher struct {
	Predicate any

	// cached type
	predicateArgType reflect.Type
}

func NewSatisfyMatcher(predicate any) *SatisfyMatcher {
	if predicate == nil {
		panic("predicate cannot be nil")
	}
	predicateType := reflect.TypeOf(predicate)
	if predicateType.Kind() != reflect.Func {
		panic("predicate must be a function")
	}
	if predicateType.NumIn() != 1 {
		panic("predicate must have 1 argument")
	}
	if predicateType.NumOut() != 1 || predicateType.Out(0).Kind() != reflect.Bool {
		panic("predicate must return bool")
	}

	return &SatisfyMatcher{
		Predicate:        predicate,
		predicateArgType: predicateType.In(0),
	}
}

func (m *SatisfyMatcher) Match(actual any) (success bool, err error) {
	// prepare a parameter to pass to the predicate
	var param reflect.Value
	if actual != nil && reflect.TypeOf(actual).AssignableTo(m.predicateArgType) {
		// The dynamic type of actual is compatible with the predicate argument.
		param = reflect.ValueOf(actual)

	} else if actual == nil && m.predicateArgType.Kind() == reflect.Interface {
		// The dynamic type of actual is unknown, so there's no way to make its
		// reflect.Value. Create a nil of the predicate argument, which is known.
		param = reflect.Zero(m.predicateArgType)

	} else {
		return false, fmt.Errorf("predicate expects '%s' but we have '%T'", m.predicateArgType, actual)
	}

	// call the predicate with `actual`
	fn := reflect.ValueOf(m.Predicate)
	result := fn.Call([]reflect.Value{param})
	return result[0].Bool(), nil
}

func (m *SatisfyMatcher) FailureMessage(actual any) (message string) {
	return format.Message(actual, "to satisfy predicate", m.Predicate)
}

func (m *SatisfyMatcher) NegatedFailureMessage(actual any) (message string) {
	return format.Message(actual, "to not satisfy predicate", m.Predicate)
}
