//go:build darwin || aix || dragonfly || freebsd || (js && wasm) || linux || netbsd || openbsd || solaris

package fastwalk

import (
	"io/fs"
	"math/rand"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"sync"
	"sync/atomic"
	"testing"
	"time"
	"unsafe"
)

func testUnixDirentParallel(t *testing.T, ent *unixDirent, want fs.FileInfo,
	fn func(*unixDirent) (fs.FileInfo, error)) {

	sameFile := func(fi1, fi2 fs.FileInfo) bool {
		return fi1.Name() == fi2.Name() &&
			fi1.Size() == fi2.Size() &&
			fi1.Mode() == fi2.Mode() &&
			fi1.ModTime() == fi2.ModTime() &&
			fi1.IsDir() == fi2.IsDir() &&
			os.SameFile(fi1, fi2)
	}

	numCPU := runtime.NumCPU()
	if numCPU < 4 {
		numCPU = 4
	}
	if numCPU > 16 {
		numCPU = 16
	}

	var wg sync.WaitGroup
	start := make(chan struct{})
	var mu sync.Mutex
	infos := make(map[*fileInfo]int)
	stats := make(map[*fileInfo]int)

	for i := 0; i < numCPU; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			for i := 0; i < 16; i++ {
				fi, err := fn(ent)
				if err != nil {
					t.Error(err)
					return
				}
				info := (*fileInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&ent.info))))
				stat := (*fileInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&ent.stat))))
				mu.Lock()
				infos[info]++
				stats[stat]++
				mu.Unlock()
				if !sameFile(fi, want) {
					t.Errorf("FileInfo not equal:\nwant: %s\ngot:  %s\n",
						FormatFileInfo(want), FormatFileInfo(fi))
					return
				}
			}
		}()
	}

	close(start)
	wg.Wait()

	t.Logf("Infos: %d Stats: %d\n", len(infos), len(stats))
}

func TestUnixDirent(t *testing.T) {
	tempdir := t.TempDir()

	fileName := filepath.Join(tempdir, "file.txt")
	if err := os.WriteFile(fileName, []byte("file.txt"), 0644); err != nil {
		t.Fatal(err)
	}
	t.Run("File", func(t *testing.T) {
		fileInfo, err := os.Lstat(fileName)
		if err != nil {
			t.Fatal(err)
		}
		t.Run("Stat", func(t *testing.T) {
			ent := newUnixDirent(tempdir, filepath.Base(fileName), fileInfo.Mode().Type(), 0)
			testUnixDirentParallel(t, ent, fileInfo, (*unixDirent).Stat)
		})
		t.Run("Info", func(t *testing.T) {
			ent := newUnixDirent(tempdir, filepath.Base(fileName), fileInfo.Mode().Type(), 0)
			testUnixDirentParallel(t, ent, fileInfo, (*unixDirent).Info)
		})
	})

	t.Run("Link", func(t *testing.T) {
		linkName := filepath.Join(tempdir, "link.link")
		if err := os.Symlink(filepath.Base(fileName), linkName); err != nil {
			t.Fatal(err)
		}
		fileInfo, err := os.Lstat(linkName)
		if err != nil {
			t.Fatal(err)
		}
		t.Run("Stat", func(t *testing.T) {
			want, err := os.Stat(linkName)
			if err != nil {
				t.Fatal(err)
			}
			ent := newUnixDirent(tempdir, filepath.Base(linkName), fileInfo.Mode().Type(), 0)
			testUnixDirentParallel(t, ent, want, (*unixDirent).Stat)
		})
		t.Run("Info", func(t *testing.T) {
			ent := newUnixDirent(tempdir, filepath.Base(linkName), fileInfo.Mode().Type(), 0)
			testUnixDirentParallel(t, ent, fileInfo, (*unixDirent).Info)
		})
	})
}

