package internal

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"runtime"
	"sync"
	"time"

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

var errInterface = reflect.TypeOf((*error)(nil)).Elem()
var gomegaType = reflect.TypeOf((*types.Gomega)(nil)).Elem()
var contextType = reflect.TypeOf(new(context.Context)).Elem()

type formattedGomegaError interface {
	FormattedGomegaError() string
}

type asyncPolledActualError struct {
	message string
}

func (err *asyncPolledActualError) Error() string {
	return err.message
}

func (err *asyncPolledActualError) FormattedGomegaError() string {
	return err.message
}

type contextWithAttachProgressReporter interface {
	AttachProgressReporter(func() string) func()
}

type asyncGomegaHaltExecutionError struct{}

func (a asyncGomegaHaltExecutionError) GinkgoRecoverShouldIgnoreThisPanic() {}
func (a asyncGomegaHaltExecutionError) Error() string {
	return `An assertion has failed in a goroutine.  You should call 

    defer GinkgoRecover()

at the top of the goroutine that caused this panic.  This will allow Ginkgo and Gomega to correctly capture and manage this panic.`
}

type AsyncAssertionType uint

const (
	AsyncAssertionTypeEventually AsyncAssertionType = iota
	AsyncAssertionTypeConsistently
)

func (at AsyncAssertionType) String() string {
	switch at {
	case AsyncAssertionTypeEventually:
		return "Eventually"
	case AsyncAssertionTypeConsistently:
		return "Consistently"
	}
	return "INVALID ASYNC ASSERTION TYPE"
}

type AsyncAssertion struct {
	asyncType AsyncAssertionType

	actualIsFunc  bool
	actual        any
	argsToForward []any

	timeoutInterval    time.Duration
	pollingInterval    time.Duration
	mustPassRepeatedly int
	ctx                context.Context
	offset             int
	g                  *Gomega
}

func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput any, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, mustPassRepeatedly int, ctx context.Context, offset int) *AsyncAssertion {
	out := &AsyncAssertion{
		asyncType:          asyncType,
		timeoutInterval:    timeoutInterval,
		pollingInterval:    pollingInterval,
		mustPassRepeatedly: mustPassRepeatedly,
		offset:             offset,
		ctx:                ctx,
		g:                  g,
	}

	out.actual = actualInput
	if actualInput != nil && reflect.TypeOf(actualInput).Kind() == reflect.Func {
		out.actualIsFunc = true
	}

	return out
}

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

func (assertion *AsyncAssertion) WithTimeout(interval time.Duration) types.AsyncAssertion {
	assertion.timeoutInterval = interval
	return assertion
}

func (assertion *AsyncAssertion) WithPolling(interval time.Duration) types.AsyncAssertion {
	assertion.pollingInterval = interval
	return assertion
}

func (assertion *AsyncAssertion) Within(timeout time.Duration) types.AsyncAssertion {
	assertion.timeoutInterval = timeout
	return assertion
}

func (assertion *AsyncAssertion) ProbeEvery(interval time.Duration) types.AsyncAssertion {
	assertion.pollingInterval = interval
	return assertion
}

func (assertion *AsyncAssertion) WithContext(ctx context.Context) types.AsyncAssertion {
	assertion.ctx = ctx
	return assertion
}

func (assertion *AsyncAssertion) WithArguments(argsToForward ...any) types.AsyncAssertion {
	assertion.argsToForward = argsToForward
	return assertion
}

func (assertion *AsyncAssertion) MustPassRepeatedly(count int) types.AsyncAssertion {
	assertion.mustPassRepeatedly = count
	return assertion
}

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

func (assertion *AsyncAssertion) To(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	return assertion.Should(matcher, optionalDescription...)
}

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

func (assertion *AsyncAssertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	return assertion.ShouldNot(matcher, optionalDescription...)
}

func (assertion *AsyncAssertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...any) bool {
	return assertion.ShouldNot(matcher, optionalDescription...)
}

func (assertion *AsyncAssertion) 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 *AsyncAssertion) processReturnValues(values []reflect.Value) (any, error) {
	if len(values) == 0 {
		return nil, &asyncPolledActualError{
			message: fmt.Sprintf("The function passed to %s did not return any values", assertion.asyncType),
		}
	}

	actual := values[0].Interface()
	if _, ok := AsPollingSignalError(actual); ok {
		return actual, actual.(error)
	}

	var err error
	for i, extraValue := range values[1:] {
		extra := extraValue.Interface()
		if extra == nil {
			continue
		}
		if _, ok := AsPollingSignalError(extra); ok {
			return actual, extra.(error)
		}
		extraType := reflect.TypeOf(extra)
		zero := reflect.Zero(extraType).Interface()
		if reflect.DeepEqual(extra, zero) {
			continue
		}
		if i == len(values)-2 && extraType.Implements(errInterface) {
			err = extra.(error)
		}
		if err == nil {
			err = &asyncPolledActualError{
				message: fmt.Sprintf("The function passed to %s had an unexpected non-nil/non-zero return value at index %d:\n%s", assertion.asyncType, i+1, format.Object(extra, 1)),
			}
		}
	}

	return actual, err
}

