package platform_test

import (
	"io/fs"
	"os"
	"path"
	"runtime"
	"sort"
	"syscall"
	"testing"

	"github.com/tetratelabs/wazero/internal/fstest"
	"github.com/tetratelabs/wazero/internal/platform"
	"github.com/tetratelabs/wazero/internal/testing/require"
)

func TestReaddirnames(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()
	require.NoError(t, fstest.WriteTestFiles(tmpDir))
	dirFS := os.DirFS(tmpDir)

	tests := []struct {
		name string
		fs   fs.FS
	}{
		{name: "os.DirFS", fs: dirFS},         // To test readdirnamesFile
		{name: "fstest.MapFS", fs: fstest.FS}, // To test adaptation of ReadDirFile
	}

	for _, tc := range tests {
		tc := tc

		t.Run(tc.name, func(t *testing.T) {
			dotF, err := tc.fs.Open(".")
			require.NoError(t, err)
			defer dotF.Close()

			t.Run("dir", func(t *testing.T) {
				names, errno := platform.Readdirnames(dotF, -1)
				require.Zero(t, errno)

				sort.Strings(names)
				require.Equal(t, []string{"animals.txt", "dir", "empty.txt", "emptydir", "sub"}, names)

				// read again even though it is exhausted
				_, errno = platform.Readdirnames(dotF, 100)
				require.Zero(t, errno)
			})

			// Don't err if something else closed the directory while reading.
			t.Run("closed dir", func(t *testing.T) {
				require.NoError(t, dotF.Close())
				_, errno := platform.Readdir(dotF, -1)
				require.Zero(t, errno)
			})

			dirF, err := tc.fs.Open("dir")
			require.NoError(t, err)
			defer dirF.Close()

			t.Run("partial", func(t *testing.T) {
				names1, errno := platform.Readdirnames(dirF, 1)
				require.Zero(t, errno)
				require.Equal(t, 1, len(names1))

				names2, errno := platform.Readdirnames(dirF, 1)
				require.Zero(t, errno)
				require.Equal(t, 1, len(names2))

				// read exactly the last entry
				names3, errno := platform.Readdirnames(dirF, 1)
				require.Zero(t, errno)
				require.Equal(t, 1, len(names3))

				names := []string{names1[0], names2[0], names3[0]}
				sort.Strings(names)

				require.Equal(t, []string{"-", "a-", "ab-"}, names)

				// no error reading an exhausted directory
				_, errno = platform.Readdirnames(dirF, 1)
				require.Zero(t, errno)
			})

			fileF, err := tc.fs.Open("empty.txt")
			require.NoError(t, err)
			defer fileF.Close()

			t.Run("file", func(t *testing.T) {
				_, errno := platform.Readdirnames(fileF, -1)
				require.EqualErrno(t, syscall.ENOTDIR, errno)
			})

			subdirF, err := tc.fs.Open("sub")
			require.NoError(t, err)
			defer subdirF.Close()

			t.Run("subdir", func(t *testing.T) {
				names, errno := platform.Readdirnames(subdirF, -1)
				require.Zero(t, errno)

				require.Equal(t, []string{"test.txt"}, names)
			})
		})
	}
}

