//go:generate hel

package onpar

import (
	"errors"
	"fmt"
	"path"
	"testing"
)

type prefs struct {
}

// Opt is an option type to pass to onpar's constructor.
type Opt func(prefs) prefs

type suite interface {
	Run()
	addRunner(runner)
	child() child
}

type child interface {
	addSpecs()
}

// Table is an entry to be used in table tests.
type Table[T, U, V any] struct {
	parent *Onpar[T, U]
	spec   func(U, V)
}

// TableSpec returns a Table type which may be used to declare table tests. The
// spec argument is the test that will be run for each entry in the table.
//
// This is effectively syntactic sugar for looping over table tests and calling
// `parent.Spec` for each entry in the table.
func TableSpec[T, U, V any](parent *Onpar[T, U], spec func(U, V)) Table[T, U, V] {
	return Table[T, U, V]{parent: parent, spec: spec}
}

// Entry adds an entry to t using entry as the value for this table entry.
func (t Table[T, U, V]) Entry(name string, entry V) Table[T, U, V] {
	t.parent.Spec(name, func(v U) {
		t.spec(v, entry)
	})
	return t
}

// FnEntry adds an entry to t that calls setup in order to get its entry value.
// The value from the BeforeEach will be passed to setup, and then both values
// will be passed to the table spec.
func (t Table[T, U, V]) FnEntry(name string, setup func(U) V) Table[T, U, V] {
	t.parent.Spec(name, func(v U) {
		entry := setup(v)
		t.spec(v, entry)
	})
	return t
}

// Onpar stores the state of the specs and groups
type Onpar[T, U any] struct {
	t TestRunner

	path []string

	parent suite

	// level is handled by (*Onpar[T]).Group(), which will adjust this field
	// each time it is called. This is how onpar knows to create nested `t.Run`
	// calls.
	level *level[T, U]

	// canBeforeEach controls which contexts BeforeEach is allowed to take this
	// suite as a parent suite.
	canBeforeEach bool

	// childSuite is assigned by BeforeEach and removed at the end of Group. If
	// BeforeEach is called twice in the same Group (or twice at the top level),
	// this is how it knows to panic.
	//
	// At the end of Group calls, childSuite.addSpecs is called, which will sync the
	// childSuite's specs to the parent.
	childSuite child
	childPath  []string

	runCalled bool
}

// TestRunner matches the methods in *testing.T that the top level onpar
// (returned from New) needs in order to work.
type TestRunner interface {
	Run(name string, fn func(*testing.T)) bool
	Failed() bool
	Cleanup(func())
}

// New creates a new Onpar suite. The top-level onpar suite must be constructed
// with this. Think `context.Background()`.
//
// It's normal to construct the top-level suite with a BeforeEach by doing the
// following:
//
//	o := BeforeEach(New(t), setupFn)
func New[T TestRunner](t T, opts ...Opt) *Onpar[*testing.T, *testing.T] {
	p := prefs{}
	for _, opt := range opts {
		p = opt(p)
	}
	o := Onpar[*testing.T, *testing.T]{
		t:             t,
		canBeforeEach: true,
		level: &level[*testing.T, *testing.T]{
			before: func(t *testing.T) *testing.T {
				return t
			},
		},
	}
	t.Cleanup(func() {
		if t.Failed() {
			return
		}
		if r := recover(); r != nil {
			panic(r)
		}
		if !o.runCalled {
			panic("onpar: Run was never called [hint: missing 'defer o.Run()'?]")
		}
	})
	return &o
}

// Run runs all of o's tests. Typically this will be called in a `defer`
// immediately after o is defined:
//
//	o := onpar.BeforeEach(onpar.New(t), setupFn)
//	defer o.Run()
func (o *Onpar[T, U]) Run() {
	if o.parent == nil {
		o.runCalled = true
		o.run(o.t)
		return
	}
	o.parent.Run()
}

