package datasource

import (
	"strings"
	"testing"

	"github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks"

	"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/launchdarkly/go-server-sdk/v6/internal/datakinds"
	"github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest"
	st "github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"

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

func TestComputeDependenciesFromFlag(t *testing.T) {
	flag1 := ldbuilders.NewFlagBuilder("key").Build()
	assert.Len(
		t,
		computeDependenciesFrom(datakinds.Features, sharedtest.FlagDescriptor(flag1)),
		0,
	)

	flag2 := ldbuilders.NewFlagBuilder("key").
		AddPrerequisite("flag2", 0).
		AddPrerequisite("flag3", 0).
		AddRule(
			ldbuilders.NewRuleBuilder().Clauses(
				ldbuilders.Clause("key", ldmodel.OperatorIn, ldvalue.String("ignore")),
				ldbuilders.SegmentMatchClause("segment1", "segment2"),
			),
		).
		AddRule(
			ldbuilders.NewRuleBuilder().Clauses(
				ldbuilders.SegmentMatchClause("segment3"),
			),
		).
		Build()
	assert.Equal(
		t,
		kindAndKeySet{
			{datakinds.Features, "flag2"}:    true,
			{datakinds.Features, "flag3"}:    true,
			{datakinds.Segments, "segment1"}: true,
			{datakinds.Segments, "segment2"}: true,
			{datakinds.Segments, "segment3"}: true,
		},
		computeDependenciesFrom(datakinds.Features, sharedtest.FlagDescriptor(flag2)),
	)

	flag3 := ldbuilders.NewFlagBuilder("key").
		AddRule(
			ldbuilders.NewRuleBuilder().Clauses(
				ldbuilders.Clause("key}", ldmodel.OperatorIn, ldvalue.String("ignore")),
				ldbuilders.SegmentMatchClause("segment1", "segment2"),
			),
		).
		Build()
	assert.Equal(
		t,
		kindAndKeySet{
			{datakinds.Segments, "segment1"}: true,
			{datakinds.Segments, "segment2"}: true,
		},
		computeDependenciesFrom(datakinds.Features, sharedtest.FlagDescriptor(flag3)),
	)
}

func TestComputeDependenciesFromSegment(t *testing.T) {
	segment := ldbuilders.NewSegmentBuilder("segment").Build()
	assert.Len(
		t,
		computeDependenciesFrom(datakinds.Segments, st.ItemDescriptor{Version: segment.Version, Item: &segment}),
		0,
	)
}

func TestComputeDependenciesFromSegmentWithSegmentReferences(t *testing.T) {
	segment1 := ldbuilders.NewSegmentBuilder("segment1").
		AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
			ldbuilders.SegmentMatchClause("segment2", "segment3"),
		)).
		Build()
	assert.Equal(
		t,
		kindAndKeySet{
			{datakinds.Segments, "segment2"}: true,
			{datakinds.Segments, "segment3"}: true,
		},
		computeDependenciesFrom(datakinds.Segments, st.ItemDescriptor{Version: segment1.Version, Item: &segment1}),
	)
}

func TestComputeDependenciesFromUnknownDataKind(t *testing.T) {
	assert.Len(
		t,
		computeDependenciesFrom(mocks.MockData, st.ItemDescriptor{Version: 1, Item: "x"}),
		0,
	)
}

func TestComputeDependenciesFromNullItem(t *testing.T) {
	assert.Len(
		t,
		computeDependenciesFrom(datakinds.Features, st.ItemDescriptor{Version: 1, Item: nil}),
		0,
	)
}

func TestSortCollectionsForDataStoreInit(t *testing.T) {
	inputData := makeDependencyOrderingDataSourceTestData()
	sortedData := sortCollectionsForDataStoreInit(inputData)
	verifySortedData(t, sortedData, inputData)
}

