package convey

import (
	"fmt"

	"github.com/jtolds/gls"
	"github.com/smartystreets/goconvey/convey/reporting"
)

type conveyErr struct {
	fmt    string
	params []any
}

func (e *conveyErr) Error() string {
	return fmt.Sprintf(e.fmt, e.params...)
}

func conveyPanic(fmt string, params ...any) {
	panic(&conveyErr{fmt, params})
}

const (
	missingGoTest = `Top-level calls to Convey(...) need a reference to the *testing.T.
		Hint: Convey("description here", t, func() { /* notice that the second argument was the *testing.T (t)! */ }) `
	extraGoTest    = `Only the top-level call to Convey(...) needs a reference to the *testing.T.`
	noStackContext = "Convey operation made without context on goroutine stack.\n" +
		"Hint: Perhaps you meant to use `Convey(..., func(c C){...})` ?"
	differentConveySituations = "Different set of Convey statements on subsequent pass!\nDid not expect %#v."
	multipleIdenticalConvey   = "Multiple convey suites with identical names: %#v"
)

const (
	failureHalt = "___FAILURE_HALT___"

	nodeKey = "node"
)

///////////////////////////////// Stack Context /////////////////////////////////

func getCurrentContext() *context {
	ctx, ok := ctxMgr.GetValue(nodeKey)
	if ok {
		return ctx.(*context)
	}
	return nil
}

func mustGetCurrentContext() *context {
	ctx := getCurrentContext()
	if ctx == nil {
		conveyPanic(noStackContext)
	}
	return ctx
}

//////////////////////////////////// Context ////////////////////////////////////

// context magically handles all coordination of Convey's and So assertions.
//
// It is tracked on the stack as goroutine-local-storage with the gls package,
// or explicitly if the user decides to call convey like:
//
//   Convey(..., func(c C) {
//     c.So(...)
//   })
//
// This implements the `C` interface.
type context struct {
	reporter reporting.Reporter

	children map[string]*context

	resets []func()

	executedOnce   bool
	expectChildRun *bool
	complete       bool

	focus       bool
	failureMode FailureMode
	stackMode   StackMode
}

// rootConvey is the main entry point to a test suite. This is called when
// there's no context in the stack already, and items must contain a `t` object,
// or this panics.
func rootConvey(items ...any) {
	entry := discover(items)

	if entry.Test == nil {
		conveyPanic(missingGoTest)
	}

	expectChildRun := true
	ctx := &context{
		reporter: buildReporter(),

		children: make(map[string]*context),

		expectChildRun: &expectChildRun,

		focus:       entry.Focus,
		failureMode: defaultFailureMode.combine(entry.FailMode),
		stackMode:   defaultStackMode.combine(entry.StackMode),
	}
	ctxMgr.SetValues(gls.Values{nodeKey: ctx}, func() {
		ctx.reporter.BeginStory(reporting.NewStoryReport(entry.Test))
		defer ctx.reporter.EndStory()

		for ctx.shouldVisit() {
			ctx.conveyInner(entry.Situation, entry.Func)
			expectChildRun = true
		}
	})
}

//////////////////////////////////// Methods ////////////////////////////////////

func (ctx *context) SkipConvey(items ...any) {
	ctx.Convey(items, skipConvey)
}

func (ctx *context) FocusConvey(items ...any) {
	ctx.Convey(items, focusConvey)
}

func (ctx *context) Convey(items ...any) {
	entry := discover(items)

	// we're a branch, or leaf (on the wind)
	if entry.Test != nil {
		conveyPanic(extraGoTest)
	}
	if ctx.focus && !entry.Focus {
		return
	}

	var inner_ctx *context
	if ctx.executedOnce {
		var ok bool
		inner_ctx, ok = ctx.children[entry.Situation]
		if !ok {
			conveyPanic(differentConveySituations, entry.Situation)
		}
	} else {
		if _, ok := ctx.children[entry.Situation]; ok {
			conveyPanic(multipleIdenticalConvey, entry.Situation)
		}
		inner_ctx = &context{
			reporter: ctx.reporter,

			children: make(map[string]*context),

			expectChildRun: ctx.expectChildRun,

			focus:       entry.Focus,
			failureMode: ctx.failureMode.combine(entry.FailMode),
			stackMode:   ctx.stackMode.combine(entry.StackMode),
		}
		ctx.children[entry.Situation] = inner_ctx
	}

	if inner_ctx.shouldVisit() {
		ctxMgr.SetValues(gls.Values{nodeKey: inner_ctx}, func() {
			inner_ctx.conveyInner(entry.Situation, entry.Func)
		})
	}
}

