package sys_test

import (
	"io/fs"
	"os"
	"path"
	"runtime"
	"testing"
	"testing/fstest"

	"github.com/tetratelabs/wazero/internal/testing/require"
	"github.com/tetratelabs/wazero/sys"
)

func Test_NewStat_t(t *testing.T) {
	tmpDir := t.TempDir()
	fileData := []byte{1, 2, 3, 4}

	dir := path.Join(tmpDir, "dir")
	require.NoError(t, os.Mkdir(dir, 0o700))
	osDirInfo, err := os.Stat(dir)
	require.NoError(t, err)

	file := path.Join(dir, "file")
	require.NoError(t, os.WriteFile(file, []byte{1, 2, 3, 4}, 0o400))
	osFileInfo, err := os.Stat(file)
	require.NoError(t, err)

	link := path.Join(dir, "file-link")
	require.NoError(t, os.Symlink(file, link))
	osSymlinkInfo, err := os.Lstat(link)
	require.NoError(t, err)

	osFileSt := sys.NewStat_t(osFileInfo)
	testFS := fstest.MapFS{
		"dir": {
			Mode:    osDirInfo.Mode(),
			ModTime: osDirInfo.ModTime(),
		},
		"dir/file": {
			Data:    fileData,
			Mode:    osFileInfo.Mode(),
			ModTime: osFileInfo.ModTime(),
		},
		"dir/file-sys": {
			// intentionally skip other fields to prove sys is read.
			Sys: &osFileSt,
		},
	}

	fsDirInfo, err := testFS.Stat("dir")
	require.NoError(t, err)
	fsFileInfo, err := testFS.Stat("dir/file")
	require.NoError(t, err)
	fsFileInfoWithSys, err := testFS.Stat("dir/file-sys")
	require.NoError(t, err)

	tests := []struct {
		name            string
		info            fs.FileInfo
		expectDevIno    bool
		expectedMode    fs.FileMode
		expectedSize    int64
		expectAtimCtime bool
	}{
		{
			name:            "os dir",
			info:            osDirInfo,
			expectDevIno:    true,
			expectedMode:    fs.ModeDir | 0o0700,
			expectedSize:    osDirInfo.Size(), // OS dependent
			expectAtimCtime: true,
		},
		{
			name:            "fs dir",
			info:            fsDirInfo,
			expectDevIno:    false,
			expectedMode:    fs.ModeDir | 0o0700,
			expectedSize:    0,
			expectAtimCtime: false,
		},
		{
			name:            "os file",
			info:            osFileInfo,
			expectDevIno:    true,
			expectedMode:    0o0400,
			expectedSize:    int64(len(fileData)),
			expectAtimCtime: true,
		},
		{
			name:            "fs file",
			info:            fsFileInfo,
			expectDevIno:    false,
			expectedMode:    0o0400,
			expectedSize:    int64(len(fileData)),
			expectAtimCtime: false,
		},
		{
			name:            "fs file with Stat_t in Sys",
			info:            fsFileInfoWithSys,
			expectDevIno:    true,
			expectedMode:    0o0400,
			expectedSize:    int64(len(fileData)),
			expectAtimCtime: true,
		},
		{
			name:            "os symlink",
			info:            osSymlinkInfo,
			expectDevIno:    true,
			expectedMode:    fs.ModeSymlink,
			expectedSize:    osSymlinkInfo.Size(), // OS dependent
			expectAtimCtime: true,
		},
	}

	for _, tt := range tests {
		tc := tt
		t.Run(tc.name, func(t *testing.T) {
			st := sys.NewStat_t(tc.info)
			if tc.expectDevIno && runtime.GOOS != "windows" {
				require.NotEqual(t, uint64(0), st.Dev)
				require.NotEqual(t, uint64(0), st.Ino)
			} else {
				require.Zero(t, st.Dev)
				require.Zero(t, st.Ino)
			}

			// link mode may differ on windows, so mask
			require.Equal(t, tc.expectedMode, st.Mode&tc.expectedMode)

			if sys.SysParseable && runtime.GOOS != "windows" {
				switch st.Nlink {
				case 2, 4: // dirents may include dot entries.
					require.Equal(t, fs.ModeDir, st.Mode.Type())
				default:
					require.Equal(t, uint64(1), st.Nlink)
				}
			} else { // Nlink is possibly wrong, but not zero.
				require.Equal(t, uint64(1), st.Nlink)
			}

			require.Equal(t, tc.expectedSize, st.Size)

			if tc.expectAtimCtime && sys.SysParseable {
				// We don't validate times strictly because it is os-dependent
				// what updates times. There are edge cases for symlinks, too.
				require.NotEqual(t, sys.EpochNanos(0), st.Ctim)
				require.NotEqual(t, sys.EpochNanos(0), st.Mtim)
				require.NotEqual(t, sys.EpochNanos(0), st.Mtim)
			} else { // mtim is used for atim and ctime
				require.Equal(t, st.Mtim, st.Ctim)
				require.NotEqual(t, sys.EpochNanos(0), st.Mtim)
				require.Equal(t, st.Mtim, st.Atim)
			}
		})
	}
}