// BeforeEach creates a new child Onpar suite with the requested function as the
// setup function for all specs. It requires a parent Onpar.
//
// The top level Onpar *must* have been constructed with New, otherwise the
// suite will not run.
//
// BeforeEach should be called only once for each level (i.e. each group). It
// will panic if it detects that it is overwriting another BeforeEach call for a
// given level.
func BeforeEach[T, U, V any](parent *Onpar[T, U], setup func(U) V) *Onpar[U, V] {
	if !parent.canBeforeEach {
		panic(fmt.Errorf("onpar: BeforeEach called with invalid parent: parent must either be a top-level suite or be used inside of a `parent.Group()` call"))
	}
	if !parent.correctGroup() {
		panic(fmt.Errorf("onpar: BeforeEach called with invalid parent: parent suite can only be used inside of its group (%v), but the group has exited", path.Join(parent.path...)))
	}
	if parent.child() != nil {
		if len(parent.childPath) == 0 {
			panic(errors.New("onpar: BeforeEach was called more than once at the top level"))
		}
		panic(fmt.Errorf("onpar: BeforeEach was called more than once for group '%s'", path.Join(parent.childPath...)))
	}
	path := parent.path
	if parent.level.name() != "" {
		path = append(parent.path, parent.level.name())
	}
	child := &Onpar[U, V]{
		path:   path,
		parent: parent,
		level: &level[U, V]{
			before: setup,
		},
	}
	parent.childSuite = child
	parent.childPath = child.path
	return child
}

// Spec is a test that runs in parallel with other specs.
func (o *Onpar[T, U]) Spec(name string, f func(U)) {
	if !o.correctGroup() {
		panic(fmt.Errorf("onpar: Spec called on child suite outside of its group (%v)", path.Join(o.path...)))
	}
	spec := concurrentSpec[U]{
		serialSpec: serialSpec[U]{
			specName: name,
			f:        f,
		},
	}
	o.addRunner(spec)
}

// SerialSpec is a test that runs synchronously (i.e. onpar will not call
// `t.Parallel`). While onpar is primarily a parallel testing suite, we
// recognize that sometimes a test just can't be run in parallel. When that is
// the case, use SerialSpec.
func (o *Onpar[T, U]) SerialSpec(name string, f func(U)) {
	if !o.correctGroup() {
		panic(fmt.Errorf("onpar: SerialSpec called on child suite outside of its group (%v)", path.Join(o.path...)))
	}
	spec := serialSpec[U]{
		specName: name,
		f:        f,
	}
	o.addRunner(spec)
}

func (o *Onpar[T, U]) addRunner(r runner) {
	o.level.runners = append(o.level.runners, r)
}

// Group is used to gather and categorize specs. Inside of each group, a new
// child *Onpar may be constructed using BeforeEach.
func (o *Onpar[T, U]) Group(name string, f func()) {
	if !o.correctGroup() {
		panic(fmt.Errorf("onpar: Group called on child suite outside of its group (%v)", path.Join(o.path...)))
	}
	oldLevel := o.level
	o.level = &level[T, U]{
		levelName: name,
	}
	o.canBeforeEach = true
	defer func() {
		o.canBeforeEach = false
		if o.child() != nil {
			o.child().addSpecs()
			o.childSuite = nil
		}
		oldLevel.runners = append(oldLevel.runners,
			&level[U, U]{
				levelName: o.level.name(),
				before: func(v U) U {
					return v
				},
				runners: o.level.runners,
			})
		o.level = oldLevel
	}()

	f()
}

// AfterEach is used to cleanup anything from the specs or BeforeEaches.
// AfterEach may only be called once for each *Onpar value constructed.
func (o *Onpar[T, U]) AfterEach(f func(U)) {
	if !o.correctGroup() {
		panic(fmt.Errorf("onpar: AfterEach called on child suite outside of its group (%v)", path.Join(o.path...)))
	}
	if o.level.after != nil {
		if len(o.childPath) == 0 {
			panic(errors.New("onpar: AfterEach was called more than once at top level"))
		}
		panic(fmt.Errorf("onpar: AfterEach was called more than once for group '%s'", path.Join(o.path...)))
	}
	o.level.after = f
}