func (assertion *AsyncAssertion) invalidFunctionError(t reflect.Type) error {
	return fmt.Errorf(`The function passed to %s had an invalid signature of %s.  Functions passed to %s must either:

	(a) have return values or
	(b) take a Gomega interface as their first argument and use that Gomega instance to make assertions.

You can learn more at https://onsi.github.io/gomega/#eventually
`, assertion.asyncType, t, assertion.asyncType)
}

func (assertion *AsyncAssertion) noConfiguredContextForFunctionError() error {
	return fmt.Errorf(`The function passed to %s requested a context.Context, but no context has been provided.  Please pass one in using %s().WithContext().

You can learn more at https://onsi.github.io/gomega/#eventually
`, assertion.asyncType, assertion.asyncType)
}

func (assertion *AsyncAssertion) argumentMismatchError(t reflect.Type, numProvided int) error {
	have := "have"
	if numProvided == 1 {
		have = "has"
	}
	return fmt.Errorf(`The function passed to %s has signature %s takes %d arguments but %d %s been provided.  Please use %s().WithArguments() to pass the correct set of arguments.

You can learn more at https://onsi.github.io/gomega/#eventually
`, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType)
}

func (assertion *AsyncAssertion) invalidMustPassRepeatedlyError(reason string) error {
	return fmt.Errorf(`Invalid use of MustPassRepeatedly with %s %s

You can learn more at https://onsi.github.io/gomega/#eventually
`, assertion.asyncType, reason)
}

func (assertion *AsyncAssertion) buildActualPoller() (func() (any, error), error) {
	if !assertion.actualIsFunc {
		return func() (any, error) { return assertion.actual, nil }, nil
	}
	actualValue := reflect.ValueOf(assertion.actual)
	actualType := reflect.TypeOf(assertion.actual)
	numIn, numOut, isVariadic := actualType.NumIn(), actualType.NumOut(), actualType.IsVariadic()

	if numIn == 0 && numOut == 0 {
		return nil, assertion.invalidFunctionError(actualType)
	}
	takesGomega, takesContext := false, false
	if numIn > 0 {
		takesGomega, takesContext = actualType.In(0).Implements(gomegaType), actualType.In(0).Implements(contextType)
	}
	if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) {
		takesContext = true
	}
	if takesContext && len(assertion.argsToForward) > 0 && reflect.TypeOf(assertion.argsToForward[0]).Implements(contextType) {
		takesContext = false
	}
	if !takesGomega && numOut == 0 {
		return nil, assertion.invalidFunctionError(actualType)
	}
	if takesContext && assertion.ctx == nil {
		return nil, assertion.noConfiguredContextForFunctionError()
	}

	var assertionFailure error
	inValues := []reflect.Value{}
	if takesGomega {
		inValues = append(inValues, reflect.ValueOf(NewGomega(assertion.g.DurationBundle).ConfigureWithFailHandler(func(message string, callerSkip ...int) {
			skip := 0
			if len(callerSkip) > 0 {
				skip = callerSkip[0]
			}
			_, file, line, _ := runtime.Caller(skip + 1)
			assertionFailure = &asyncPolledActualError{
				message: fmt.Sprintf("The function passed to %s failed at %s:%d with:\n%s", assertion.asyncType, file, line, message),
			}
			// we throw an asyncGomegaHaltExecutionError so that defer GinkgoRecover() can catch this error if the user makes an assertion in a goroutine
			panic(asyncGomegaHaltExecutionError{})
		})))
	}
	if takesContext {
		inValues = append(inValues, reflect.ValueOf(assertion.ctx))
	}
	for _, arg := range assertion.argsToForward {
		inValues = append(inValues, reflect.ValueOf(arg))
	}

	if !isVariadic && numIn != len(inValues) {
		return nil, assertion.argumentMismatchError(actualType, len(inValues))
	} else if isVariadic && len(inValues) < numIn-1 {
		return nil, assertion.argumentMismatchError(actualType, len(inValues))
	}

	if assertion.mustPassRepeatedly != 1 && assertion.asyncType != AsyncAssertionTypeEventually {
		return nil, assertion.invalidMustPassRepeatedlyError("it can only be used with Eventually")
	}
	if assertion.mustPassRepeatedly < 1 {
		return nil, assertion.invalidMustPassRepeatedlyError("parameter can't be < 1")
	}

	return func() (actual any, err error) {
		var values []reflect.Value
		assertionFailure = nil
		defer func() {
			if numOut == 0 && takesGomega {
				actual = assertionFailure
			} else {
				actual, err = assertion.processReturnValues(values)
				_, isAsyncError := AsPollingSignalError(err)
				if assertionFailure != nil && !isAsyncError {
					err = assertionFailure
				}
			}
			if e := recover(); e != nil {
				if _, isAsyncError := AsPollingSignalError(e); isAsyncError {
					err = e.(error)
				} else if assertionFailure == nil {
					panic(e)
				}
			}
		}()
		values = actualValue.Call(inValues)
		return
	}, nil
}