func TestSortCollectionsLeavesItemsOfUnknownDataKindUnchanged(t *testing.T) {
	item1 := mocks.MockDataItem{Key: "item1"}
	item2 := mocks.MockDataItem{Key: "item2"}
	flag := ldbuilders.NewFlagBuilder("a").Build()
	inputData := []st.Collection{
		{Kind: mocks.MockData,
			Items: []st.KeyedItemDescriptor{
				{Key: item1.Key, Item: item1.ToItemDescriptor()},
				{Key: item2.Key, Item: item2.ToItemDescriptor()},
			}},
		{Kind: datakinds.Features,
			Items: []st.KeyedItemDescriptor{
				{Key: "a", Item: sharedtest.FlagDescriptor(flag)},
			}},
		{Kind: datakinds.Segments, Items: nil},
	}
	sortedData := sortCollectionsForDataStoreInit(inputData)

	// the unknown data kind appears last, and the ordering of its items is unchanged
	assert.Len(t, sortedData, 3)
	assert.Equal(t, datakinds.Segments, sortedData[0].Kind)
	assert.Equal(t, datakinds.Features, sortedData[1].Kind)
	assert.Equal(t, mocks.MockData, sortedData[2].Kind)
	assert.Equal(t, inputData[0].Items, sortedData[2].Items)
}

func TestDependencyTrackerReturnsSingleValueResultForUnknownItem(t *testing.T) {
	dt := newDependencyTracker()

	// a change to any item with no known depenencies affects only itself
	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag1", kindAndKey{datakinds.Features, "flag1"})
}

func TestDependencyTrackerBuildsGraph(t *testing.T) {
	dt := newDependencyTracker()

	segment3 := ldbuilders.NewSegmentBuilder("segment3").Build()
	segment2 := ldbuilders.NewSegmentBuilder("segment2").
		AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
			ldbuilders.SegmentMatchClause(segment3.Key),
		)).
		Build()
	segment1 := ldbuilders.NewSegmentBuilder("segment1").Build()

	flag1 := ldbuilders.NewFlagBuilder("flag1").
		AddPrerequisite("flag2", 0).
		AddPrerequisite("flag3", 0).
		AddRule(
			ldbuilders.NewRuleBuilder().Clauses(
				ldbuilders.SegmentMatchClause(segment1.Key, segment2.Key),
			),
		).
		Build()

	flag2 := ldbuilders.NewFlagBuilder("flag2").
		AddPrerequisite("flag4", 0).
		AddRule(
			ldbuilders.NewRuleBuilder().Clauses(
				ldbuilders.SegmentMatchClause(segment2.Key),
			),
		).
		Build()

	for _, s := range []ldmodel.Segment{segment1, segment2, segment3} {
		dt.updateDependenciesFrom(datakinds.Segments, s.Key, sharedtest.SegmentDescriptor(s))
	}
	for _, f := range []ldmodel.FeatureFlag{flag1, flag2} {
		dt.updateDependenciesFrom(datakinds.Features, f.Key, sharedtest.FlagDescriptor(f))
	}

	// a change to flag1 affects only flag1
	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag1",
		kindAndKey{datakinds.Features, "flag1"},
	)

	// a change to flag2 affects flag2 and flag1
	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag2",
		kindAndKey{datakinds.Features, "flag2"},
		kindAndKey{datakinds.Features, "flag1"},
	)

	// a change to flag3 affects flag3 and flag1
	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
		kindAndKey{datakinds.Features, "flag3"},
		kindAndKey{datakinds.Features, "flag1"},
	)

	// a change to segment1 affects segment1 and flag1
	verifyDependencyAffectedItems(t, dt, datakinds.Segments, "segment1",
		kindAndKey{datakinds.Segments, "segment1"},
		kindAndKey{datakinds.Features, "flag1"},
	)

	// a change to segment2 affects segment2, flag1, and flag2
	verifyDependencyAffectedItems(t, dt, datakinds.Segments, "segment2",
		kindAndKey{datakinds.Segments, "segment2"},
		kindAndKey{datakinds.Features, "flag1"},
		kindAndKey{datakinds.Features, "flag2"},
	)

	// a change to segment3 affects segment2, which affects flag1 and flag2
	verifyDependencyAffectedItems(t, dt, datakinds.Segments, "segment3",
		kindAndKey{datakinds.Segments, "segment3"},
		kindAndKey{datakinds.Segments, "segment2"},
		kindAndKey{datakinds.Features, "flag1"},
		kindAndKey{datakinds.Features, "flag2"},
	)
}

