package internal

import (
	"fmt"
	"reflect"

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

type Assertion struct {
	actuals     []any    // actual value plus all extra values
	actualIndex int      // value to pass to the matcher
	vet         vetinari // the vet to call before calling Gomega matcher
	offset      int
	g           *Gomega
}

// ...obligatory discworld reference, as "vetineer" doesn't sound ... quite right.
type vetinari func(assertion *Assertion, optionalDescription ...any) bool

func NewAssertion(actualInput any, g *Gomega, offset int, extra ...any) *Assertion {
	return &Assertion{
		actuals:     append([]any{actualInput}, extra...),
		actualIndex: 0,
		vet:         (*Assertion).vetActuals,
		offset:      offset,
		g:           g,
	}
}

func (assertion *Assertion) WithOffset(offset int) types.Assertion {
	assertion.offset = offset
	return assertion
}

func (assertion *Assertion) Error() types.Assertion {
	return &Assertion{
		actuals:     assertion.actuals,
		actualIndex: len(assertion.actuals) - 1,
		vet:         (*Assertion).vetError,
		offset:      assertion.offset,
		g:           assertion.g,
	}
}

func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	assertion.g.THelper()
	vetOptionalDescription("Assertion", optionalDescription...)
	return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
}

func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	assertion.g.THelper()
	vetOptionalDescription("Assertion", optionalDescription...)
	return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
}

func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	assertion.g.THelper()
	vetOptionalDescription("Assertion", optionalDescription...)
	return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
}

func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	assertion.g.THelper()
	vetOptionalDescription("Assertion", optionalDescription...)
	return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
}

func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	assertion.g.THelper()
	vetOptionalDescription("Assertion", optionalDescription...)
	return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
}

func (assertion *Assertion) buildDescription(optionalDescription ...any) string {
	switch len(optionalDescription) {
	case 0:
		return ""
	case 1:
		if describe, ok := optionalDescription[0].(func() string); ok {
			return describe() + "\n"
		}
	}
	return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
}

func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...any) bool {
	actualInput := assertion.actuals[assertion.actualIndex]
	matches, err := matcher.Match(actualInput)
	assertion.g.THelper()
	if err != nil {
		description := assertion.buildDescription(optionalDescription...)
		assertion.g.Fail(description+err.Error(), 2+assertion.offset)
		return false
	}
	if matches != desiredMatch {
		var message string
		if desiredMatch {
			message = matcher.FailureMessage(actualInput)
		} else {
			message = matcher.NegatedFailureMessage(actualInput)
		}
		description := assertion.buildDescription(optionalDescription...)
		assertion.g.Fail(description+message, 2+assertion.offset)
		return false
	}

	return true
}

// vetActuals vets the actual values, with the (optional) exception of a
// specific value, such as the first value in case non-error assertions, or the
// last value in case of Error()-based assertions.
func (assertion *Assertion) vetActuals(optionalDescription ...any) bool {
	success, message := vetActuals(assertion.actuals, assertion.actualIndex)
	if success {
		return true
	}

	description := assertion.buildDescription(optionalDescription...)
	assertion.g.THelper()
	assertion.g.Fail(description+message, 2+assertion.offset)
	return false
}

// vetError vets the actual values, except for the final error value, in case
// the final error value is non-zero. Otherwise, it doesn't vet the actual
// values, as these are allowed to take on any values unless there is a non-zero
// error value.
func (assertion *Assertion) vetError(optionalDescription ...any) bool {
	if err := assertion.actuals[assertion.actualIndex]; err != nil {
		// Go error result idiom: all other actual values must be zero values.
		return assertion.vetActuals(optionalDescription...)
	}
	return true
}

// vetActuals vets a slice of actual values, optionally skipping a particular
// value slice element, such as the first or last value slice element.
func vetActuals(actuals []any, skipIndex int) (bool, string) {
	for i, actual := range actuals {
		if i == skipIndex {
			continue
		}
		if actual != nil {
			zeroValue := reflect.Zero(reflect.TypeOf(actual)).Interface()
			if !reflect.DeepEqual(zeroValue, actual) {
				var message string
				if err, ok := actual.(error); ok {
					message = fmt.Sprintf("Unexpected error: %s\n%s", err, format.Object(err, 1))
				} else {
					message = fmt.Sprintf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i, actual, actual)
				}
				return false, message
			}
		}
	}
	return true, ""
}
