package scanner

import (
	"context"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"testing/fstest"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	. "github.com/onsi/gomega/gstruct"
)

var _ = Describe("walk_dir_tree", func() {
	dir, _ := os.Getwd()
	baseDir := filepath.Join(dir, "tests", "fixtures")

	Describe("walkDirTree", func() {
		It("reads all info correctly", func() {
			var collected = dirMap{}
			results, errC := walkDirTree(context.Background(), baseDir)

			for {
				stats, more := <-results
				if !more {
					break
				}
				collected[stats.Path] = stats
			}

			Consistently(errC).ShouldNot(Receive())
			Expect(collected[baseDir]).To(MatchFields(IgnoreExtras, Fields{
				"Images":          BeEmpty(),
				"HasPlaylist":     BeFalse(),
				"AudioFilesCount": BeNumerically("==", 13),
			}))
			Expect(collected[filepath.Join(baseDir, "artist", "an-album")]).To(MatchFields(IgnoreExtras, Fields{
				"Images":          ConsistOf("cover.jpg", "front.png", "artist.png"),
				"HasPlaylist":     BeFalse(),
				"AudioFilesCount": BeNumerically("==", 1),
			}))
			Expect(collected[filepath.Join(baseDir, "playlists")].HasPlaylist).To(BeTrue())
			Expect(collected).To(HaveKey(filepath.Join(baseDir, "symlink2dir")))
			Expect(collected).To(HaveKey(filepath.Join(baseDir, "empty_folder")))
		})
	})

	Describe("isDirOrSymlinkToDir", func() {
		It("returns true for normal dirs", func() {
			dirEntry := getDirEntry("tests", "fixtures")
			Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeTrue())
		})
		It("returns true for symlinks to dirs", func() {
			dirEntry := getDirEntry(baseDir, "symlink2dir")
			Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeTrue())
		})
		It("returns false for files", func() {
			dirEntry := getDirEntry(baseDir, "test.mp3")
			Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeFalse())
		})
		It("returns false for symlinks to files", func() {
			dirEntry := getDirEntry(baseDir, "symlink")
			Expect(isDirOrSymlinkToDir(baseDir, dirEntry)).To(BeFalse())
		})
	})
	Describe("isDirIgnored", func() {
		It("returns false for normal dirs", func() {
			dirEntry := getDirEntry(baseDir, "empty_folder")
			Expect(isDirIgnored(baseDir, dirEntry)).To(BeFalse())
		})
		It("returns true when folder contains .ndignore file", func() {
			dirEntry := getDirEntry(baseDir, "ignored_folder")
			Expect(isDirIgnored(baseDir, dirEntry)).To(BeTrue())
		})
		It("returns true when folder name starts with a `.`", func() {
			dirEntry := getDirEntry(baseDir, ".hidden_folder")
			Expect(isDirIgnored(baseDir, dirEntry)).To(BeTrue())
		})
		It("returns false when folder name starts with ellipses", func() {
			dirEntry := getDirEntry(baseDir, "...unhidden_folder")
			Expect(isDirIgnored(baseDir, dirEntry)).To(BeFalse())
		})
		It("returns false when folder name is $Recycle.Bin", func() {
			dirEntry := getDirEntry(baseDir, "$Recycle.Bin")
			Expect(isDirIgnored(baseDir, dirEntry)).To(BeFalse())
		})
	})

	Describe("fullReadDir", func() {
		var fsys fakeFS
		var ctx context.Context
		BeforeEach(func() {
			ctx = context.Background()
			fsys = fakeFS{MapFS: fstest.MapFS{
				"root/a/f1": {},
				"root/b/f2": {},
				"root/c/f3": {},
			}}
		})
		It("reads all entries", func() {
			dir, _ := fsys.Open("root")
			entries := fullReadDir(ctx, dir.(fs.ReadDirFile))
			Expect(entries).To(HaveLen(3))
			Expect(entries[0].Name()).To(Equal("a"))
			Expect(entries[1].Name()).To(Equal("b"))
			Expect(entries[2].Name()).To(Equal("c"))
		})
		It("skips entries with permission error", func() {
			fsys.failOn = "b"
			dir, _ := fsys.Open("root")
			entries := fullReadDir(ctx, dir.(fs.ReadDirFile))
			Expect(entries).To(HaveLen(2))
			Expect(entries[0].Name()).To(Equal("a"))
			Expect(entries[1].Name()).To(Equal("c"))
		})
		It("aborts if it keeps getting 'readdirent: no such file or directory'", func() {
			fsys.err = fs.ErrNotExist
			dir, _ := fsys.Open("root")
			entries := fullReadDir(ctx, dir.(fs.ReadDirFile))
			Expect(entries).To(BeEmpty())
		})
	})
})

type fakeFS struct {
	fstest.MapFS
	failOn string
	err    error
}

func (f *fakeFS) Open(name string) (fs.File, error) {
	dir, err := f.MapFS.Open(name)
	return &fakeDirFile{File: dir, fail: f.failOn, err: f.err}, err
}

type fakeDirFile struct {
	fs.File
	entries []fs.DirEntry
	pos     int
	fail    string
	err     error
}

// Only works with n == -1
func (fd *fakeDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
	if fd.err != nil {
		return nil, fd.err
	}
	if fd.entries == nil {
		fd.entries, _ = fd.File.(fs.ReadDirFile).ReadDir(-1)
	}
	var dirs []fs.DirEntry
	for {
		if fd.pos >= len(fd.entries) {
			break
		}
		e := fd.entries[fd.pos]
		fd.pos++
		if e.Name() == fd.fail {
			return dirs, &fs.PathError{Op: "lstat", Path: e.Name(), Err: fs.ErrPermission}
		}
		dirs = append(dirs, e)
	}
	return dirs, nil
}

func getDirEntry(baseDir, name string) os.DirEntry {
	dirEntries, _ := os.ReadDir(baseDir)
	for _, entry := range dirEntries {
		if entry.Name() == name {
			return entry
		}
	}
	panic(fmt.Sprintf("Could not find %s in %s", name, baseDir))
}