func TestDependencyTrackerUpdatesGraph(t *testing.T) {
	dt := newDependencyTracker()

	flag1 := ldbuilders.NewFlagBuilder("flag1").
		AddPrerequisite("flag3", 0).
		Build()
	dt.updateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1.Version, Item: &flag1})

	flag2 := ldbuilders.NewFlagBuilder("flag2").
		AddPrerequisite("flag3", 0).
		Build()
	dt.updateDependenciesFrom(datakinds.Features, flag2.Key, st.ItemDescriptor{Version: flag2.Version, Item: &flag2})

	// at this point, a change to flag3 affects flag3, flag2, and flag1
	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
		kindAndKey{datakinds.Features, "flag3"},
		kindAndKey{datakinds.Features, "flag2"},
		kindAndKey{datakinds.Features, "flag1"},
	)

	// now make it so flag1 now depends on flag4 instead of flag2
	flag1v2 := ldbuilders.NewFlagBuilder("flag1").
		AddPrerequisite("flag4", 0).
		Build()
	dt.updateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1v2.Version, Item: &flag1v2})

	// now, a change to flag3 affects flag3 and flag2
	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
		kindAndKey{datakinds.Features, "flag3"},
		kindAndKey{datakinds.Features, "flag2"},
	)

	// and a change to flag4 affects flag4 and flag1
	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag4",
		kindAndKey{datakinds.Features, "flag4"},
		kindAndKey{datakinds.Features, "flag1"},
	)
}

func TestDependencyTrackerResetsGraph(t *testing.T) {
	dt := newDependencyTracker()

	flag1 := ldbuilders.NewFlagBuilder("flag1").
		AddPrerequisite("flag3", 0).
		Build()
	dt.updateDependenciesFrom(datakinds.Features, flag1.Key, st.ItemDescriptor{Version: flag1.Version, Item: &flag1})

	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
		kindAndKey{datakinds.Features, "flag3"},
		kindAndKey{datakinds.Features, "flag1"},
	)

	dt.reset()

	verifyDependencyAffectedItems(t, dt, datakinds.Features, "flag3",
		kindAndKey{datakinds.Features, "flag3"},
	)
}

func verifyDependencyAffectedItems(
	t *testing.T,
	dt *dependencyTracker,
	kind st.DataKind,
	key string,
	expected ...kindAndKey,
) {
	expectedSet := make(kindAndKeySet)
	for _, value := range expected {
		expectedSet.add(value)
	}
	result := make(kindAndKeySet)
	dt.addAffectedItems(result, kindAndKey{kind, key})
	assert.Equal(t, expectedSet, result)
}

func makeDependencyOrderingDataSourceTestData() []st.Collection {
	return sharedtest.NewDataSetBuilder().
		Flags(
			ldbuilders.NewFlagBuilder("a").AddPrerequisite("b", 0).AddPrerequisite("c", 0).Build(),
			ldbuilders.NewFlagBuilder("b").AddPrerequisite("c", 0).AddPrerequisite("e", 0).Build(),
			ldbuilders.NewFlagBuilder("c").Build(),
			ldbuilders.NewFlagBuilder("d").Build(),
			ldbuilders.NewFlagBuilder("e").Build(),
			ldbuilders.NewFlagBuilder("f").Build(),
		).
		Segments(
			ldbuilders.NewSegmentBuilder("1").Build(),
		).
		Build()
}

func verifySortedData(t *testing.T, sortedData []st.Collection, inputData []st.Collection) {
	assert.Len(t, sortedData, len(inputData))

	assert.Equal(t, datakinds.Segments, sortedData[0].Kind) // Segments should always be first
	assert.Equal(t, datakinds.Features, sortedData[1].Kind)

	inputDataMap := fullDataSetToMap(inputData)
	assert.Len(t, sortedData[0].Items, len(inputDataMap[datakinds.Segments]))
	assert.Len(t, sortedData[1].Items, len(inputDataMap[datakinds.Features]))

	flags := sortedData[1].Items
	findFlagIndex := func(key string) int {
		for i, item := range flags {
			if item.Key == key {
				return i
			}
		}
		return -1
	}

	for _, item := range inputData[0].Items {
		if flag, ok := item.Item.Item.(*ldmodel.FeatureFlag); ok {
			flagIndex := findFlagIndex(item.Key)
			for _, prereq := range flag.Prerequisites {
				prereqIndex := findFlagIndex(prereq.Key)
				if prereqIndex > flagIndex {
					keys := make([]string, 0, len(flags))
					for _, item := range flags {
						keys = append(keys, item.Key)
					}
					assert.True(t, false, "%s depends on %s, but %s was listed first; keys in order are [%s]",
						flag.Key, prereq.Key, strings.Join(keys, ", "))
				}
			}
		}
	}
}