func TestReaddir(t *testing.T) {
	t.Parallel()

	tmpDir := t.TempDir()
	require.NoError(t, fstest.WriteTestFiles(tmpDir))
	dirFS := os.DirFS(tmpDir)

	tests := []struct {
		name      string
		fs        fs.FS
		expectIno bool
	}{
		{name: "os.DirFS", fs: dirFS, expectIno: runtime.GOOS != "windows"}, // To test readdirFile
		{name: "fstest.MapFS", fs: fstest.FS, expectIno: false},             // To test adaptation of ReadDirFile
	}

	for _, tc := range tests {
		tc := tc

		t.Run(tc.name, func(t *testing.T) {
			dotF, err := tc.fs.Open(".")
			require.NoError(t, err)
			defer dotF.Close()

			t.Run("dir", func(t *testing.T) {
				dirents, errno := platform.Readdir(dotF, -1)
				require.Zero(t, errno) // no io.EOF when -1 is used
				sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })

				requireIno(t, dirents, tc.expectIno)

				require.Equal(t, []*platform.Dirent{
					{Name: "animals.txt", Type: 0},
					{Name: "dir", Type: fs.ModeDir},
					{Name: "empty.txt", Type: 0},
					{Name: "emptydir", Type: fs.ModeDir},
					{Name: "sub", Type: fs.ModeDir},
				}, dirents)

				// read again even though it is exhausted
				dirents, errno = platform.Readdir(dotF, 100)
				require.Zero(t, errno)
				require.Zero(t, len(dirents))
			})

			// Don't err if something else closed the directory while reading.
			t.Run("closed dir", func(t *testing.T) {
				require.NoError(t, dotF.Close())
				_, errno := platform.Readdir(dotF, -1)
				require.Zero(t, errno)
			})

			fileF, err := tc.fs.Open("empty.txt")
			require.NoError(t, err)
			defer fileF.Close()

			t.Run("file", func(t *testing.T) {
				_, errno := platform.Readdir(fileF, -1)
				require.EqualErrno(t, syscall.ENOTDIR, errno)
			})

			dirF, err := tc.fs.Open("dir")
			require.NoError(t, err)
			defer dirF.Close()

			t.Run("partial", func(t *testing.T) {
				dirents1, errno := platform.Readdir(dirF, 1)
				require.Zero(t, errno)
				require.Equal(t, 1, len(dirents1))

				dirents2, errno := platform.Readdir(dirF, 1)
				require.Zero(t, errno)
				require.Equal(t, 1, len(dirents2))

				// read exactly the last entry
				dirents3, errno := platform.Readdir(dirF, 1)
				require.Zero(t, errno)
				require.Equal(t, 1, len(dirents3))

				dirents := []*platform.Dirent{dirents1[0], dirents2[0], dirents3[0]}
				sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })

				requireIno(t, dirents, tc.expectIno)

				require.Equal(t, []*platform.Dirent{
					{Name: "-", Type: 0},
					{Name: "a-", Type: fs.ModeDir},
					{Name: "ab-", Type: 0},
				}, dirents)

				// no error reading an exhausted directory
				_, errno = platform.Readdir(dirF, 1)
				require.Zero(t, errno)
			})

			subdirF, err := tc.fs.Open("sub")
			require.NoError(t, err)
			defer subdirF.Close()

			t.Run("subdir", func(t *testing.T) {
				dirents, errno := platform.Readdir(subdirF, -1)
				require.Zero(t, errno)
				sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })

				require.Equal(t, 1, len(dirents))
				require.Equal(t, "test.txt", dirents[0].Name)
				require.Zero(t, dirents[0].Type)
			})
		})
	}

	// Don't err if something else removed the directory while reading.
	t.Run("removed while open", func(t *testing.T) {
		dirF, err := dirFS.Open("dir")
		require.NoError(t, err)
		defer dirF.Close()

		dirents, errno := platform.Readdir(dirF, 1)
		require.Zero(t, errno)
		require.Equal(t, 1, len(dirents))

		// Speculatively try to remove even if it won't likely work
		// on windows.
		err = os.RemoveAll(path.Join(tmpDir, "dir"))
		if err != nil && runtime.GOOS == "windows" {
			t.Skip()
		} else {
			require.NoError(t, err)
		}

		_, errno = platform.Readdir(dirF, 1)
		require.Zero(t, errno)
		// don't validate the contents as due to caching it might be present.
	})
}

func requireIno(t *testing.T, dirents []*platform.Dirent, expectIno bool) {
	for _, e := range dirents {
		if expectIno {
			require.NotEqual(t, uint64(0), e.Ino, "%+v", e)
			e.Ino = 0
		} else {
			require.Zero(t, e.Ino, "%+v", e)
		}
	}
}