// NB: this must be kept in sync with the
// TestSortDirents in dirent_portable_test.go
func TestSortDirents(t *testing.T) {
	direntNames := func(dents []*unixDirent) []string {
		names := make([]string, len(dents))
		for i, d := range dents {
			names[i] = d.Name()
		}
		return names
	}

	t.Run("None", func(t *testing.T) {
		dents := []*unixDirent{
			{name: "b"},
			{name: "a"},
			{name: "d"},
			{name: "c"},
		}
		want := direntNames(dents)
		sortDirents(SortNone, dents)
		got := direntNames(dents)
		if !reflect.DeepEqual(got, want) {
			t.Errorf("got: %q want: %q", got, want)
		}
	})

	rr := rand.New(rand.NewSource(time.Now().UnixNano()))
	shuffleDirents := func(dents []*unixDirent) []*unixDirent {
		rr.Shuffle(len(dents), func(i, j int) {
			dents[i], dents[j] = dents[j], dents[i]
		})
		return dents
	}

	// dents needs to be in the expected order
	test := func(t *testing.T, dents []*unixDirent, mode SortMode) {
		want := direntNames(dents)
		// Run multiple times with different shuffles
		for i := 0; i < 10; i++ {
			t.Run("", func(t *testing.T) {
				sortDirents(mode, shuffleDirents(dents))
				got := direntNames(dents)
				if !reflect.DeepEqual(got, want) {
					t.Errorf("got: %q want: %q", got, want)
				}
			})
		}
	}

	t.Run("Lexical", func(t *testing.T) {
		dents := []*unixDirent{
			{name: "a"},
			{name: "b"},
			{name: "c"},
			{name: "d"},
		}
		test(t, dents, SortLexical)
	})

	t.Run("FilesFirst", func(t *testing.T) {
		dents := []*unixDirent{
			// Files lexically
			{name: "f1", typ: 0},
			{name: "f2", typ: 0},
			{name: "f3", typ: 0},
			// Non-dirs lexically
			{name: "a1", typ: fs.ModeSymlink},
			{name: "a2", typ: fs.ModeSymlink},
			{name: "a3", typ: fs.ModeSymlink},
			{name: "s1", typ: fs.ModeSocket},
			{name: "s2", typ: fs.ModeSocket},
			{name: "s3", typ: fs.ModeSocket},
			// Dirs lexically
			{name: "d1", typ: fs.ModeDir},
			{name: "d2", typ: fs.ModeDir},
			{name: "d3", typ: fs.ModeDir},
		}
		test(t, dents, SortFilesFirst)
	})

	t.Run("DirsFirst", func(t *testing.T) {
		dents := []*unixDirent{
			// Dirs lexically
			{name: "d1", typ: fs.ModeDir},
			{name: "d2", typ: fs.ModeDir},
			{name: "d3", typ: fs.ModeDir},
			// Files lexically
			{name: "f1", typ: 0},
			{name: "f2", typ: 0},
			{name: "f3", typ: 0},
			// Non-dirs lexically
			{name: "a1", typ: fs.ModeSymlink},
			{name: "a2", typ: fs.ModeSymlink},
			{name: "a3", typ: fs.ModeSymlink},
			{name: "s1", typ: fs.ModeSocket},
			{name: "s2", typ: fs.ModeSocket},
			{name: "s3", typ: fs.ModeSocket},
		}
		test(t, dents, SortDirsFirst)
	})
}

func BenchmarkUnixDirentLoadFileInfo(b *testing.B) {
	wd, err := os.Getwd()
	if err != nil {
		b.Fatal(err)
	}
	fi, err := os.Lstat(wd)
	if err != nil {
		b.Fatal(err)
	}
	parent, name := filepath.Split(wd)
	d := newUnixDirent(parent, name, fi.Mode().Type(), 0)

	for i := 0; i < b.N; i++ {
		loadFileInfo(&d.info)
		d.info = nil
	}
}

func BenchmarkUnixDirentInfo(b *testing.B) {
	wd, err := os.Getwd()
	if err != nil {
		b.Fatal(err)
	}
	fi, err := os.Lstat(wd)
	if err != nil {
		b.Fatal(err)
	}
	parent, name := filepath.Split(wd)
	d := newUnixDirent(parent, name, fi.Mode().Type(), 0)

	for i := 0; i < b.N; i++ {
		fi, err := d.Info()
		if err != nil {
			b.Fatal(err)
		}
		if fi == nil {
			b.Fatal("Nil FileInfo")
		}
	}
}

func BenchmarkUnixDirentStat(b *testing.B) {
	wd, err := os.Getwd()
	if err != nil {
		b.Fatal(err)
	}
	fi, err := os.Lstat(wd)
	if err != nil {
		b.Fatal(err)
	}
	parent, name := filepath.Split(wd)
	d := newUnixDirent(parent, name, fi.Mode().Type(), 0)

	for i := 0; i < b.N; i++ {
		fi, err := d.Stat()
		if err != nil {
			b.Fatal(err)
		}
		if fi == nil {
			b.Fatal("Nil FileInfo")
		}
	}
}