func (assertion *AsyncAssertion) afterTimeout() <-chan time.Time {
	if assertion.timeoutInterval >= 0 {
		return time.After(assertion.timeoutInterval)
	}

	if assertion.asyncType == AsyncAssertionTypeConsistently {
		return time.After(assertion.g.DurationBundle.ConsistentlyDuration)
	} else {
		if assertion.ctx == nil || assertion.g.DurationBundle.EnforceDefaultTimeoutsWhenUsingContexts {
			return time.After(assertion.g.DurationBundle.EventuallyTimeout)
		} else {
			return nil
		}
	}
}

func (assertion *AsyncAssertion) afterPolling() <-chan time.Time {
	if assertion.pollingInterval >= 0 {
		return time.After(assertion.pollingInterval)
	}
	if assertion.asyncType == AsyncAssertionTypeConsistently {
		return time.After(assertion.g.DurationBundle.ConsistentlyPollingInterval)
	} else {
		return time.After(assertion.g.DurationBundle.EventuallyPollingInterval)
	}
}

func (assertion *AsyncAssertion) matcherSaysStopTrying(matcher types.GomegaMatcher, value any) bool {
	if assertion.actualIsFunc || types.MatchMayChangeInTheFuture(matcher, value) {
		return false
	}
	return true
}

func (assertion *AsyncAssertion) pollMatcher(matcher types.GomegaMatcher, value any) (matches bool, err error) {
	defer func() {
		if e := recover(); e != nil {
			if _, isAsyncError := AsPollingSignalError(e); isAsyncError {
				err = e.(error)
			} else {
				panic(e)
			}
		}
	}()

	matches, err = matcher.Match(value)

	return
}

