package choose_files

import (
	"fmt"
	"io/fs"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/kovidgoyal/kitty/tools/ignorefiles"
	"github.com/kovidgoyal/kitty/tools/utils"
)

var _ = fmt.Print

func TestAsLower(t *testing.T) {
	buf := [512]byte{}
	for _, q := range []string{
		"abc", "aBc", "aBCCf83Dx", "mOoseÇa", "89ÇĞxxA", "", "23", "aIİBc",
	} {
		n := as_lower(q, buf[:])
		actual := utils.UnsafeBytesToString(buf[:n])
		if diff := cmp.Diff(strings.ToLower(q), actual); diff != "" {
			t.Fatalf("Failed to lowercase: %#v\n%s", q, diff)
		}
	}
}

type node struct {
	name     string
	children map[string]*node
	data     string
}

func (n node) Name() string {
	return n.name
}

func (n node) IsDir() bool {
	return n.children != nil
}

func (n node) String() string {
	return fmt.Sprintf("{name: %s num_children: %d}", n.name, len(n.children))
}

func (n node) Type() fs.FileMode {
	if n.children == nil {
		return 0
	}
	return fs.ModeDir
}

func (n node) Info() (fs.FileInfo, error) {
	return nil, fmt.Errorf("Info() not implemented")
}

func random_name(r *rand.Rand) string {
	length := 3 + r.Intn(23)
	bytes := make([]byte, length)
	for i := range length {
		bytes[i] = byte(r.Intn(26) + 'a')
	}
	return string(bytes)
}

func (n *node) generate_random_tree(depth, breadth int) {
	r := rand.New(rand.NewSource(111))
	n.children = make(map[string]*node)
	for range breadth {
		c := &node{name: random_name(r)}
		n.children[c.name] = c
		if depth > 0 && r.Intn(10) < 3 {
			c.generate_random_tree(depth-1, breadth)
		}
	}
}

func (n node) dir_entries() []fs.DirEntry {
	entries := make([]fs.DirEntry, 0, len(n.children))
	for _, v := range n.children {
		entries = append(entries, v)
	}
	return entries
}

func (n node) ReadFile(name string) ([]byte, error) {
	if name == string(os.PathSeparator) {
		return nil, fs.ErrNotExist
	}
	p := &n
	for x := range strings.SplitSeq(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
		c, found := p.children[x]
		if !found {
			return nil, fs.ErrNotExist
		}
		p = c
	}
	return []byte(p.data), nil
}

func (n node) ReadDir(name string) ([]fs.DirEntry, error) {
	if name == string(os.PathSeparator) {
		return n.dir_entries(), nil
	}
	p := &n
	for x := range strings.SplitSeq(strings.Trim(name, string(os.PathSeparator)), string(os.PathSeparator)) {
		c, found := p.children[x]
		if !found {
			return nil, fs.ErrNotExist
		}
		if !c.IsDir() {
			return nil, fs.ErrExist
		}
		p = c
	}
	return p.dir_entries(), nil
}

func TestChooseFilesIgnore(t *testing.T) {
	root := node{name: string(os.PathSeparator), children: map[string]*node{
		"a":          {name: "a"},
		"b":          {name: "b"},
		"c.png":      {name: "c.png"},
		".ignore":    {name: ".ignore", data: "a\nx/s/n"},
		".gitignore": {name: ".gitignore", data: "b"},
		"x": {name: "x", children: map[string]*node{
			"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
			"s": {name: "s", children: map[string]*node{
				"m": {name: "m"}, "n": {name: "n"},
			}},
		}},
		"y": {name: "y", children: map[string]*node{
			"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
			"s": {name: "s", children: map[string]*node{
				"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"}, "6": {name: "6"},
			}},
			".gitignore": {name: ".gitignore", data: "/s/5"},
			".git": {name: ".git", children: map[string]*node{
				"info": {name: "info", children: map[string]*node{
					"exclude": {name: "exclude", data: "s/4"},
				}},
			}},
		}},
	}}
	r := func(respect_ignores bool, expected string) {
		ch := make(chan bool)
		s := new_filesystem_scanner("/", ch, nil)
		s.dir_reader = root.ReadDir
		s.file_reader = root.ReadFile
		s.global_gitignore = ignorefiles.NewGitignore()
		s.global_ignore = ignorefiles.NewGitignore()
		s.respect_ignores = respect_ignores
		if err := s.global_gitignore.LoadLines("*.png", "s/3"); err != nil {
			t.Fatal(err)
		}
		if err := s.global_ignore.LoadLines("x/3"); err != nil {
			t.Fatal(err)
		}
		s.Start()
		for range ch {
		}
		if s.err != nil {
			t.Fatal(s.err)
		}
		ci := CollectionIndex{}
		actual := utils.Map(func(x ResultItem) string { return x.text }, s.Batch(&ci))
		if diff := cmp.Diff(strings.Split(expected, ` `), actual); diff != "" {
			t.Fatalf("Incorrect ignoring:\n%s", diff)
		}
	}
	r(true, `x y b c.png x/s x/1 x/2 y/s y/3 y/4 y/5 x/s/m y/s/6`)
	r(false, `x y a b c.png x/s x/1 x/2 x/3 y/s y/3 y/4 y/5 x/s/m x/s/n y/s/3 y/s/4 y/s/5 y/s/6`)
}

