package evaluation

import (
	"fmt"
	"testing"

	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
	"github.com/launchdarkly/go-sdk-common/v3/ldreason"
	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"

	"github.com/stretchr/testify/assert"
)

const basicUserKey = "userkey"

type simpleBigSegmentProvider struct {
	getMembership         func(contextKey string) BigSegmentMembership
	getStatus             func(contextKey string) ldreason.BigSegmentsStatus
	membershipKeysQueried []string
}

func basicBigSegmentsProvider() *simpleBigSegmentProvider {
	return &simpleBigSegmentProvider{}
}

func (s *simpleBigSegmentProvider) GetMembership(contextKey string) (BigSegmentMembership, ldreason.BigSegmentsStatus) {
	s.membershipKeysQueried = append(s.membershipKeysQueried, contextKey)
	var membership BigSegmentMembership
	if s.getMembership != nil {
		membership = s.getMembership(contextKey)
	}
	status := ldreason.BigSegmentsHealthy
	if s.getStatus != nil {
		status = s.getStatus(contextKey)
	}
	return membership, status
}

func (s *simpleBigSegmentProvider) withStatus(status ldreason.BigSegmentsStatus) *simpleBigSegmentProvider {
	return &simpleBigSegmentProvider{
		getStatus:     func(string) ldreason.BigSegmentsStatus { return status },
		getMembership: s.getMembership,
	}
}

func (s *simpleBigSegmentProvider) withStatusForKey(key string, status ldreason.BigSegmentsStatus) *simpleBigSegmentProvider {
	previousGetStatus := s.getStatus
	return &simpleBigSegmentProvider{
		getStatus: func(queriedKey string) ldreason.BigSegmentsStatus {
			if key == queriedKey {
				return status
			}
			if previousGetStatus != nil {
				return previousGetStatus(queriedKey)
			}
			return ldreason.BigSegmentsHealthy
		},
		getMembership: s.getMembership,
	}
}

func (s *simpleBigSegmentProvider) withMembership(
	key string,
	membership *simpleMembership,
) *simpleBigSegmentProvider {
	previousGetMembership := s.getMembership
	return &simpleBigSegmentProvider{
		getStatus: s.getStatus,
		getMembership: func(queriedKey string) BigSegmentMembership {
			if key == queriedKey {
				return membership
			}
			if previousGetMembership != nil {
				return previousGetMembership(queriedKey)
			}
			return nil
		},
	}
}

type simpleMembership struct {
	segmentChecks []string
	included      []string
	excluded      []string
}

func (m *simpleMembership) CheckMembership(segmentRef string) ldvalue.OptionalBool {
	m.segmentChecks = append(m.segmentChecks, segmentRef)
	for _, inc := range m.included {
		if inc == segmentRef {
			return ldvalue.NewOptionalBool(true)
		}
	}
	for _, exc := range m.excluded {
		if exc == segmentRef {
			return ldvalue.NewOptionalBool(false)
		}
	}
	return ldvalue.OptionalBool{}
}

func basicMembership() *simpleMembership { return &simpleMembership{} }

func (s *simpleMembership) include(segmentRefs ...string) *simpleMembership {
	s.included = append(s.included, segmentRefs...)
	return s
}

func (s *simpleMembership) exclude(segmentRefs ...string) *simpleMembership {
	s.excluded = append(s.excluded, segmentRefs...)
	return s
}

func TestBigSegmentWithNoProviderIsNotMatched(t *testing.T) {
	evaluator := NewEvaluator(
		basicDataProvider().withStoredSegments(
			ldbuilders.NewSegmentBuilder("segmentkey").
				Unbounded(true).
				Generation(1).
				Included(basicUserKey). // Included should be ignored for a big segment
				Build(),
		),
	)
	f := makeBooleanFlagToMatchAnyOfSegments("segmentkey")

	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
	assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
	assert.Equal(t, ldreason.BigSegmentsNotConfigured, result.Detail.Reason.GetBigSegmentsStatus())
}

