package walker

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

	"github.com/pkg/errors"
	"github.com/restic/restic/internal/restic"
)

// TestTree is used to construct a list of trees for testing the walker.
type TestTree map[string]interface{}

// TestNode is used to test the walker.
type TestFile struct {
	Size uint64
}

func BuildTreeMap(tree TestTree) (m TreeMap, root restic.ID) {
	m = TreeMap{}
	id := buildTreeMap(tree, m)
	return m, id
}

func buildTreeMap(tree TestTree, m TreeMap) restic.ID {
	tb := restic.NewTreeJSONBuilder()
	var names []string
	for name := range tree {
		names = append(names, name)
	}
	sort.Strings(names)

	for _, name := range names {
		item := tree[name]
		switch elem := item.(type) {
		case TestFile:
			err := tb.AddNode(&restic.Node{
				Name: name,
				Type: "file",
				Size: elem.Size,
			})
			if err != nil {
				panic(err)
			}
		case TestTree:
			id := buildTreeMap(elem, m)
			err := tb.AddNode(&restic.Node{
				Name:    name,
				Subtree: &id,
				Type:    "dir",
			})
			if err != nil {
				panic(err)
			}
		default:
			panic(fmt.Sprintf("invalid type %T", elem))
		}
	}

	buf, err := tb.Finalize()
	if err != nil {
		panic(err)
	}

	id := restic.Hash(buf)

	if _, ok := m[id]; !ok {
		m[id] = buf
	}

	return id
}

// TreeMap returns the trees from the map on LoadTree.
type TreeMap map[restic.ID][]byte

func (t TreeMap) LoadBlob(_ context.Context, tpe restic.BlobType, id restic.ID, _ []byte) ([]byte, error) {
	if tpe != restic.TreeBlob {
		return nil, errors.New("can only load trees")
	}
	tree, ok := t[id]
	if !ok {
		return nil, errors.New("tree not found")
	}
	return tree, nil
}

func (t TreeMap) Connections() uint {
	return 2
}

// checkFunc returns a function suitable for walking the tree to check
// something, and a function which will check the final result.
type checkFunc func(t testing.TB) (walker WalkFunc, final func(testing.TB))

// checkItemOrder ensures that the order of the 'path' arguments is the one passed in as 'want'.
func checkItemOrder(want []string) checkFunc {
	pos := 0
	return func(t testing.TB) (walker WalkFunc, final func(testing.TB)) {
		walker = func(treeID restic.ID, path string, node *restic.Node, err error) (bool, error) {
			if err != nil {
				t.Errorf("error walking %v: %v", path, err)
				return false, err
			}

			if pos >= len(want) {
				t.Errorf("additional unexpected path found: %v", path)
				return false, nil
			}

			if path != want[pos] {
				t.Errorf("wrong path found, want %q, got %q", want[pos], path)
			}
			pos++
			return false, nil
		}

		final = func(t testing.TB) {
			if pos != len(want) {
				t.Errorf("not enough items returned, want %d, got %d", len(want), pos)
			}
		}

		return walker, final
	}
}

// checkParentTreeOrder ensures that the order of the 'parentID' arguments is the one passed in as 'want'.
func checkParentTreeOrder(want []string) checkFunc {
	pos := 0
	return func(t testing.TB) (walker WalkFunc, final func(testing.TB)) {
		walker = func(treeID restic.ID, path string, node *restic.Node, err error) (bool, error) {
			if err != nil {
				t.Errorf("error walking %v: %v", path, err)
				return false, err
			}

			if pos >= len(want) {
				t.Errorf("additional unexpected parent tree ID found: %v", treeID)
				return false, nil
			}

			if treeID.String() != want[pos] {
				t.Errorf("wrong parent tree ID found, want %q, got %q", want[pos], treeID.String())
			}
			pos++
			return false, nil
		}

		final = func(t testing.TB) {
			if pos != len(want) {
				t.Errorf("not enough items returned, want %d, got %d", len(want), pos)
			}
		}

		return walker, final
	}
}

