package suite

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"strconv"
	"sync"
	"testing"

	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/selection"

	"github.com/tidbcloud/eks-provider/tests/e2e/dag"
	"github.com/tidbcloud/eks-provider/tests/e2e/label"
)

// runNode actually setup env and run test cases in DAG order
type runNode struct {
	t     *testing.T
	env   label.Object[dag.Env]
	cases []label.Object[dag.Case]

	err error
}

func (n *runNode) tryToSkip() bool {
	if label.MatchState(n.env, label.StateSkipped) {
		n.err = fmt.Errorf("env %s has been skipped", n.env.Get().Name())
		return true
	}
	return false
}

func (n *runNode) failf(format string, args ...any) {
	n.t.Fail()
	n.err = fmt.Errorf(format, args...)
	n.env.Fail(n.err)
}

func (n *runNode) Run(ctx context.Context, es ...dag.Env) dag.Env {
	// env has been skipped, all cases will be skipped
	if n.tryToSkip() {
		return nil
	}

	if ctx.Err() != nil {
		n.failf("context err: %w", ctx.Err())
		return nil
	}

	env := n.env.Get()
	fmt.Println("Setup", env.Name())

	for _, e := range es {
		in := dag.NewInjector(e)
		if err := in.Inject(env); err != nil {
			n.failf("inject from parent env %s failed: %w", env.Name(), err)
			return nil
		}
	}
	if err := env.Setup(ctx); err != nil {
		n.failf("setup env err: %w", err)
		return nil
	}
	wg := sync.WaitGroup{}
	in := dag.NewInjector(env)
	for i := range n.cases {
		c := n.cases[i]
		if label.MatchState(c, label.StateSkipped) {
			continue
		}
		tc := c.Get()
		if err := in.Inject(tc); err != nil {
			n.failf("inject to case(%s) failed: %w", c.GetDesc(), err)
			return nil
		}
		wg.Add(1)
		go func(c label.Object[dag.Case]) {
			defer wg.Done()
			n.t.Run("["+env.Name()+"]"+c.GetDesc(), func(tt *testing.T) {
				tc.Test(tt)
				if tt.Failed() {
					c.Fail(fmt.Errorf("case is failed"))
				} else {
					c.Succeed()
				}
			})
		}(c)
	}

	wg.Wait()

	return env
}

func (n *runNode) WithContext(ctx context.Context) context.Context {
	wc, ok := n.env.Get().(dag.WithContext)
	if ok {
		return wc.WithContext(ctx)
	}

	return ctx
}

func (n *runNode) Skip() {
	if n.tryToSkip() {
		return
	}

	n.failf("env %s is failed to setup because of parent env is failed", n.env.Get().Name())
	for _, c := range n.cases {
		// only fail pending cases
		if label.MatchState(c, label.StatePending) {
			c.Fail(fmt.Errorf("parent env is failed to setup"))
		}
	}
}

func (n *runNode) Error() error {
	return n.err
}

func newRunNode(t *testing.T, envMap EnvMap, envName string) *runNode {
	env := envMap.GetEnv(envName)
	cases := envMap.GetCases(envName)
	return &runNode{
		t:     t,
		env:   env,
		cases: cases,
	}
}

type cleanNode struct {
	env label.Object[dag.Env]

	err error
}

func (n *cleanNode) Run(ctx context.Context, rs ...bool) bool {
	if label.MatchState(n.env, label.StateSkipped) {
		return false
	}
	c, ok := n.env.Get().(dag.Cleaner)
	if !ok {
		return false
	}
	if err := c.Cleanup(ctx); err != nil {
		n.err = err
	}
	return false
}

func (n *cleanNode) Skip() {}
func (n *cleanNode) Error() error {
	return n.err
}

type Runner interface {
	AddEnv(envs ...label.Object[dag.Env])
	AddCase(cs ...label.Object[dag.Case])

	Run(t *testing.T, selector string) error
	Cancel(err error)

	Reporter
}

type dagBuilder struct {
	envs  []label.Object[dag.Env]
	cases []label.Object[dag.Case]

	ctx    context.Context
	cancel context.CancelCauseFunc

	r Reporter
}

func NewRunner() Runner {
	ctx, cancel := context.WithCancelCause(context.Background())
	return &dagBuilder{
		ctx:    ctx,
		cancel: cancel,
	}
}

func (b *dagBuilder) AddEnv(envs ...label.Object[dag.Env]) {
	b.envs = append(b.envs, envs...)
}

func (b *dagBuilder) AddCase(cs ...label.Object[dag.Case]) {
	b.cases = append(b.cases, cs...)
}

func (b *dagBuilder) Summary() *Summary {
	return b.r.Summary()
}