func TestBigSegmentWithNoGenerationIsNotMatched(t *testing.T) {
	segment := ldbuilders.NewSegmentBuilder("segmentkey").
		Unbounded(true). // but we didn't set Generation
		Build()
	evaluator := NewEvaluatorWithOptions(
		basicDataProvider().withStoredSegments(segment),
		EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().withMembership(basicUserKey,
			basicMembership().include(makeBigSegmentRef(&segment)))),
	)
	f := makeBooleanFlagToMatchAnyOfSegments(segment.Key)

	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
	assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
	assert.Equal(t, ldreason.BigSegmentsNotConfigured, result.Detail.Reason.GetBigSegmentsStatus())
}

func TestBigSegmentMatch(t *testing.T) {
	contextKey := "contextkey"
	segmentKey := "segmentkey"
	flag := makeBooleanFlagToMatchAnyOfSegments(segmentKey)
	makeEvaluator := func(segment ldmodel.Segment, contextMembership *simpleMembership) Evaluator {
		return NewEvaluatorWithOptions(
			basicDataProvider().withStoredSegments(segment),
			EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().withMembership(contextKey, contextMembership)),
		)
	}
	for _, contextKind := range []ldcontext.Kind{"", ldcontext.DefaultKind, "other"} {
		for _, isMultiKind := range []bool{false, true} {
			t.Run(fmt.Sprintf("kind=%s, isMultiKind=%t", contextKind, isMultiKind), func(t *testing.T) {
				context := ldcontext.NewWithKind(contextKind, contextKey)
				if isMultiKind {
					context = ldcontext.NewMulti(context, ldcontext.NewWithKind("irrelevantKind", "irrelevantKey"))
				}
				contextWithoutDesiredKind := ldcontext.NewWithKind("irrelevantKind", contextKey)
				if isMultiKind {
					contextWithoutDesiredKind = ldcontext.NewMulti(contextWithoutDesiredKind,
						ldcontext.NewWithKind("irrelevantKind2", "irrelevantKey"))
				}

				t.Run("includes", func(t *testing.T) {
					segment := ldbuilders.NewSegmentBuilder(segmentKey).
						Unbounded(true).
						UnboundedContextKind(contextKind).
						Generation(2).
						Build()
					evaluator := makeEvaluator(segment, basicMembership().include(makeBigSegmentRef(&segment)))

					t.Run("matched by include", func(t *testing.T) {
						result := evaluator.Evaluate(&flag, context, nil)
						assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
						assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
					})

					t.Run("unmatched if context does not have specified kind", func(t *testing.T) {
						result := evaluator.Evaluate(&flag, contextWithoutDesiredKind, nil)
						assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
						// BigSegmentsStatus should *not* have been set because we don't even do the big segment store
						// query if the context doesn't have the right kind.
						assert.Equal(t, ldreason.BigSegmentsStatus(""), result.Detail.Reason.GetBigSegmentsStatus())
					})
				})

				t.Run("rule match", func(t *testing.T) {
					segment := ldbuilders.NewSegmentBuilder(segmentKey).
						Unbounded(true).
						UnboundedContextKind(contextKind).
						Generation(2).
						AddRule(
							ldbuilders.NewSegmentRuleBuilder().Clauses(makeClauseToMatchAnyContextOfKind(contextKind)),
						).
						Build()
					evaluator := makeEvaluator(segment, basicMembership())

					t.Run("matched by rule", func(t *testing.T) {
						result := evaluator.Evaluate(&flag, context, nil)
						assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
						assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
					})

					t.Run("rules ignored if context does not have specified kind", func(t *testing.T) {
						result := evaluator.Evaluate(&flag, contextWithoutDesiredKind, nil)
						assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
						// BigSegmentsStatus should *not* have been set because we don't even do the big segment store
						// query if the context doesn't have the right kind.
						assert.Equal(t, ldreason.BigSegmentsStatus(""), result.Detail.Reason.GetBigSegmentsStatus())
					})

					t.Run("exclude takes priority over rule", func(t *testing.T) {
						evaluatorWithExclude := makeEvaluator(segment,
							basicMembership().exclude(makeBigSegmentRef(&segment)))
						result := evaluatorWithExclude.Evaluate(&flag, context, nil)
						assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
						assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
					})
				})
			})
		}
	}
}

