// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package generic

import (
	"context"
	"fmt"
	"sort"
	"testing"

	"code.forgejo.org/f3/gof3/v3/path"
	"code.forgejo.org/f3/gof3/v3/tree/generic"
	"code.forgejo.org/f3/gof3/v3/tree/memory"

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

func TestBothApplyWalk(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")

	for _, testCase := range []struct {
		path     string
		expected []string
	}{
		{
			path:     "/T-A",
			expected: []string{"/T-A", "/T-A/T-B", "/T-A/T-B/T-C", "/T-A/T-B/T-D", "/T-A/T-B/T-E", "/T-A/T-F", "/T-A/T-F/T-G", "/T-A/T-F/T-H", "/T-A/T-F/T-I", "/T-A/T-J", "/T-A/T-J/T-K", "/T-A/T-J/T-L", "/T-A/T-J/T-M"},
		},
		{
			path:     "/T-A/T-B",
			expected: []string{"/T-A/T-B", "/T-A/T-B/T-C", "/T-A/T-B/T-D", "/T-A/T-B/T-E"},
		},
		{
			path:     "/T-A/T-B/T-C",
			expected: []string{"/T-A/T-B/T-C"},
		},
	} {
		testTreeBuild(t, tree, 2)

		collected := make([]string, 0, 10)
		p := generic.NewPathFromString(testCase.path)
		walk := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
			collect := func(ctx context.Context, p path.Path, node generic.NodeInterface) {
				p = p.Append(node)
				collected = append(collected, p.String())
			}
			node.Walk(ctx, parent, generic.NewWalkOptions(collect))
		}
		assert.True(t, tree.Apply(ctx, p, generic.NewApplyOptions(walk)))
		sort.Strings(testCase.expected)
		sort.Strings(collected)
		assert.EqualValues(t, testCase.expected, collected)
	}
}

func TestWalk(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")

	expected := []string{"", "/T-A", "/T-A/T-B", "/T-A/T-B/T-C", "/T-A/T-B/T-D", "/T-A/T-B/T-E", "/T-A/T-F", "/T-A/T-F/T-G", "/T-A/T-F/T-H", "/T-A/T-F/T-I", "/T-A/T-J", "/T-A/T-J/T-K", "/T-A/T-J/T-L", "/T-A/T-J/T-M"}
	testTreeBuild(t, tree, 2)

	collected := make([]string, 0, 10)
	collect := func(ctx context.Context, p path.Path, node generic.NodeInterface) {
		p = p.Append(node)
		collected = append(collected, p.String())
	}
	tree.Walk(ctx, generic.NewWalkOptions(collect))
	sort.Strings(expected)
	sort.Strings(collected)
	assert.EqualValues(t, expected, collected)
}

func TestWalkAndGet(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")

	testTreeBuild(t, tree, 1)
	tree.Clear(ctx)

	collected := make([]string, 0, 10)
	collect := func(ctx context.Context, p path.Path, node generic.NodeInterface) {
		p = p.Append(node)
		collected = append(collected, p.String())
	}
	tree.Walk(ctx, generic.NewWalkOptions(collect))
	assert.EqualValues(t, []string{""}, collected)

	collected = make([]string, 0, 10)
	tree.WalkAndGet(ctx, generic.NewWalkOptions(collect))
	expected := []string{"", "/T-A", "/T-A/T-B", "/T-A/T-C", "/T-A/T-D"}
	sort.Strings(expected)
	sort.Strings(collected)
	assert.EqualValues(t, expected, collected)
}

func TestApplyVisitID(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")
	testTreeBuild(t, tree, 2)

	for _, testPath := range []string{"/T-A", "/T-A/T-B", "/T-A/T-B/T-C"} {

		collected := make([]string, 0, 10)
		p := generic.NewPathFromString(testPath)
		collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
			parent = parent.Append(node)
			assert.False(t, node.GetIsNil(), node.String())
			assert.EqualValues(t, parent.Length(), node.GetCurrentPath().Length())
			expected := parent.PathString().Join()
			actual := node.GetCurrentPath().String()
			assert.EqualValues(t, actual, expected)
			collected = append(collected, parent.String())
		}
		assert.True(t, tree.Apply(ctx, p, generic.NewApplyOptions(collect)))
		if assert.EqualValues(t, 1, len(collected)) {
			assert.EqualValues(t, testPath, collected[0])
		}
	}

	p := generic.NewPathFromString("/1/2/3/4")
	called := false
	assert.False(t, tree.Apply(ctx, p, generic.NewApplyOptions(func(context.Context, path.Path, path.Path, generic.NodeInterface) { called = true })))
	assert.False(t, called)
}

func TestApplyVisitByName(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")
	testTreeBuild(t, tree, 2)

	for _, testCase := range []struct {
		path     string
		expected string
	}{
		{
			path:     "/T-A/content T-B/T-C",
			expected: "/T-A/T-B/T-C",
		},
		{
			path:     "/T-A/content T-B/content T-C",
			expected: "/T-A/T-B/T-C",
		},
		{
			path:     "/content T-A/content T-B/content T-C",
			expected: "/T-A/T-B/T-C",
		},
	} {
		collected := make([]string, 0, 10)
		p := generic.NewPathFromString(testCase.path)
		collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
			parent = parent.Append(node)
			assert.False(t, node.GetIsNil(), node.String())
			assert.EqualValues(t, parent.Length(), node.GetCurrentPath().Length())
			expected := parent.PathString().Join()
			actual := node.GetCurrentPath().String()
			assert.EqualValues(t, actual, expected)
			collected = append(collected, parent.String())
		}
		assert.True(t, tree.Apply(ctx, p, generic.NewApplyOptions(collect).SetSearch(generic.ApplySearchByName)))
		if assert.EqualValues(t, 1, len(collected)) {
			assert.EqualValues(t, testCase.expected, collected[0])
		}
	}
}