func TestChooseFilesScoring(t *testing.T) {
	root := node{name: string(os.PathSeparator), children: map[string]*node{
		"b":     {name: "b"},
		"a":     {name: "a"},
		"c.png": {name: "c.png"},
		"x": {name: "x", children: map[string]*node{
			"1": {name: "1"}, "2": {name: "2"}, "3": {name: "3"},
			"s": {name: "s", children: map[string]*node{
				"m": {name: "m"}, "n": {name: "n"},
			}},
		}},
		"y": {name: "y", children: map[string]*node{
			"3": {name: "3"}, "4": {name: "4"}, "5": {name: "5"},
		}},
	}}
	wg := sync.WaitGroup{}
	wg.Add(1)
	s := NewFileSystemScorer(string(os.PathSeparator), "", Filter{}, false, func(err error, is_complete bool) {
		if is_complete {
			wg.Done()
		}
	})
	s.dir_reader = root.ReadDir
	s.global_gitignore = ignorefiles.NewGitignore()
	s.Start()
	wg.Wait()
	results := func() (ans []string) {
		rr, _ := s.Results()
		for _, r := range rr.RenderedMatches(CollectionIndex{}, -1) {
			ans = append(ans, r.text)
		}
		return
	}
	ae := func(query string, expected ...string) {
		wg.Add(1)
		s.Change_query(query)
		wg.Wait()
		if diff := cmp.Diff(expected, results()); diff != "" {
			t.Fatalf("Query less scoring failed\n%s", diff)
		}
	}
	ae("a", "a")
	ae("3", "x/3", "y/3")
	ae("s", "x/s", "x/s/m", "x/s/n")
	ae("sn", "x/s/n")
	ae("", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")

	af := func(filter string, expected ...string) {
		f, _ := NewFilter(filter)
		wg.Add(1)
		s.Change_filter(*f)
		wg.Wait()
		if diff := cmp.Diff(expected, results()); diff != "" {
			t.Fatalf("filter %s failed\n%s", filter, diff)
		}
	}
	af("glob:a:A", "x", "y", "a", "x/s")
	af("glob:[ab]:A", "x", "y", "a", "b", "x/s")
	af("mime:image/png:A", "x", "y", "c.png", "x/s")
	af("mime:image/*:A", "x", "y", "c.png", "x/s")
	af("glob:*:All", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
}

func TestSortedResults(t *testing.T) {
	r := NewSortedResults()
	idx := CollectionIndex{}
	m := func(items ...int) []*ResultItem {
		ans := make([]*ResultItem, len(items))
		for i, x := range items {
			ans[i] = &ResultItem{text: strconv.Itoa(x), score: CombinedScore(x)}
		}
		return ans
	}
	v := func(slice, pos, num int) []int {
		if num == 0 {
			num = r.Len()
		}
		return utils.Map(func(r *ResultItem) int { return int(r.score) }, r.RenderedMatches(CollectionIndex{slice, pos}, num))
	}
	tv := func(slice, pos, num int, expected ...int) {
		if diff := cmp.Diff(expected, v(slice, pos, num)); diff != "" {
			t.Fatalf("view failed for %v num:%d\n%s", CollectionIndex{slice, pos}, num, diff)
		}
	}
	tci := func(increment int, expected int) {
		orig := idx
		idx = r.IncrementIndexWithWrapAround(idx, increment)
		actual := int(r.At(idx).score)
		if actual != expected {
			t.Fatalf("increment: %d on %v failed\nexpected: %d actual: %d idx: %v", increment, orig, expected, actual, idx)
		}
	}
	dt := func(a, b CollectionIndex, expected int) {
		actual := r.distance(a, b)
		if expected != actual {
			t.Fatalf("distance on %v and %v failed\nexpected: %d actual: %d ", a, b, expected, actual)
		}
		if r.distance(b, a) != actual {
			t.Fatalf("distance on %v and %v not commutative %d != %d", a, b, actual, r.distance(b, a))
		}
	}
	tc := func(num_before, expected_new_before int, ci CollectionIndex, expected ...[]int) {
		ac, new_num_before, _ := r.SplitIntoColumns(func(int) int { return 2 }, 2, num_before, ci)
		actual := make([][]int, len(ac))
		for i, x := range ac {
			actual[i] = utils.Map(func(r *ResultItem) int { return int(r.score) }, x)
		}
		if expected_new_before != new_num_before {
			t.Fatalf("new_num_before not as expected for num_before: %d ci: %v\n%d != %d", num_before, ci, expected_new_before, new_num_before)
		}
		if diff := cmp.Diff(expected, actual); diff != "" {
			t.Fatalf("wrong columns for num_before: %d ci: %v\n%s", num_before, ci, diff)
		}
	}
	r.AddSortedSlice(m(10, 20, 30))
	r.AddSortedSlice(m(40, 50, 60))
	r.AddSortedSlice(m(70, 80, 90))

	tc(0, 0, CollectionIndex{}, []int{10, 20}, []int{30, 40})
	tc(1, 1, CollectionIndex{Pos: 1}, []int{10, 20}, []int{30, 40})
	tc(1, 1, CollectionIndex{Pos: 2}, []int{20, 30}, []int{40, 50})
	tc(2, 2, CollectionIndex{Pos: 2}, []int{10, 20}, []int{30, 40})
	tc(20, 2, CollectionIndex{Pos: 2}, []int{10, 20}, []int{30, 40})
	for num_before := range 4 {
		tc(num_before, 3, CollectionIndex{2, 2}, []int{60, 70}, []int{80, 90})
	}
	tc(1, 1, CollectionIndex{1, 1}, []int{40, 50}, []int{60, 70})

	dt(CollectionIndex{Pos: 0}, CollectionIndex{Pos: 2}, 2)
	dt(CollectionIndex{Pos: 0}, CollectionIndex{Slice: 1}, 3)
	dt(CollectionIndex{Pos: 0}, CollectionIndex{Slice: 1, Pos: 1}, 4)

	tv(0, 0, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
	tv(0, 2, 3, 30, 40, 50)
	tv(0, 3, 3, 40, 50, 60)
	tv(1, 0, 4, 40, 50, 60, 70)

	tci(1, 20)
	tci(3, 50)
	tci(-1, 40)
	tci(-3, 10)
	tci(-2, 80)
	tci(3, 20)
	tci(9, 20)
	tci(-9, 20)

	r.AddSortedSlice(m(100, 110, 120))
	r.AddSortedSlice(m(41, 61, 71, 99))
	tv(0, 0, 0, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120)
	r.AddSortedSlice(m(1, 2, 3))
	tv(0, 0, 0, 1, 2, 3, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120)
	r.AddSortedSlice(m(1000, 2000))
	tv(0, 0, 0, 1, 2, 3, 10, 20, 30, 40, 41, 50, 60, 61, 70, 71, 80, 90, 99, 100, 110, 120, 1000, 2000)
}

func run_scoring(b *testing.B, depth, breadth int, query string) {

	root := node{name: string(os.PathSeparator)}
	root.generate_random_tree(depth, breadth)

	for b.Loop() {
		b.StopTimer()
		wg := sync.WaitGroup{}
		wg.Add(1)
		s := NewFileSystemScorer(string(os.PathSeparator), query, Filter{}, false, func(err error, is_complete bool) {
			if is_complete {
				wg.Done()
			}
		})
		s.dir_reader = root.ReadDir
		s.global_gitignore = ignorefiles.NewGitignore()
		b.StartTimer()
		s.scanner.Start()
		s.Start()
		wg.Wait()
	}
	fmt.Println("\nnumber of iterations: ", b.N)
	fmt.Println("time per iteration:", b.Elapsed()/time.Duration(b.N))
}

// To run this benchmark with profiling use:
// go test -bench=FileNameScoringWithoutQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe
func BenchmarkFileNameScoringWithoutQuery(b *testing.B) {
	run_scoring(b, 5, 20, "")
}

// To run this benchmark with profiling use:
// go test -bench=FileNameScoringWithQuery -benchmem -cpuprofile=/tmp/cpu.prof -memprofile=/tmp/mem.prof github.com/kovidgoyal/kitty/kittens/choose_files -o /tmp/cfexe
func BenchmarkFileNameScoringWithQuery(b *testing.B) {
	run_scoring(b, 5, 20, "abc")
}