func TestBigSegmentIsMatchedWithRuleWhenSegmentDataForUserShowsNoMatch(t *testing.T) {
	segment := ldbuilders.NewSegmentBuilder("segmentkey").
		Unbounded(true).
		Generation(2).
		AddRule(ldbuilders.NewSegmentRuleBuilder().
			Clauses(ldbuilders.Clause(ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(basicUserKey)))).
		Build()
	evaluator := NewEvaluatorWithOptions(
		basicDataProvider().withStoredSegments(segment),
		EvaluatorOptionBigSegmentProvider(
			basicBigSegmentsProvider().withMembership(basicUserKey, basicMembership())),
	)
	f := makeBooleanFlagToMatchAnyOfSegments(segment.Key)

	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
	assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
	assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
}

func TestBigSegmentStatusIsReturnedFromProvider(t *testing.T) {
	segment := ldbuilders.NewSegmentBuilder("segmentkey").
		Unbounded(true).
		Generation(2).
		Build()
	evaluator := NewEvaluatorWithOptions(
		basicDataProvider().withStoredSegments(segment),
		EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().
			withMembership(basicUserKey, basicMembership().include(makeBigSegmentRef(&segment))).
			withStatus(ldreason.BigSegmentsStale)),
	)
	f := makeBooleanFlagToMatchAnyOfSegments(segment.Key)

	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
	assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
	assert.Equal(t, ldreason.BigSegmentsStale, result.Detail.Reason.GetBigSegmentsStatus())
}

func TestBigSegmentStateIsQueriedOnlyOncePerUniqueContextKey(t *testing.T) {
	segmentKey1, segmentKey2 := "segmentKey1", "segmentKey2"
	flag := makeBooleanFlagToMatchAllOfSegments(segmentKey1, segmentKey2)
	// Note: in this flag configuration, both segment keys are referenced in one clause, so if
	// segmentKey1 is a match then we should still also see it testing segmentKey2.

	t.Run("single context kind", func(t *testing.T) {
		contextKey := "contextKey"
		context := ldcontext.New(contextKey)
		segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
		segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).Build()
		membership := basicMembership().include(makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2))
		bigSegmentsProvider := basicBigSegmentsProvider().withMembership(contextKey, membership)
		evaluator := NewEvaluatorWithOptions(
			basicDataProvider().withStoredSegments(segment1, segment2),
			EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
		)

		result := evaluator.Evaluate(&flag, context, nil)

		assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
		assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
		assert.Equal(t, []string{contextKey}, bigSegmentsProvider.membershipKeysQueried)
		assert.Equal(t, []string{makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2)}, membership.segmentChecks)
	})

	t.Run("two context kinds referenced, both have same key", func(t *testing.T) {
		contextKey := "contextKey"
		otherKind := ldcontext.Kind("other")
		context := ldcontext.NewMulti(
			ldcontext.New(contextKey), // default context kind
			ldcontext.NewWithKind(otherKind, contextKey),
		)
		segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
		segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).
			UnboundedContextKind(otherKind).Build()
		membership := basicMembership().include(makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2))
		bigSegmentsProvider := basicBigSegmentsProvider().withMembership(contextKey, membership)
		evaluator := NewEvaluatorWithOptions(
			basicDataProvider().withStoredSegments(segment1, segment2),
			EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
		)

		result := evaluator.Evaluate(&flag, context, nil)

		assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
		assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
		assert.Equal(t, []string{contextKey}, bigSegmentsProvider.membershipKeysQueried)
		assert.Equal(t, []string{makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2)}, membership.segmentChecks)
	})

	t.Run("two context kinds referenced, each with a different key", func(t *testing.T) {
		contextKey1, contextKey2 := "contextKey1", "contextKey2"
		otherKind := ldcontext.Kind("other")
		context := ldcontext.NewMulti(
			ldcontext.New(contextKey1), // default context kind
			ldcontext.NewWithKind(otherKind, contextKey2),
		)
		segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
		segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).
			UnboundedContextKind(otherKind).Build()
		membershipForKey1 := basicMembership().include(makeBigSegmentRef(&segment1))
		membershipForKey2 := basicMembership().include(makeBigSegmentRef(&segment2))
		bigSegmentsProvider := basicBigSegmentsProvider().
			withMembership(contextKey1, membershipForKey1).
			withMembership(contextKey2, membershipForKey2)
		evaluator := NewEvaluatorWithOptions(
			basicDataProvider().withStoredSegments(segment1, segment2),
			EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
		)

		result := evaluator.Evaluate(&flag, context, nil)

		assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
		assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
		assert.Equal(t, []string{contextKey1, contextKey2}, bigSegmentsProvider.membershipKeysQueried)
		assert.Equal(t, []string{makeBigSegmentRef(&segment1)}, membershipForKey1.segmentChecks)
		assert.Equal(t, []string{makeBigSegmentRef(&segment2)}, membershipForKey2.segmentChecks)
	})
}