// checkSkipFor returns ErrSkipNode if path is in skipFor, it checks that the
// paths the walk func is called for are exactly the ones in wantPaths.
func checkSkipFor(skipFor map[string]struct{}, wantPaths []string) checkFunc {
	var pos int

	return func(t testing.TB) (walker WalkFunc, final func(testing.TB)) {
		walker = func(treeID restic.ID, path string, node *restic.Node, err error) (bool, error) {
			if err != nil {
				t.Errorf("error walking %v: %v", path, err)
				return false, err
			}

			if pos >= len(wantPaths) {
				t.Errorf("additional unexpected path found: %v", path)
				return false, nil
			}

			if path != wantPaths[pos] {
				t.Errorf("wrong path found, want %q, got %q", wantPaths[pos], path)
			}
			pos++

			if _, ok := skipFor[path]; ok {
				return false, ErrSkipNode
			}

			return false, nil
		}

		final = func(t testing.TB) {
			if pos != len(wantPaths) {
				t.Errorf("wrong number of paths returned, want %d, got %d", len(wantPaths), pos)
			}
		}

		return walker, final
	}
}

// checkIgnore returns ErrSkipNode if path is in skipFor and sets ignore according
// to ignoreFor. It checks that the paths the walk func is called for are exactly
// the ones in wantPaths.
func checkIgnore(skipFor map[string]struct{}, ignoreFor map[string]bool, wantPaths []string) checkFunc {
	var pos int

	return func(t testing.TB) (walker WalkFunc, final func(testing.TB)) {
		walker = func(treeID restic.ID, path string, node *restic.Node, err error) (bool, error) {
			if err != nil {
				t.Errorf("error walking %v: %v", path, err)
				return false, err
			}

			if pos >= len(wantPaths) {
				t.Errorf("additional unexpected path found: %v", path)
				return ignoreFor[path], nil
			}

			if path != wantPaths[pos] {
				t.Errorf("wrong path found, want %q, got %q", wantPaths[pos], path)
			}
			pos++

			if _, ok := skipFor[path]; ok {
				return ignoreFor[path], ErrSkipNode
			}

			return ignoreFor[path], nil
		}

		final = func(t testing.TB) {
			if pos != len(wantPaths) {
				t.Errorf("wrong number of paths returned, want %d, got %d", len(wantPaths), pos)
			}
		}

		return walker, final
	}
}