func (b *dagBuilder) build() (EnvMap, error) {
	m := NewEnvMap()
	for _, env := range b.envs {
		if err := m.AddEnv(env); err != nil {
			return nil, err
		}
	}

	for _, env := range b.envs {
		for _, p := range env.Get().Envs() {
			if m.GetEnv(p) == nil {
				return nil, fmt.Errorf("env %s is not registered, cannot be a parent node for env %s", p, env.Get().Name())

			}
		}
	}

	for _, c := range b.cases {
		if err := m.AddCase(c); err != nil {
			return nil, err
		}
	}

	return m, nil
}

func (b *dagBuilder) Run(t *testing.T, s string) error {
	selector, err := labels.Parse(s)
	if err != nil {
		return err
	}
	envMap, err := b.build()
	if err != nil {
		return err
	}

	labelDAG, err := b.buildLabelDAG(selector, envMap)
	if err != nil {
		return err
	}
	// Run label DAG before building other DAGs
	labelDAG.Run(b.ctx)

	b.r = NewReporter(envMap)

	filterDAG, err := b.buildFilterDAG(selector, envMap)
	if err != nil {
		return err
	}

	runDAG, err := b.buildRunDAG(t, envMap)
	if err != nil {
		return err
	}
	cleanDAG, err := b.buildCleanDAG(envMap)
	if err != nil {
		return err
	}

	filterDAG.Run(b.ctx)

	ch := make(chan os.Signal, 2)
	signal.Notify(ch, os.Interrupt)
	go func() {
		<-ch
		fmt.Println("SIGINT received, try to clean up env, you can use Ctrl+C again to force exit")
		b.cancel(errors.New("SIGINT received"))
		<-ch
		fmt.Println("SIGINT received again, force exit, please use --seed to clean up env")
		os.Exit(1)
	}()

	runDAG.Run(b.ctx)

	t.Cleanup(func() {
		fmt.Println("Start to clean up")
		// cleanup DAG doesn't need to be interrupted by context
		cleanDAG.Run(context.Background())
	})

	return nil
}

func (b *dagBuilder) Cancel(err error) {
	b.cancel(err)
}

type newDAGNodeFunc[T any] func(envMap EnvMap, envName string) dag.DAGNode[T]

func buildDAG[T any](envMap EnvMap, newFunc newDAGNodeFunc[T], reverse bool) (dag.DAG[T], error) {
	g := dag.NewDAG[T]()
	ns := map[string]dag.DAGNode[T]{}
	for _, name := range envMap.EnvNames() {
		n := newFunc(envMap, name)
		ns[name] = n
	}
	addEdge := func(from, to dag.DAGNode[T]) error {
		if reverse {
			return g.AddEdge(to, from)
		}
		return g.AddEdge(from, to)
	}
	for _, name := range envMap.EnvNames() {
		self, ok := ns[name]
		if !ok {
			panic("unreachable")
		}

		if err := addEdge(self, nil); err != nil {
			return nil, err
		}
		parents := envMap.GetEnvParent(name, true)
		// fmt.Println("env:", name, "parents:", parents, "labels:", envMap.GetEnv(name).GetLabels())
		for _, p := range parents {
			np, ok := ns[p]
			if !ok {
				panic("unreachable")
			}
			if err := addEdge(np, self); err != nil {
				return nil, err
			}
		}
		env := envMap.GetEnv(name)
		if env == nil {
			panic("unreachable")
		}

		inner := env.Get()
		for _, p := range inner.Envs() {
			parent, ok := ns[p]
			if !ok {
				panic("unreachable")
			}
			if err := addEdge(parent, self); err != nil {
				return nil, err
			}
		}
	}

	return g, nil

}

func (b *dagBuilder) buildRunDAG(t *testing.T, envMap EnvMap) (dag.DAG[dag.Env], error) {
	return buildDAG(envMap, func(envMap EnvMap, envName string) dag.DAGNode[dag.Env] {
		return newRunNode(t, envMap, envName)
	}, false)
}

func (b *dagBuilder) buildCleanDAG(envMap EnvMap) (dag.DAG[bool], error) {
	return buildDAG(envMap, func(envMap EnvMap, envName string) dag.DAGNode[bool] {
		n := &cleanNode{
			env: envMap.GetEnv(envName),
		}
		return n
	}, true)
}

func (b *dagBuilder) buildLabelDAG(selector labels.Selector, envMap EnvMap) (dag.DAG[*envLabels], error) {
	return buildDAG(envMap, func(envMap EnvMap, envName string) dag.DAGNode[*envLabels] {
		return &labelNode{
			selector: selector,
			envMap:   envMap,
			env:      envMap.GetEnv(envName),
			cases:    envMap.GetCases(envName),
		}
	}, false)
}