func TestBigSegmentStatusWithMultipleQueries(t *testing.T) {
	// A single evaluation could end up doing more than one big segments query if there are two different
	// context keys involved. If those queries don't return the same status, we want to make sure we
	// report whichever status is most problematic: StoreError is the worst, Stale is the second worst.
	segmentKey1, segmentKey2 := "segmentKey1", "segmentKey2"
	contextKey1, contextKey2 := "contextKey1", "contextKey2"
	otherKind := ldcontext.Kind("other")
	context := ldcontext.NewMulti(
		ldcontext.New(contextKey1), // default context kind
		ldcontext.NewWithKind(otherKind, contextKey2),
	)
	segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
	segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).
		UnboundedContextKind(otherKind).Build()
	membershipForKey1 := basicMembership().include(makeBigSegmentRef(&segment1))
	membershipForKey2 := basicMembership().include(makeBigSegmentRef(&segment2))
	flag := makeBooleanFlagToMatchAllOfSegments(segmentKey1, segmentKey2)

	type params struct{ status1, status2, expected ldreason.BigSegmentsStatus }
	var allParams []params
	allStatuses := []ldreason.BigSegmentsStatus{ldreason.BigSegmentsHealthy, ldreason.BigSegmentsStale,
		ldreason.BigSegmentsStoreError, ldreason.BigSegmentsNotConfigured}
	for i := 0; i < len(allStatuses)-1; i++ {
		better, worse := allStatuses[i], allStatuses[i+1]
		allParams = append(allParams, params{better, worse, worse})
		allParams = append(allParams, params{worse, better, worse})
	}
	for _, p := range allParams {
		t.Run(fmt.Sprintf("%s, %s", p.status1, p.status2), func(t *testing.T) {
			bigSegmentsProvider := basicBigSegmentsProvider().
				withMembership(contextKey1, membershipForKey1).
				withMembership(contextKey2, membershipForKey2).
				withStatusForKey(contextKey1, p.status1).
				withStatusForKey(contextKey2, p.status2)
			evaluator := NewEvaluatorWithOptions(
				basicDataProvider().withStoredSegments(segment1, segment2),
				EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
			)

			result := evaluator.Evaluate(&flag, context, nil)

			assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
			assert.Equal(t, []string{contextKey1, contextKey2}, bigSegmentsProvider.membershipKeysQueried)
			assert.Equal(t, p.expected, result.Detail.Reason.GetBigSegmentsStatus())
		})
	}
}

func TestBigSegmentStatusIsReturnedWhenBigSegmentWasReferencedFromPrerequisiteFlag(t *testing.T) {
	segment := ldbuilders.NewSegmentBuilder("segmentkey").
		Unbounded(true).
		Generation(2).
		Build()

	f1 := makeBooleanFlagToMatchAnyOfSegments(segment.Key)
	f0 := ldbuilders.NewFlagBuilder("feature0").
		On(true).
		Variations(ldvalue.Bool(false), ldvalue.Bool(true)).FallthroughVariation(1).
		AddPrerequisite(f1.Key, 1).
		Build()

	evaluator := NewEvaluatorWithOptions(
		basicDataProvider().withStoredFlags(f1).withStoredSegments(segment),
		EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().
			withMembership(basicUserKey, basicMembership().include(makeBigSegmentRef(&segment))).
			withStatus(ldreason.BigSegmentsStale)),
	)

	result := evaluator.Evaluate(&f0, ldcontext.New(basicUserKey), nil)
	assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
	assert.Equal(t, ldreason.BigSegmentsStale, result.Detail.Reason.GetBigSegmentsStatus())
}
