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

package generic

import (
	"context"
	"fmt"
	"path/filepath"
	"sort"
	"testing"

	"code.forgejo.org/f3/gof3/v3/kind"
	"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"
)

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

	for _, testCase := range []struct {
		path     string
		expected []string
	}{
		{
			path:     "",
			expected: []string{},
		},
		{
			path:     "/O-A",
			expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A"},
		},
		{
			path:     "/O-A/O-B",
			expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"},
		},
		{
			path:     "/O-A/O-B/O-C",
			expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C"},
		},
	} {
		t.Run(" "+testCase.path, func(t *testing.T) {
			originTree := NewMemoryTree(ctx, "O")
			testTreeBuild(t, originTree, 2)
			destinationTree := NewMemoryTree(ctx, "D")

			collected := make([]string, 0, 10)
			p := generic.NewPathFromString(testCase.path)
			upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
				fmt.Printf("origin %v destination %v\n", origin, destination)
				originPath = originPath.Append(origin)
				destinationPath = destinationPath.Append(destination)
				collected = append(collected, originPath.String()+":"+origin.String()+" => "+destinationPath.String()+":"+destination.String())
			}
			generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
			assert.EqualValues(t, testCase.expected, collected)
			collected = make([]string, 0, 10)
			generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
			assert.EqualValues(t, testCase.expected, collected)
		})
	}
}

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

	for _, testCase := range []struct {
		noremap  bool
		path     string
		expected []string
	}{
		{
			noremap:  false,
			path:     "/O-A/O-B",
			expected: []string{"/O-A:O-A=content O-A => /D-A:D-A=content O-A", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"},
		},
		{
			noremap:  true,
			path:     "/O-A/O-B",
			expected: []string{"/O-A:O-A=content O-A => /O-A:O-A=content O-A", "/O-A/O-B:O-B=content O-B => /O-A/O-B:O-B=content O-B"},
		},
	} {
		t.Run(fmt.Sprintf("noremap=%v,path=%s", testCase.noremap, testCase.path), func(t *testing.T) {
			originName := "O"
			originTree := NewMemoryTree(ctx, originName)
			testTreeBuild(t, originTree, 2)
			var destinationName string
			if testCase.noremap {
				destinationName = originName
			} else {
				destinationName = "D"
			}
			destinationTree := NewMemoryTree(ctx, destinationName)

			collected := make([]string, 0, 10)
			p := generic.NewPathFromString(testCase.path)
			upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
				fmt.Printf("origin %v destination %v\n", origin, destination)
				originPath = originPath.Append(origin)
				destinationPath = destinationPath.Append(destination)
				collected = append(collected, originPath.String()+":"+origin.String()+" => "+destinationPath.String()+":"+destination.String())
			}
			generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetNoRemap(testCase.noremap))
			assert.EqualValues(t, testCase.expected, collected)
			collected = make([]string, 0, 10)
			generic.TreeUnifyPath(ctx, originTree, p, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetNoRemap(testCase.noremap))
			assert.EqualValues(t, testCase.expected, collected)
		})
	}
}

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

	for _, testCase := range []struct {
		start       string
		destination string
		expected    []string
	}{
		{
			start:       "/O-A/O-B",
			destination: "O-C",
			expected:    []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C"},
		},
		{
			start:       "/O-A/O-B",
			destination: ".",
			expected:    []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B"},
		},
		{
			start:       "/O-A/O-B",
			destination: "../O-F/O-G",
			expected:    []string{"cd: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A:O-A=content O-A => /D-A:D-A=content O-A", "unify: /O-A/O-F:O-F=content O-F => /D-A/D-C:D-C=content O-F", "unify: /O-A/O-F/O-G:O-G=content O-G => /D-A/D-C/D-D:D-D=content O-G"},
		},
		{
			start:       "/O-A/O-B/O-C",
			destination: "../O-E",
			expected:    []string{"cd: /O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C", "unify: /O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "unify: /O-A/O-B/O-E:O-E=content O-E => /D-A/D-B/D-D:D-D=content O-E"},
		},
	} {
		t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
			originTree := NewMemoryTree(ctx, "O")
			testTreeBuild(t, originTree, 2)
			destinationTree := NewMemoryTree(ctx, "D")

			var collected []string
			start := generic.NewPathFromString(testCase.start)
			collect := func(prefix string, origin, destination generic.NodeInterface) {
				originPath := origin.GetCurrentPath().String()
				destinationPath := destination.GetCurrentPath().String()
				collected = append(collected, prefix+originPath+":"+origin.GetSelf().String()+" => "+destinationPath+":"+destination.GetSelf().String())
			}
			//
			// Unify testCase.start
			//
			upsert := func(ctx context.Context, origin generic.NodeInterface, originParent path.Path, destination generic.NodeInterface, destinationParent path.Path) {
				collect("unify: ", origin, destination)
			}
			generic.TreeUnifyPath(ctx, originTree, start, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
			//
			// Beginning from testCase.start, unify testCase.destination
			//
			cd := func(ctx context.Context, origin, destination generic.NodeInterface) {
				collect("cd: ", origin, destination)
				path := generic.NewPathFromString(testCase.destination)
				originParent := origin.GetParent().GetCurrentPath()
				destinationParent := destination.GetParent().GetCurrentPath()
				generic.NodeUnifyPath(ctx, origin.GetSelf(), originParent, path, destinationParent, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
			}
			//
			//
			//
			collected = make([]string, 0, 10)
			generic.TreeParallelApply(ctx, originTree, start, destinationTree, generic.NewParallelApplyOptions(cd))
			assert.EqualValues(t, testCase.expected, collected)
			//
			// Do it twice to verify it is idempotent
			//
			collected = make([]string, 0, 10)
			generic.TreeParallelApply(ctx, originTree, start, destinationTree, generic.NewParallelApplyOptions(cd))
			assert.EqualValues(t, testCase.expected, collected)
		})
	}
}

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

	//
	// build and populate a tree
	//
	originTree := NewMemoryTree(ctx, "O")
	testTreeBuild(t, originTree, 2)

	//
	// build an empty tree
	//
	destinationTree := NewMemoryTree(ctx, "D")

	//
	// accumulate the call results for verification
	//
	var collected []string
	upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
		originPath = originPath.Append(origin)
		destinationPath = destinationPath.Append(destination)
		what := originPath.String() + ":" + origin.String() + " => " + destinationPath.String() + ":" + destination.String()
		fmt.Printf("unify: %T => %T | %s\n", origin, destination, what)
		collected = append(collected, what)
	}

	assertTree := func(tree generic.TreeInterface, expected []string) {
		collected := make([]string, 0, 10)
		tree.Walk(ctx, generic.NewWalkOptions(func(ctx context.Context, path path.Path, node generic.NodeInterface) {
			if node.GetKind() == kind.KindRoot {
				return
			}
			path = path.Append(node)
			collected = append(collected, path.String()+":"+node.String())
		}))
		sort.Strings(collected)
		assert.EqualValues(t, expected, collected)
	}

	//
	// unify the originTree with the destinationTree on the specified path
	//
	fullPath := generic.NewPathFromString("/O-A/O-B/O-C")
	collected = make([]string, 0, 10)
	generic.TreeUnifyPath(ctx, originTree, fullPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
	sort.Strings(collected)
	assert.EqualValues(t, []string{"/O-A/O-B/O-C:O-C=content O-C => /D-A/D-B/D-C:D-C=content O-C", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A:O-A=content O-A => /D-A:D-A=content O-A"}, collected)
	assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=content O-C", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=content O-A"})

	//
	// Add a node unrelated to the unification path
	//
	var unrelatedOriginPath path.Path
	{
		originTree.Apply(ctx, generic.NewPathFromString("/O-A/O-B"), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
			assert.EqualValues(t, parent.Length()+1, node.GetCurrentPath().Length())
			unrelated := node.CreateChild(ctx)
			unrelated.Upsert(ctx)
			memory.SetContent(unrelated, "content "+unrelated.GetID().String())
			unrelated.Upsert(ctx)
			unrelatedOriginPath = unrelated.GetCurrentPath()
			assert.EqualValues(t, "/O-A/O-B/O-N", (unrelatedOriginPath.PathString().Join()))
		}))
	}

	//
	// Replace the content of the last node
	//
	lastContent := "LAST"
	{
		lastPath := generic.NewPathFromString("/O-A/O-B/O-C")
		originTree.Apply(ctx, lastPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
			memory.SetContent(node, lastContent)
		}))
		collected = make([]string, 0, 10)
		generic.TreeUnifyPath(ctx, originTree, lastPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
		sort.Strings(collected)
		assert.EqualValues(t, []string{"/O-A/O-B/O-C:O-C=LAST => /D-A/D-B/D-C:D-C=LAST", "/O-A/O-B:O-B=content O-B => /D-A/D-B:D-B=content O-B", "/O-A:O-A=content O-A => /D-A:D-A=content O-A"}, collected)
		assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=content O-A"})
	}
	//
	// Replace the content of the first node
	//
	firstContent := "FIRST"
	{
		firstPath := generic.NewPathFromString("/O-A")
		originTree.Apply(ctx, firstPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
			memory.SetContent(node, firstContent)
		}))
		collected = make([]string, 0, 10)
		generic.TreeUnifyPath(ctx, originTree, firstPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
		sort.Strings(collected)
		assert.EqualValues(t, []string{"/O-A:O-A=" + firstContent + " => /D-A:D-A=" + firstContent}, collected)
		assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=content O-B", "/D-A:D-A=FIRST"})
	}
	//
	// Replace the content of the second node
	//
	secondContent := "SECOND"
	{
		secondPath := generic.NewPathFromString("/O-A/O-B")
		originTree.Apply(ctx, secondPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
			memory.SetContent(node, secondContent)
		}))
		collected = make([]string, 0, 10)
		generic.TreeUnifyPath(ctx, originTree, secondPath, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert))
		assert.EqualValues(t, []string{"/O-A:O-A=" + firstContent + " => /D-A:D-A=" + firstContent, "/O-A/O-B:O-B=" + secondContent + " => /D-A/D-B:D-B=" + secondContent}, collected)
		sort.Strings(collected)
		assertTree(destinationTree, []string{"/D-A/D-B/D-C:D-C=LAST", "/D-A/D-B:D-B=SECOND", "/D-A:D-A=FIRST"})
	}
	//
	// verify the node unrelated to the unification is still there
	//
	{
		var found bool
		originTree.Apply(ctx, unrelatedOriginPath, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
			found = true
		}))
		assert.True(t, found)
	}
}

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

	//
	// build and populate a tree
	//
	originTree := NewMemoryTree(ctx, "O")
	log := originTree.GetLogger()
	testTreeBuild(t, originTree, 2)

	//
	// build an empty tree
	//
	destinationTree := NewMemoryTree(ctx, "D")

	upsert := func(ctx context.Context, origin generic.NodeInterface, originPath path.Path, destination generic.NodeInterface, destinationPath path.Path) {
		assert.NotNil(t, origin.GetDriver().(*memory.Driver))
		assert.NotNil(t, destination.GetDriver().(*memory.Driver))
		originPath = originPath.Append(origin)
		destinationPath = destinationPath.Append(destination)
		what := fmt.Sprintf("%s:%s => %s:%s", originPath, origin.GetSelf(), destinationPath, destination.GetSelf())
		log.Trace("mirror upsert: %T => %T | %s", origin, destination, what)
	}
	delete := func(ctx context.Context, destination generic.NodeInterface, destinationPath path.Path) {
		assert.NotNil(t, destination.GetDriver().(*memory.Driver))
		destinationPath = destinationPath.Append(destination)
		log.Trace("mirror delete: %T  | %s:%s", destination, destinationPath, destination)
	}

	var sameTree func(origin, destination generic.NodeInterface) bool
	sameTree = func(origin, destination generic.NodeInterface) bool {
		what := origin.GetCurrentPath().String() + ":" + origin.GetSelf().String() + " => " + destination.GetCurrentPath().String() + ":" + destination.GetSelf().String()
		log.Trace("sameTree: %T => %T | %s", origin.GetSelf(), destination.GetSelf(), what)
		if origin.GetMappedID() != destination.GetID() {
			log.Trace("sameTree: different: %s != %s", origin.GetMappedID(), destination.GetID())
			return false
		}
		originChildren := origin.GetChildren()
		destinationChildren := destination.GetChildren()
		if len(originChildren) != len(destinationChildren) {
			log.Trace("sameTree: different: length %v != %v", len(originChildren), len(destinationChildren))
			return false
		}

		for _, originChild := range originChildren {
			destinationChild := destination.GetChild(originChild.GetMappedID())
			if destinationChild == generic.NilNode {
				log.Trace("sameTree: different: %s not found", originChild.GetMappedID())
				return false
			}
			if !sameTree(originChild, destinationChild) {
				return false
			}
		}
		return true
	}

	//
	// unify the originTree with the destinationTree
	//
	assert.False(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
	generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
	assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
	generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
	assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))

	{
		addNode := func(tree generic.TreeInterface, pathString string) {
			tree.Apply(ctx, generic.NewPathFromString(pathString), generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
				new := node.CreateChild(ctx)
				new.Upsert(ctx)
				memory.SetContent(new, "content "+new.GetID().String())
				new.Upsert(ctx)
				log.Trace("add: %s", parent.ReadableString())
				log.Trace("add: %s", node.GetCurrentPath().ReadableString())
				log.Trace("add: %s", new.GetCurrentPath().ReadableString())
			}))
		}

		for _, testCase := range []struct {
			existingPath string
			newPath      string
		}{
			{
				existingPath: "/O-A/O-B",
				newPath:      "/D-A/D-B/D-N",
			},
			{
				existingPath: "/O-A",
				newPath:      "/D-A/D-O",
			},
			{
				existingPath: "/O-A/O-J/O-K",
				newPath:      "/D-A/D-J/D-K/D-P",
			},
		} {
			t.Run("add"+testCase.newPath, func(t *testing.T) {
				destinationPath := generic.NewPathFromString(testCase.newPath)
				addNode(originTree, testCase.existingPath)
				assert.False(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
				assert.False(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
				generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
				assert.True(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
				assert.True(t, sameTree(originTree.GetRoot(), destinationTree.GetRoot()))
			})
		}
	}

	{
		deleteNode := func(tree generic.TreeInterface, toDelete path.Path) {
			tree.Apply(ctx, toDelete, generic.NewApplyOptions(func(ctx context.Context, parent, path path.Path, node generic.NodeInterface) {
				node.Delete(ctx)
			}))
		}

		for _, testCase := range []struct {
			originPath      string
			destinationPath string
		}{
			{
				originPath:      "/O-A/O-F",
				destinationPath: "/D-A/D-F",
			},
		} {
			t.Run("delete"+testCase.originPath, func(t *testing.T) {
				originPath := generic.NewPathFromString(testCase.originPath)
				destinationPath := generic.NewPathFromString(testCase.destinationPath)
				assert.True(t, originTree.Exists(ctx, originPath))
				assert.True(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
				deleteNode(originTree, originPath)
				assert.False(t, originTree.Exists(ctx, originPath))
				generic.TreeUnify(ctx, originTree, destinationTree, generic.NewUnifyOptions(destinationTree).SetUpsert(upsert).SetDelete(delete))
				assert.False(t, destinationTree.Exists(ctx, destinationPath), destinationPath.String())
			})
		}
	}
}

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

	for _, testCase := range []struct {
		start            string
		destination      string
		expected         []string
		expectedRemapped string
	}{
		{
			start:            "/O-A/O-B",
			destination:      "O-C",
			expected:         []string{"/O-A/O-B => /D-A/D-B", "/O-A/O-B/O-C => /D-A/D-B/D-C"},
			expectedRemapped: "/D-A/D-B/D-C",
		},
		{
			start:            "/O-A/O-B/O-D",
			destination:      ".",
			expected:         []string{"/O-A/O-B/O-D => /D-A/D-B/D-D"},
			expectedRemapped: "/D-A/D-B/D-D",
		},
		{
			start:            ".",
			destination:      ".",
			expected:         []string{" => "},
			expectedRemapped: "",
		},
		{
			start:            "/O-A/O-B/O-C",
			destination:      "../O-D",
			expected:         []string{"/O-A/O-B => /D-A/D-B", "/O-A/O-B/O-D => /D-A/D-B/D-D"},
			expectedRemapped: "/D-A/D-B/D-D",
		},
		{
			start:            "/",
			destination:      ".",
			expected:         []string{" => "},
			expectedRemapped: "",
		},
	} {
		t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
			originTree := NewMemoryTree(ctx, "O")
			testTreeBuild(t, originTree, 2)
			destinationTree := NewMemoryTree(ctx, "D")

			//
			// Mirror two trees
			//
			generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions())
			//
			// collect all nodes traversed by the apply function along testCase.destination
			//
			collected := make([]string, 0, 10)
			collect := func(ctx context.Context, origin, destination generic.NodeInterface) {
				collected = append(collected, fmt.Sprintf("%s => %s", origin.GetCurrentPath().String(), destination.GetCurrentPath().String()))
			}
			//
			// get to testCase.start and from there run the apply function to reach testCase.destination
			//
			nodeApply := func(ctx context.Context, origin, destination generic.NodeInterface) {
				assert.True(t, generic.NodeParallelApply(ctx, origin, generic.NewPathFromString(testCase.destination), destination, generic.NewParallelApplyOptions(collect).SetWhere(generic.ApplyEachNode)))
			}
			assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(nodeApply)))
			assert.EqualValues(t, testCase.expected, collected)

			//
			// test TreePathRemap
			//
			remappedPath := generic.TreePathRemap(ctx, originTree, generic.NewPathFromString(filepath.Join(testCase.start, testCase.destination)), destinationTree)
			assert.EqualValues(t, testCase.expectedRemapped, remappedPath.String())
		})
	}
}

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

	for _, testCase := range []struct {
		noremap          bool
		start            string
		expected         []string
		expectedRemapped string
	}{
		{
			noremap:          false,
			start:            "/O-A/O-B",
			expected:         []string{" => ", "/O-A => /D-A", "/O-A/O-B => /D-A/D-B"},
			expectedRemapped: "/D-A/D-B",
		},
		{
			noremap:          true,
			start:            "/O-A/O-B",
			expected:         []string{" => ", "/O-A => /O-A", "/O-A/O-B => /O-A/O-B"},
			expectedRemapped: "/O-A/O-B",
		},
	} {
		t.Run(fmt.Sprintf("noremap=%v,start=%s", testCase.noremap, testCase.start), func(t *testing.T) {
			originName := "O"
			originTree := NewMemoryTree(ctx, originName)
			testTreeBuild(t, originTree, 2)
			var destinationName string
			if testCase.noremap {
				destinationName = originName
			} else {
				destinationName = "D"
			}
			destinationTree := NewMemoryTree(ctx, destinationName)

			//
			// Mirror two trees
			//
			generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions())
			//
			// collect all nodes traversed by the apply function along testCase.destination
			//
			collected := make([]string, 0, 10)
			collect := func(ctx context.Context, origin, destination generic.NodeInterface) {
				collected = append(collected, fmt.Sprintf("%s => %s", origin.GetCurrentPath(), destination.GetCurrentPath()))
			}
			//
			// get to testCase.start and from there run the apply function to reach testCase.destination
			//
			assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(collect).SetWhere(generic.ApplyEachNode).SetNoRemap(testCase.noremap)))
			assert.EqualValues(t, testCase.expected, collected)

			//
			// test TreePathRemap
			//
			remappedPath := generic.TreePathRemap(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree)
			assert.EqualValues(t, testCase.expectedRemapped, remappedPath.String())
		})
	}
}

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

	for _, testCase := range []struct {
		start       string
		destination string
	}{
		{
			start:       "/O-A",
			destination: "O-B/???",
		},
		{
			start:       "/O-A/O-B",
			destination: "???",
		},
		{
			start:       "/O-A/O-B",
			destination: "../???",
		},
	} {
		t.Run(" "+testCase.start+" => "+testCase.destination, func(t *testing.T) {
			originTree := NewMemoryTree(ctx, "O")
			testTreeBuild(t, originTree, 2)
			destinationTree := NewMemoryTree(ctx, "D")

			//
			// Mirror two trees
			//
			generic.TreeMirror(ctx, originTree, destinationTree, generic.NewPathFromString("/"), generic.NewMirrorOptions())
			//
			// get to testCase.start and from there run the apply function to reach testCase.destination
			//
			var called bool
			nodeApply := func(ctx context.Context, origin, destination generic.NodeInterface) {
				called = true
				found := generic.NodeParallelApply(ctx, origin, generic.NewPathFromString(testCase.destination), destination, generic.NewParallelApplyOptions(nil))
				assert.False(t, found)
			}
			assert.True(t, generic.TreeParallelApply(ctx, originTree, generic.NewPathFromString(testCase.start), destinationTree, generic.NewParallelApplyOptions(nodeApply)))
			assert.True(t, called)
		})
	}
}
