package test_helpers

import (
	"fmt"
	"strings"
	"sync"

	"github.com/onsi/ginkgo/v2/formatter"
	"github.com/onsi/ginkgo/v2/internal"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/types"
)

/*

RunTracker tracks invocations of functions - useful to assert orders in which nodes run

*/

type RunTracker struct {
	lock        *sync.Mutex
	trackedRuns []string
	trackedData map[string]map[string]interface{}
}

func NewRunTracker() *RunTracker {
	return &RunTracker{
		lock:        &sync.Mutex{},
		trackedData: map[string]map[string]interface{}{},
	}
}

func (rt *RunTracker) Reset() {
	rt.lock.Lock()
	defer rt.lock.Unlock()
	rt.trackedRuns = []string{}
}

func (rt *RunTracker) Run(text string) {
	rt.lock.Lock()
	defer rt.lock.Unlock()
	rt.trackedRuns = append(rt.trackedRuns, text)
}

func (rt *RunTracker) RunWithData(text string, kv ...interface{}) {
	rt.lock.Lock()
	defer rt.lock.Unlock()
	rt.trackedRuns = append(rt.trackedRuns, text)
	data := map[string]interface{}{}
	for i := 0; i < len(kv); i += 2 {
		key := kv[i].(string)
		value := kv[i+1]
		data[key] = value
	}
	rt.trackedData[text] = data
}

func (rt *RunTracker) TrackedRuns() []string {
	rt.lock.Lock()
	defer rt.lock.Unlock()
	trackedRuns := make([]string, len(rt.trackedRuns))
	copy(trackedRuns, rt.trackedRuns)
	return trackedRuns
}

func (rt *RunTracker) DataFor(text string) map[string]interface{} {
	rt.lock.Lock()
	defer rt.lock.Unlock()
	return rt.trackedData[text]
}

func (rt *RunTracker) T(text string, callback ...func()) func() {
	return func() {
		rt.Run(text)
		if len(callback) > 0 {
			callback[0]()
		}
	}
}

func (rt *RunTracker) TSC(text string, callback ...func(internal.SpecContext)) func(internal.SpecContext) {
	return func(c internal.SpecContext) {
		rt.Run(text)
		if len(callback) > 0 {
			callback[0](c)
		}
	}
}

func (rt *RunTracker) C(text string, callback ...func()) func(args []string, additionalArgs []string) {
	return func(args []string, additionalArgs []string) {
		rt.RunWithData(text, "Args", args, "AdditionalArgs", additionalArgs)
		if len(callback) > 0 {
			callback[0]()
		}
	}
}

func HaveRun(run string) OmegaMatcher {
	return WithTransform(func(rt *RunTracker) []string {
		return rt.TrackedRuns()
	}, ContainElement(run))
}

func HaveRunWithData(run string, kv ...interface{}) OmegaMatcher {
	matchers := []types.GomegaMatcher{}
	for i := 0; i < len(kv); i += 2 {
		matchers = append(matchers, HaveKeyWithValue(kv[i], kv[i+1]))
	}
	return And(
		HaveRun(run),
		WithTransform(func(rt *RunTracker) map[string]interface{} {
			return rt.DataFor(run)
		}, And(matchers...)),
	)
}

func HaveTrackedNothing() OmegaMatcher {
	return WithTransform(func(rt *RunTracker) []string {
		return rt.TrackedRuns()
	}, BeEmpty())
}

type HaveTrackedMatcher struct {
	expectedRuns []string
	message      string
}

func (m *HaveTrackedMatcher) Match(actual interface{}) (bool, error) {
	rt, ok := actual.(*RunTracker)
	if !ok {
		return false, fmt.Errorf("HaveTracked() must be passed a RunTracker - got %T instead", actual)
	}
	actualRuns := rt.TrackedRuns()
	n := len(actualRuns)
	if n < len(m.expectedRuns) {
		n = len(m.expectedRuns)
	}
	failureMessage, success := &strings.Builder{}, true
	fmt.Fprintf(failureMessage, "{{/}}%10s == %-10s{{/}}\n", "Actual", "Expected")
	fmt.Fprintf(failureMessage, "{{/}}========================\n{{/}}")
	for i := 0; i < n; i++ {
		var expected, actual string
		if i < len(actualRuns) {
			actual = actualRuns[i]
		}
		if i < len(m.expectedRuns) {
			expected = m.expectedRuns[i]
		}
		if actual != expected {
			success = false
			fmt.Fprintf(failureMessage, "{{red}}%10s != %-10s{{/}}\n", actual, expected)
		} else {
			fmt.Fprintf(failureMessage, "{{green}}%10s == %-10s{{/}}\n", actual, expected)
		}

	}
	m.message = failureMessage.String()
	return success, nil

}
func (m *HaveTrackedMatcher) FailureMessage(actual interface{}) string {
	return "Expected runs did not match tracked runs:\n" + formatter.F(m.message)

}
func (m *HaveTrackedMatcher) NegatedFailureMessage(actual interface{}) string {
	return "Expected runs matched tracked runs:\n" + formatter.F(m.message)
}

func HaveTracked(runs ...string) OmegaMatcher {
	return &HaveTrackedMatcher{expectedRuns: runs}
}
