package ldevents

import (
	"encoding/json"

	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
	"github.com/launchdarkly/go-sdk-common/v3/ldreason"
	"github.com/launchdarkly/go-sdk-common/v3/ldtime"
	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"

	"github.com/launchdarkly/go-test-helpers/v3/jsonhelpers"
	m "github.com/launchdarkly/go-test-helpers/v3/matchers"
)

func equalNumericTime(unixTime ldtime.UnixMillisecondTime) m.Matcher {
	// To avoid problems with mismatch of numeric types, it's simplest to use JSONEqual which compares as a JSON number
	return m.JSONEqual(unixTime)
}

func eventKindIs(kind string) m.Matcher {
	return m.JSONProperty("kind").Should(m.Equal(kind))
}

func anyIndexEvent() m.Matcher {
	return eventKindIs("index")
}

func anyIdentifyEvent() m.Matcher {
	return eventKindIs("identify")
}

func anyFeatureEvent() m.Matcher {
	return eventKindIs("feature")
}

func anyCustomEvent() m.Matcher {
	return eventKindIs("custom")
}

func anySummaryEvent() m.Matcher {
	return eventKindIs("summary")
}

func identifyEventForContextKey(key string) m.Matcher {
	return m.AllOf(
		eventKindIs("identify"),
		m.JSONProperty("context").Should(m.JSONProperty("key").Should(m.Equal(key))),
	)
}

func indexEventForContextKey(key string) m.Matcher {
	return m.AllOf(
		eventKindIs("index"),
		m.JSONProperty("context").Should(m.JSONProperty("key").Should(m.Equal(key))),
	)
}

func featureEventForFlag(flag FlagEventProperties) m.Matcher {
	return m.AllOf(
		m.JSONProperty("kind").Should(m.Equal("feature")),
		m.JSONProperty("key").Should(m.Equal(flag.Key)))
}

func featureEventWithAllProperties(sourceEvent EvaluationData, flag FlagEventProperties) m.Matcher {
	return matchFeatureOrDebugEvent(sourceEvent, flag, false, nil)
}

func debugEventWithAllProperties(sourceEvent EvaluationData, flag FlagEventProperties, contextJSON interface{}) m.Matcher {
	return matchFeatureOrDebugEvent(sourceEvent, flag, true, contextJSON)
}

func matchFeatureOrDebugEvent(sourceEvent EvaluationData, flag FlagEventProperties,
	debug bool, inlineContext interface{}) m.Matcher {
	props := map[string]interface{}{
		"kind":         "feature",
		"key":          flag.Key,
		"creationDate": sourceEvent.CreationDate,
		"version":      flag.Version,
		"value":        sourceEvent.Value,
		"default":      nil,
	}
	if debug {
		props["kind"] = "debug"
	}
	if sourceEvent.Variation.IsDefined() {
		props["variation"] = sourceEvent.Variation.IntValue()
	}
	if sourceEvent.Reason.GetKind() != "" {
		props["reason"] = json.RawMessage(jsonhelpers.ToJSON(sourceEvent.Reason))
	}
	if inlineContext == nil {
		props["contextKeys"] = expectedContextKeys(sourceEvent.Context.context)
	} else {
		props["context"] = inlineContext
	}
	return m.JSONEqual(props)
}

func customEventWithEventKey(eventKey string) m.Matcher {
	return m.AllOf(
		eventKindIs("custom"),
		m.JSONProperty("key").Should(m.Equal(eventKey)),
	)
}

func summaryEventWithFlag(flag FlagEventProperties, counterProps ...[]m.Matcher) m.Matcher {
	counters := make([]m.Matcher, 0, len(counterProps))
	for _, cp := range counterProps {
		counters = append(counters, m.AllOf(
			append(cp, m.JSONProperty("version").Should(m.Equal(flag.Version)))...,
		))
	}
	return m.AllOf(
		m.JSONProperty("kind").Should(m.Equal("summary")),
		m.JSONProperty("features").Should(
			m.JSONProperty(flag.Key).Should(
				m.JSONProperty("counters").Should(m.ItemsInAnyOrder(counters...)),
			),
		),
	)
}

func summaryCounterProps(variation ldvalue.OptionalInt, value ldvalue.Value, count int) []m.Matcher {
	return []m.Matcher{
		m.JSONProperty("value").Should(m.JSONEqual(value)),
		m.JSONProperty("count").Should(m.Equal(count)),
		m.JSONOptProperty("variation").Should(m.JSONEqual(variation)),
	}
}

func summaryCounterPropsFromEval(evalDetail ldreason.EvaluationDetail, count int) []m.Matcher {
	return summaryCounterProps(evalDetail.VariationIndex, evalDetail.Value, count)
}

func valueIsPositiveNonZeroInteger() m.Matcher {
	return m.New(
		func(value interface{}) bool {
			v := ldvalue.Parse(jsonhelpers.ToJSON(value))
			return v.IsInt() && v.IntValue() > 0
		},
		func() string {
			return "is an int > 0"
		},
		func(value interface{}) string {
			return "was not an int or was negative"
		},
	)
}

func expectedContextKeys(c ldcontext.Context) map[string]string {
	ret := make(map[string]string)
	for i := 0; i < c.IndividualContextCount(); i++ {
		if ic := c.IndividualContextByIndex(i); ic.IsDefined() {
			ret[string(ic.Kind())] = ic.Key()
		}
	}
	return ret
}