func (b *dagBuilder) buildFilterDAG(selector labels.Selector, envMap EnvMap) (dag.DAG[bool], error) {
	return buildDAG(envMap, func(envMap EnvMap, envName string) dag.DAGNode[bool] {
		return &filterNode{
			selector: selector,
			env:      envMap.GetEnv(envName),
			cases:    envMap.GetCases(envName),
		}
	}, true)
}

// this node inherit labels from envs to their test cases in DAG order
type labelNode struct {
	envMap   EnvMap
	selector labels.Selector
	env      label.Object[dag.Env]
	cases    []label.Object[dag.Case]
}

type envLabels struct {
	name string
	ls   label.Labels
}

func (n *labelNode) Run(ctx context.Context, lss ...*envLabels) *envLabels {
	for _, ls := range lss {
		found := false
		for _, p := range n.env.Get().Envs() {
			if ls.name == p {
				found = true
			}
		}
		if !found && !MayMatch(n.selector, ls.ls) {
			fmt.Println("filter parent labels for", n.env.Get().Name(), ls.name, ls.ls)
			n.envMap.FilterEnvParent(n.env.Get().Name(), ls.name)
			continue
		}
		if key, hasConflict := label.HasConflicts(n.env.GetLabels(), ls.ls); hasConflict {
			// labels cannot have conflicts
			panic(fmt.Sprintf("env(%s) has conflict label with parent, key: %s", n.env.Get().Name(), key))
		}

		n.env.Labels(ls.ls)
	}

	for _, c := range n.cases {
		if key, hasConflict := label.HasConflicts(c.GetLabels(), n.env.GetLabels()); hasConflict {
			// labels cannot have conflicts
			panic(fmt.Sprintf("cases(%s) has conflict label with parent env(%s), key: %s", c.GetDesc(), n.env.Get().Name(), key))
		}

		c.Labels(n.env.GetLabels())
	}

	return &envLabels{
		name: n.env.Get().Name(),
		ls:   n.env.GetLabels(),
	}
}

func (n *labelNode) Skip() {}

func (n *labelNode) Error() error {
	return nil
}

// filter nodes in reverse DAG order
type filterNode struct {
	selector labels.Selector

	env   label.Object[dag.Env]
	cases []label.Object[dag.Case]
}

func (n *filterNode) Run(ctx context.Context, res ...bool) bool {
	allSkipped := true
	for _, c := range n.cases {
		if !n.selector.Matches(c.GetLabels()) {
			fmt.Println("skip case, env:", n.env.Get().Name(), c.GetDesc(), c.GetLabels())
			c.Skip()
		} else {
			allSkipped = false
		}
	}

	if !allSkipped {
		// not skipped
		return false
	}

	for _, skipped := range res {
		if !skipped {
			// some children are not skipped
			return false
		}
	}

	fmt.Println("skip env, env:", n.env.Get().Name(), n.env.GetLabels())
	n.env.Skip()
	return true
}

func (n *filterNode) Skip() {}

func (n *filterNode) Error() error {
	return nil
}

func MayMatch(s labels.Selector, ls labels.Labels) bool {
	reqs, ok := s.Requirements()
	if !ok {
		return false
	}

	for _, req := range reqs {
		switch req.Operator() {
		case selection.In, selection.Equals, selection.DoubleEquals:
			// may match if key does not exist
			if !ls.Has(req.Key()) {
				continue
			}
			val := ls.Get(req.Key())
			if !req.Values().Has(val) {
				return false
			}
		case selection.NotIn, selection.NotEquals:
			// may match if key does not exist
			if !ls.Has(req.Key()) {
				continue
			}
			val := ls.Get(req.Key())
			if req.Values().Has(val) {
				return false
			}
		case selection.Exists:
			if ls.Has(req.Key()) {
				continue
			}
		case selection.DoesNotExist:
			if ls.Has(req.Key()) {
				return false
			}
		case selection.GreaterThan, selection.LessThan:
			if !ls.Has(req.Key()) {
				continue
			}
			lsValue, err := strconv.ParseInt(ls.Get(req.Key()), 10, 64)
			if err != nil {
				return false
			}

			vals := req.Values().List()
			// There should be only one strValue in r.strValues, and can be converted to an integer.
			if len(vals) != 1 {
				return false
			}

			rValue, err := strconv.ParseInt(vals[0], 10, 64)
			if err != nil {
				return false
			}

			if req.Operator() == selection.GreaterThan && lsValue <= rValue {
				return false
			}

			if req.Operator() == selection.LessThan && lsValue >= rValue {
				return false
			}
		default:
			return false
		}
	}

	return true
}