func (ctx *context) SkipSo(stuff ...any) {
	ctx.assertionReport(reporting.NewSkipReport())
}

func (ctx *context) So(actual any, assert Assertion, expected ...any) {
	if result := assert(actual, expected...); result == assertionSuccess {
		ctx.assertionReport(reporting.NewSuccessReport())
	} else {
		ctx.assertionReport(reporting.NewFailureReport(result, ctx.shouldShowStack()))
	}
}

func (ctx *context) SoMsg(msg string, actual any, assert Assertion, expected ...any) {
	if result := assert(actual, expected...); result == assertionSuccess {
		ctx.assertionReport(reporting.NewSuccessReport())
		return
	} else {
		ctx.reporter.Enter(reporting.NewScopeReport(msg))
		defer ctx.reporter.Exit()
		ctx.assertionReport(reporting.NewFailureReport(result, ctx.shouldShowStack()))
	}
}

func (ctx *context) Reset(action func()) {
	/* TODO: Failure mode configuration */
	ctx.resets = append(ctx.resets, action)
}

func (ctx *context) Print(items ...any) (int, error) {
	fmt.Fprint(ctx.reporter, items...)
	return fmt.Print(items...)
}

func (ctx *context) Println(items ...any) (int, error) {
	fmt.Fprintln(ctx.reporter, items...)
	return fmt.Println(items...)
}

func (ctx *context) Printf(format string, items ...any) (int, error) {
	fmt.Fprintf(ctx.reporter, format, items...)
	return fmt.Printf(format, items...)
}

//////////////////////////////////// Private ////////////////////////////////////

// shouldVisit returns true iff we should traverse down into a Convey. Note
// that just because we don't traverse a Convey this time, doesn't mean that
// we may not traverse it on a subsequent pass.
func (c *context) shouldVisit() bool {
	return !c.complete && *c.expectChildRun
}

func (c *context) shouldShowStack() bool {
	return c.stackMode == StackFail
}

// conveyInner is the function which actually executes the user's anonymous test
// function body. At this point, Convey or RootConvey has decided that this
// function should actually run.
func (ctx *context) conveyInner(situation string, f func(C)) {
	// Record/Reset state for next time.
	defer func() {
		ctx.executedOnce = true

		// This is only needed at the leaves, but there's no harm in also setting it
		// when returning from branch Convey's
		*ctx.expectChildRun = false
	}()

	// Set up+tear down our scope for the reporter
	ctx.reporter.Enter(reporting.NewScopeReport(situation))
	defer ctx.reporter.Exit()

	// Recover from any panics in f, and assign the `complete` status for this
	// node of the tree.
	defer func() {
		ctx.complete = true
		if problem := recover(); problem != nil {
			if problem, ok := problem.(*conveyErr); ok {
				panic(problem)
			}
			if problem != failureHalt {
				ctx.reporter.Report(reporting.NewErrorReport(problem))
			}
		} else {
			for _, child := range ctx.children {
				if !child.complete {
					ctx.complete = false
					return
				}
			}
		}
	}()

	// Resets are registered as the `f` function executes, so nil them here.
	// All resets are run in registration order (FIFO).
	ctx.resets = []func(){}
	defer func() {
		for _, r := range ctx.resets {
			// panics handled by the previous defer
			r()
		}
	}()

	if f == nil {
		// if f is nil, this was either a Convey(..., nil), or a SkipConvey
		ctx.reporter.Report(reporting.NewSkipReport())
	} else {
		f(ctx)
	}
}

// assertionReport is a helper for So and SkipSo which makes the report and
// then possibly panics, depending on the current context's failureMode.
func (ctx *context) assertionReport(r *reporting.AssertionResult) {
	ctx.reporter.Report(r)
	if r.Failure != "" && ctx.failureMode == FailureHalts {
		panic(failureHalt)
	}
}