func TestWalker(t *testing.T) {
	var tests = []struct {
		tree   TestTree
		checks []checkFunc
	}{
		{
			tree: TestTree{
				"foo": TestFile{},
				"subdir": TestTree{
					"subfile": TestFile{},
				},
			},
			checks: []checkFunc{
				checkItemOrder([]string{
					"/",
					"/foo",
					"/subdir",
					"/subdir/subfile",
				}),
				checkParentTreeOrder([]string{
					"a760536a8fd64dd63f8dd95d85d788d71fd1bee6828619350daf6959dcb499a0", // tree /
					"a760536a8fd64dd63f8dd95d85d788d71fd1bee6828619350daf6959dcb499a0", // tree /
					"a760536a8fd64dd63f8dd95d85d788d71fd1bee6828619350daf6959dcb499a0", // tree /
					"670046b44353a89b7cd6ef84c78422232438f10eb225c29c07989ae05283d797", // tree /subdir
				}),
				checkSkipFor(
					map[string]struct{}{
						"/subdir": {},
					}, []string{
						"/",
						"/foo",
						"/subdir",
					},
				),
				checkIgnore(
					map[string]struct{}{}, map[string]bool{
						"/subdir": true,
					}, []string{
						"/",
						"/foo",
						"/subdir",
						"/subdir/subfile",
					},
				),
			},
		},
		{
			tree: TestTree{
				"foo": TestFile{},
				"subdir1": TestTree{
					"subfile1": TestFile{},
				},
				"subdir2": TestTree{
					"subfile2": TestFile{},
					"subsubdir2": TestTree{
						"subsubfile3": TestFile{},
					},
				},
			},
			checks: []checkFunc{
				checkItemOrder([]string{
					"/",
					"/foo",
					"/subdir1",
					"/subdir1/subfile1",
					"/subdir2",
					"/subdir2/subfile2",
					"/subdir2/subsubdir2",
					"/subdir2/subsubdir2/subsubfile3",
				}),
				checkParentTreeOrder([]string{
					"7a0e59b986cc83167d9fbeeefc54e4629770124c5825d391f7ee0598667fcdf1", // tree /
					"7a0e59b986cc83167d9fbeeefc54e4629770124c5825d391f7ee0598667fcdf1", // tree /
					"7a0e59b986cc83167d9fbeeefc54e4629770124c5825d391f7ee0598667fcdf1", // tree /
					"22c9feefa0b9fabc7ec5383c90cfe84ba714babbe4d2968fcb78f0ec7612e82f", // tree /subdir1
					"7a0e59b986cc83167d9fbeeefc54e4629770124c5825d391f7ee0598667fcdf1", // tree /
					"9bfe4aab3ac0ad7a81909355d7221801441fb20f7ed06c0142196b3f10358493", // tree /subdir2
					"9bfe4aab3ac0ad7a81909355d7221801441fb20f7ed06c0142196b3f10358493", // tree /subdir2
					"6b962fef064ef9beecc27dfcd6e0f2e7beeebc9c1f1f4f477d4af59fc45f411d", // tree /subdir2/subsubdir2
				}),
				checkSkipFor(
					map[string]struct{}{
						"/subdir1": {},
					}, []string{
						"/",
						"/foo",
						"/subdir1",
						"/subdir2",
						"/subdir2/subfile2",
						"/subdir2/subsubdir2",
						"/subdir2/subsubdir2/subsubfile3",
					},
				),
				checkSkipFor(
					map[string]struct{}{
						"/subdir1":            {},
						"/subdir2/subsubdir2": {},
					}, []string{
						"/",
						"/foo",
						"/subdir1",
						"/subdir2",
						"/subdir2/subfile2",
						"/subdir2/subsubdir2",
					},
				),
				checkSkipFor(
					map[string]struct{}{
						"/foo": {},
					}, []string{
						"/",
						"/foo",
					},
				),
			},
		},
		{
			tree: TestTree{
				"foo": TestFile{},
				"subdir1": TestTree{
					"subfile1": TestFile{},
					"subfile2": TestFile{},
					"subfile3": TestFile{},
				},
				"subdir2": TestTree{
					"subfile1": TestFile{},
					"subfile2": TestFile{},
					"subfile3": TestFile{},
				},
				"subdir3": TestTree{
					"subfile1": TestFile{},
					"subfile2": TestFile{},
					"subfile3": TestFile{},
				},
				"zzz other": TestFile{},
			},
			checks: []checkFunc{
				checkItemOrder([]string{
					"/",
					"/foo",
					"/subdir1",
					"/subdir1/subfile1",
					"/subdir1/subfile2",
					"/subdir1/subfile3",
					"/subdir2",
					"/subdir2/subfile1",
					"/subdir2/subfile2",
					"/subdir2/subfile3",
					"/subdir3",
					"/subdir3/subfile1",
					"/subdir3/subfile2",
					"/subdir3/subfile3",
					"/zzz other",
				}),
				checkParentTreeOrder([]string{
					"c2efeff7f217a4dfa12a16e8bb3cefedd37c00873605c29e5271c6061030672f", // tree /
					"c2efeff7f217a4dfa12a16e8bb3cefedd37c00873605c29e5271c6061030672f", // tree /
					"c2efeff7f217a4dfa12a16e8bb3cefedd37c00873605c29e5271c6061030672f", // tree /
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir1
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir1
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir1
					"c2efeff7f217a4dfa12a16e8bb3cefedd37c00873605c29e5271c6061030672f", // tree /
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir2
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir2
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir2
					"c2efeff7f217a4dfa12a16e8bb3cefedd37c00873605c29e5271c6061030672f", // tree /
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir3
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir3
					"57ee8960c7a86859b090a76e5d013f83d10c0ce11d5460076ca8468706f784ab", // tree /subdir3
					"c2efeff7f217a4dfa12a16e8bb3cefedd37c00873605c29e5271c6061030672f", // tree /
				}),
				checkIgnore(
					map[string]struct{}{
						"/subdir1": {},
					}, map[string]bool{
						"/subdir1": true,
					}, []string{
						"/",
						"/foo",
						"/subdir1",
						"/zzz other",
					},
				),
				checkIgnore(
					map[string]struct{}{}, map[string]bool{
						"/subdir1": true,
					}, []string{
						"/",
						"/foo",
						"/subdir1",
						"/subdir1/subfile1",
						"/subdir1/subfile2",
						"/subdir1/subfile3",
						"/zzz other",
					},
				),
				checkIgnore(
					map[string]struct{}{
						"/subdir2": {},
					}, map[string]bool{
						"/subdir2": true,
					}, []string{
						"/",
						"/foo",
						"/subdir1",
						"/subdir1/subfile1",
						"/subdir1/subfile2",
						"/subdir1/subfile3",
						"/subdir2",
						"/zzz other",
					},
				),
				checkIgnore(
					map[string]struct{}{}, map[string]bool{
						"/subdir1/subfile1": true,
						"/subdir1/subfile2": true,
						"/subdir1/subfile3": true,
					}, []string{
						"/",
						"/foo",
						"/subdir1",
						"/subdir1/subfile1",
						"/subdir1/subfile2",
						"/subdir1/subfile3",
						"/zzz other",
					},
				),
				checkIgnore(
					map[string]struct{}{}, map[string]bool{
						"/subdir2/subfile1": true,
						"/subdir2/subfile2": true,
						"/subdir2/subfile3": true,
					}, []string{
						"/",
						"/foo",
						"/subdir1",
						"/subdir1/subfile1",
						"/subdir1/subfile2",
						"/subdir1/subfile3",
						"/subdir2",
						"/subdir2/subfile1",
						"/subdir2/subfile2",
						"/subdir2/subfile3",
						"/zzz other",
					},
				),
			},
		},
		{
			tree: TestTree{
				"subdir1": TestTree{},
				"subdir2": TestTree{},
				"subdir3": TestTree{
					"file": TestFile{},
				},
				"subdir4": TestTree{
					"file": TestFile{},
				},
				"subdir5": TestTree{},
				"subdir6": TestTree{},
			},
			checks: []checkFunc{
				checkItemOrder([]string{
					"/",
					"/subdir1",
					"/subdir2",
					"/subdir3",
					"/subdir3/file",
					"/subdir4",
					"/subdir4/file",
					"/subdir5",
					"/subdir6",
				}),
			},
		},
		{
			tree: TestTree{
				"subdir1": TestTree{},
				"subdir2": TestTree{},
				"subdir3": TestTree{
					"file": TestFile{},
				},
				"subdir4": TestTree{},
				"subdir5": TestTree{
					"file": TestFile{},
				},
				"subdir6": TestTree{},
			},
			checks: []checkFunc{
				checkIgnore(
					map[string]struct{}{}, map[string]bool{
						"/subdir2": true,
					}, []string{
						"/",
						"/subdir1",
						"/subdir2",
						"/subdir3",
						"/subdir3/file",
						"/subdir5",
						"/subdir5/file",
					},
				),
			},
		},
	}

	for _, test := range tests {
		t.Run("", func(t *testing.T) {
			repo, root := BuildTreeMap(test.tree)
			for _, check := range test.checks {
				t.Run("", func(t *testing.T) {
					ctx, cancel := context.WithCancel(context.TODO())
					defer cancel()

					fn, last := check(t)
					err := Walk(ctx, repo, root, restic.NewIDSet(), fn)
					if err != nil {
						t.Error(err)
					}
					last(t)
				})
			}
		})
	}
}