func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...any) bool {
	timer := time.Now()
	timeout := assertion.afterTimeout()
	lock := sync.Mutex{}

	var matches, hasLastValidActual bool
	var actual, lastValidActual any
	var actualErr, matcherErr error
	var oracleMatcherSaysStop bool

	assertion.g.THelper()

	pollActual, buildActualPollerErr := assertion.buildActualPoller()
	if buildActualPollerErr != nil {
		assertion.g.Fail(buildActualPollerErr.Error(), 2+assertion.offset)
		return false
	}

	actual, actualErr = pollActual()
	if actualErr == nil {
		lastValidActual = actual
		hasLastValidActual = true
		oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, actual)
		matches, matcherErr = assertion.pollMatcher(matcher, actual)
	}

	renderError := func(preamble string, err error) string {
		message := ""
		if pollingSignalErr, ok := AsPollingSignalError(err); ok {
			message = err.Error()
			for _, attachment := range pollingSignalErr.Attachments {
				message += fmt.Sprintf("\n%s:\n", attachment.Description)
				message += format.Object(attachment.Object, 1)
			}
		} else {
			message = preamble + "\n" + format.Object(err, 1)
		}
		return message
	}

	messageGenerator := func() string {
		// can be called out of band by Ginkgo if the user requests a progress report
		lock.Lock()
		defer lock.Unlock()
		message := ""

		if actualErr == nil {
			if matcherErr == nil {
				if desiredMatch != matches {
					if desiredMatch {
						message += matcher.FailureMessage(actual)
					} else {
						message += matcher.NegatedFailureMessage(actual)
					}
				} else {
					if assertion.asyncType == AsyncAssertionTypeConsistently {
						message += "There is no failure as the matcher passed to Consistently has not yet failed"
					} else {
						message += "There is no failure as the matcher passed to Eventually succeeded on its most recent iteration"
					}
				}
			} else {
				var fgErr formattedGomegaError
				if errors.As(actualErr, &fgErr) {
					message += fgErr.FormattedGomegaError() + "\n"
				} else {
					message += renderError(fmt.Sprintf("The matcher passed to %s returned the following error:", assertion.asyncType), matcherErr)
				}
			}
		} else {
			var fgErr formattedGomegaError
			if errors.As(actualErr, &fgErr) {
				message += fgErr.FormattedGomegaError() + "\n"
			} else {
				message += renderError(fmt.Sprintf("The function passed to %s returned the following error:", assertion.asyncType), actualErr)
			}
			if hasLastValidActual {
				message += fmt.Sprintf("\nAt one point, however, the function did return successfully.\nYet, %s failed because", assertion.asyncType)
				_, e := matcher.Match(lastValidActual)
				if e != nil {
					message += renderError(" the matcher returned the following error:", e)
				} else {
					message += " the matcher was not satisfied:\n"
					if desiredMatch {
						message += matcher.FailureMessage(lastValidActual)
					} else {
						message += matcher.NegatedFailureMessage(lastValidActual)
					}
				}
			}
		}

		description := assertion.buildDescription(optionalDescription...)
		return fmt.Sprintf("%s%s", description, message)
	}

	fail := func(preamble string) {
		assertion.g.THelper()
		assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s", preamble, time.Since(timer).Seconds(), messageGenerator()), 3+assertion.offset)
	}

	var contextDone <-chan struct{}
	if assertion.ctx != nil {
		contextDone = assertion.ctx.Done()
		if v, ok := assertion.ctx.Value("GINKGO_SPEC_CONTEXT").(contextWithAttachProgressReporter); ok {
			detach := v.AttachProgressReporter(messageGenerator)
			defer detach()
		}
	}

	// Used to count the number of times in a row a step passed
	passedRepeatedlyCount := 0
	for {
		var nextPoll <-chan time.Time = nil
		var isTryAgainAfterError = false

		for _, err := range []error{actualErr, matcherErr} {
			if pollingSignalErr, ok := AsPollingSignalError(err); ok {
				if pollingSignalErr.IsStopTrying() {
					if pollingSignalErr.IsSuccessful() {
						if assertion.asyncType == AsyncAssertionTypeEventually {
							fail("Told to stop trying (and ignoring call to Successfully(), as it is only relevant with Consistently)")
						} else {
							return true // early escape hatch for Consistently
						}
					} else {
						fail("Told to stop trying")
					}
					return false
				}
				if pollingSignalErr.IsTryAgainAfter() {
					nextPoll = time.After(pollingSignalErr.TryAgainDuration())
					isTryAgainAfterError = true
				}
			}
		}

		if actualErr == nil && matcherErr == nil && matches == desiredMatch {
			if assertion.asyncType == AsyncAssertionTypeEventually {
				passedRepeatedlyCount += 1
				if passedRepeatedlyCount == assertion.mustPassRepeatedly {
					return true
				}
			}
		} else if !isTryAgainAfterError {
			if assertion.asyncType == AsyncAssertionTypeConsistently {
				fail("Failed")
				return false
			}
			// Reset the consecutive pass count
			passedRepeatedlyCount = 0
		}

		if oracleMatcherSaysStop {
			if assertion.asyncType == AsyncAssertionTypeEventually {
				fail("No future change is possible.  Bailing out early")
				return false
			} else {
				return true
			}
		}

		if nextPoll == nil {
			nextPoll = assertion.afterPolling()
		}

		select {
		case <-nextPoll:
			a, e := pollActual()
			lock.Lock()
			actual, actualErr = a, e
			lock.Unlock()
			if actualErr == nil {
				lock.Lock()
				lastValidActual = actual
				hasLastValidActual = true
				lock.Unlock()
				oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, actual)
				m, e := assertion.pollMatcher(matcher, actual)
				lock.Lock()
				matches, matcherErr = m, e
				lock.Unlock()
			}
		case <-contextDone:
			err := context.Cause(assertion.ctx)
			if err != nil && err != context.Canceled {
				fail(fmt.Sprintf("Context was cancelled (cause: %s)", err))
			} else {
				fail("Context was cancelled")
			}
			return false
		case <-timeout:
			if assertion.asyncType == AsyncAssertionTypeEventually {
				fail("Timed out")
				return false
			} else {
				if isTryAgainAfterError {
					fail("Timed out while waiting on TryAgainAfter")
					return false
				}
				return true
			}
		}
	}
}