func TestApplyAndGet(t *testing.T) {
	ctx := context.Background()

	for _, testCase := range []struct {
		path     string
		expected []string
	}{
		{
			path:     "/T-A",
			expected: []string{"", "/T-A"},
		},
		{
			path:     "/T-A/T-B",
			expected: []string{"", "/T-A", "/T-A/T-B"},
		},
		{
			path:     "/T-A/T-B/T-C",
			expected: []string{"", "/T-A", "/T-A/T-B", "/T-A/T-B/T-C"},
		},
	} {
		tree := NewMemoryTree(ctx, "T")

		testTreeBuild(t, tree, 2)
		tree.Clear(ctx)

		p := generic.NewPathFromString(testCase.path)

		var collected []string
		collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
			parent = parent.Append(node)
			collected = append(collected, parent.String())
		}

		{
			collected = make([]string, 0, 10)
			require.False(t, tree.Apply(ctx, p, generic.NewApplyOptions(collect)))
		}

		{
			collected = make([]string, 0, 10)
			require.True(t, tree.ApplyAndGet(ctx, p, generic.NewApplyOptions(collect)))
			require.EqualValues(t, 1, len(collected))
			assert.EqualValues(t, testCase.path, collected[0])
		}

		{
			collected = make([]string, 0, 10)
			require.True(t, tree.ApplyAndGet(ctx, p, generic.NewApplyOptions(collect).SetWhere(generic.ApplyEachNode)))
			sort.Strings(testCase.expected)
			sort.Strings(collected)
			assert.EqualValues(t, testCase.expected, collected)
		}
	}
}

func TestApplyVisitRelative(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")

	// The "destination" is clean and there is no need to test a/../b which becomes
	// b etc. Only when the first element of the path is either an id or ..
	for _, testCase := range []struct {
		start       string
		destination string
		expected    string
	}{
		{
			start:       "/T-A",
			destination: "T-B",
			expected:    "/T-A/T-B",
		},
		{
			start:       "/T-A/T-B",
			destination: ".",
			expected:    "/T-A/T-B",
		},
		{
			start:       "/T-A/T-B",
			destination: "T-C",
			expected:    "/T-A/T-B/T-C",
		},
		{
			start:       "/T-A/T-B",
			destination: "..",
			expected:    "/T-A",
		},
		{
			start:       "/T-A/T-B",
			destination: "../T-F/T-G",
			expected:    "/T-A/T-F/T-G",
		},
		{
			start:       "/T-A/T-B/T-C",
			destination: "../../T-F",
			expected:    "/T-A/T-F",
		},
	} {
		t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
			testTreeBuild(t, tree, 2)

			collected := make([]string, 0, 10)
			start := generic.NewPathFromString(testCase.start)
			collect := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
				parent = parent.Append(node)
				assert.False(t, node.GetIsNil(), node.String())
				assert.EqualValues(t, parent.Length(), node.GetCurrentPath().Length())
				expected := parent.PathString().Join()
				actual := node.GetCurrentPath().String()
				assert.EqualValues(t, actual, expected)
				collected = append(collected, parent.String())
			}
			cd := func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
				destination := generic.NewPathFromString(testCase.destination)
				fmt.Println("start ", node.GetCurrentPath().String())
				assert.True(t, node.Apply(ctx, parent, destination, generic.NewApplyOptions(collect)))
			}
			assert.True(t, tree.Apply(ctx, start, generic.NewApplyOptions(cd)))
			if assert.EqualValues(t, 1, len(collected)) {
				assert.EqualValues(t, testCase.expected, collected[0])
			}
		})

		p := generic.NewPathFromString("/1/2/3/4")
		called := false
		assert.False(t, tree.Apply(ctx, p, generic.NewApplyOptions(func(context.Context, path.Path, path.Path, generic.NodeInterface) { called = true })))
		assert.False(t, called)
	}
}

func TestApplyUpsert(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")

	testTreeBuild(t, tree, 2)

	expected := generic.NewPathFromString("/T-A/T-B/T-N")
	assert.False(t, tree.Exists(ctx, expected))

	assert.True(t, tree.Apply(ctx, generic.NewPathFromString("/T-A/T-B"), generic.NewApplyOptions(func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
		new := node.CreateChild(ctx)
		new.Upsert(ctx)
		memory.SetContent(new, "content "+new.GetID().String())
		new.Upsert(ctx)
	})))

	assert.True(t, tree.Exists(ctx, expected))
}

func TestApplyDelete(t *testing.T) {
	ctx := context.Background()

	tree := NewMemoryTree(ctx, "T")

	testTreeBuild(t, tree, 2)

	toDelete := generic.NewPathFromString("/T-A/T-B")
	assert.True(t, tree.Exists(ctx, toDelete))

	assert.True(t, tree.Apply(ctx, toDelete, generic.NewApplyOptions(func(ctx context.Context, parent, p path.Path, node generic.NodeInterface) {
		node.Delete(ctx)
	})))

	assert.False(t, tree.Exists(ctx, toDelete))
}