func (o *Onpar[T, U]) run(t TestRunner) {
	if o.child() != nil {
		// This happens when New is called before BeforeEach, e.g.:
		//
		//     o := onpar.New()
		//     defer o.Run(t)
		//
		//     b := onpar.BeforeEach(o, setup)
		//
		// Since there's no call to o.Group, the child won't be synced, so we
		// need to do that here.
		o.child().addSpecs()
		o.childSuite = nil
	}
	top, ok := any(o.level).(groupRunner[*testing.T])
	if !ok {
		// This should be impossible - the only place that `run` is called is in
		// `New()`, which is only capable of returning `*Onpar[*testing.T,
		// *testing.T]`.
		var empty T
		panic(fmt.Errorf("onpar: run was called on a child level (type '%T' is not *testing.T)", empty))
	}
	top.runSpecs(t, func() testScope[*testing.T] {
		return baseScope{}
	})
}

func (o *Onpar[T, U]) child() child {
	return o.childSuite
}

func (o *Onpar[T, U]) correctGroup() bool {
	if o.parent == nil {
		return true
	}
	if o.parent.child() == o {
		return true
	}
	return false
}

// addSpecs is called by parent Group() calls to tell o to add its specs to its
// parent.
func (o *Onpar[T, U]) addSpecs() {
	o.parent.addRunner(o.level)
}

type testScope[T any] interface {
	before(*testing.T) T
	after()
}

type baseScope struct {
}

func (s baseScope) before(t *testing.T) *testing.T {
	return t
}

func (s baseScope) after() {}

type runner interface {
	name() string
}

type groupRunner[T any] interface {
	runner
	runSpecs(t TestRunner, scope func() testScope[T])
}

type specRunner[T any] interface {
	runner
	run(t *testing.T, scope func() testScope[T])
}

type concurrentSpec[T any] struct {
	serialSpec[T]
}

func (s concurrentSpec[T]) run(t *testing.T, scope func() testScope[T]) {
	t.Parallel()

	s.serialSpec.run(t, scope)
}

type serialSpec[T any] struct {
	specName string
	f        func(T)
}

func (s serialSpec[T]) name() string {
	return s.specName
}

func (s serialSpec[T]) run(t *testing.T, scope func() testScope[T]) {
	sc := scope()
	v := sc.before(t)
	s.f(v)
	sc.after()
}

type levelScope[T, U any] struct {
	val          U
	parentBefore func(*testing.T) T
	childBefore  func(T) U
	childAfter   func(U)
	parentAfter  func()
}

func (s *levelScope[T, U]) before(t *testing.T) U {
	parentVal := s.parentBefore(t)
	s.val = s.childBefore(parentVal)
	return s.val
}

func (s *levelScope[T, U]) after() {
	if s.childAfter != nil {
		s.childAfter(s.val)
	}
	if s.parentAfter != nil {
		s.parentAfter()
	}
}

type level[T, U any] struct {
	levelName string
	before    func(T) U
	after     func(U)
	runners   []runner
}

func (l *level[T, U]) name() string {
	return l.levelName
}

func (l *level[T, U]) runSpecs(t TestRunner, scope func() testScope[T]) {
	for _, r := range l.runners {
		childScope := func() testScope[U] {
			parentScope := scope()
			return &levelScope[T, U]{
				parentBefore: parentScope.before,
				childBefore:  l.before,
				childAfter:   l.after,
				parentAfter:  parentScope.after,
			}
		}
		switch r := r.(type) {
		case groupRunner[U]:
			if r.name() == "" {
				// If the name is empty, running the group as a sub-group would
				// result in ugly output. Just run the test function at this level
				// instead.
				r.runSpecs(t, childScope)
				return
			}
			t.Run(r.name(), func(t *testing.T) {
				r.runSpecs(t, childScope)
			})
		case specRunner[U]:
			t.Run(r.name(), func(t *testing.T) {
				r.run(t, childScope)
			})
		default:
			panic(fmt.Errorf("onpar: spec runner type [%T] is not supported", r))
		}
	}
}
