package winfsp

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"
)

func TestTruncate(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "resize")
	original := bytes.Repeat([]byte("abcd"), 4096) // 16 KiB

	if err := os.WriteFile(path, original, 0644); err != nil {
		t.Fatalf("write: %v", err)
	}

	// Shrink.
	if err := os.Truncate(path, 100); err != nil {
		t.Fatalf("truncate down: %v", err)
	}
	got, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read after shrink: %v", err)
	}
	if len(got) != 100 {
		t.Fatalf("size after shrink = %d, want 100", len(got))
	}
	if !bytes.Equal(got, original[:100]) {
		t.Fatal("the surviving prefix does not match the original")
	}

	// Grow: the new space has to read back as zeros, not stale bytes.
	if err := os.Truncate(path, 8192); err != nil {
		t.Fatalf("truncate up: %v", err)
	}
	got, err = os.ReadFile(path)
	if err != nil {
		t.Fatalf("read after grow: %v", err)
	}
	if len(got) != 8192 {
		t.Fatalf("size after grow = %d, want 8192", len(got))
	}
	if !bytes.Equal(got[100:], make([]byte, 8192-100)) {
		t.Fatal("extension is not zero-filled")
	}

	// Truncate to empty.
	if err := os.Truncate(path, 0); err != nil {
		t.Fatalf("truncate to zero: %v", err)
	}
	if info, err := os.Stat(path); err != nil || info.Size() != 0 {
		t.Fatalf("stat after zero truncate: size=%v err=%v", info, err)
	}
}

func TestAppend(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "log")

	for i := 0; i < 5; i++ {
		f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
		if err != nil {
			t.Fatalf("open for append: %v", err)
		}
		if _, err := fmt.Fprintf(f, "line %d\n", i); err != nil {
			t.Fatalf("append: %v", err)
		}
		if err := f.Close(); err != nil {
			t.Fatalf("close: %v", err)
		}
	}

	got, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	want := "line 0\nline 1\nline 2\nline 3\nline 4\n"
	if string(got) != want {
		t.Fatalf("appended content = %q, want %q", got, want)
	}
}

func TestReadPastEndOfFile(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "short")
	if err := os.WriteFile(path, []byte("12345"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}

	f, err := os.Open(path)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer f.Close()

	buf := make([]byte, 16)
	n, err := f.ReadAt(buf, 100)
	if n != 0 {
		t.Fatalf("read %d bytes past EOF, want 0", n)
	}
	if err == nil {
		t.Fatal("reading past EOF returned no error, want io.EOF")
	}
}

func TestErrorPaths(t *testing.T) {
	dir := testRoot(t)

	t.Run("open missing file", func(t *testing.T) {
		_, err := os.Open(filepath.Join(dir, "does-not-exist"))
		if !os.IsNotExist(err) {
			t.Fatalf("got %v, want a not-exist error", err)
		}
	})

	t.Run("exclusive create over existing", func(t *testing.T) {
		path := filepath.Join(dir, "taken")
		if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
			t.Fatalf("write: %v", err)
		}
		_, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
		if !os.IsExist(err) {
			t.Fatalf("got %v, want an already-exists error", err)
		}
	})

	t.Run("remove missing file", func(t *testing.T) {
		if err := os.Remove(filepath.Join(dir, "never-there")); !os.IsNotExist(err) {
			t.Fatalf("got %v, want a not-exist error", err)
		}
	})

	t.Run("mkdir under a file", func(t *testing.T) {
		path := filepath.Join(dir, "regular")
		if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
			t.Fatalf("write: %v", err)
		}
		if err := os.Mkdir(filepath.Join(path, "child"), 0755); err == nil {
			t.Fatal("created a directory underneath a regular file")
		}
	})

	t.Run("readdir of a missing directory", func(t *testing.T) {
		if _, err := os.ReadDir(filepath.Join(dir, "no-such-dir")); err == nil {
			t.Fatal("listed a directory that does not exist")
		}
	})
}

func TestRenameOverExisting(t *testing.T) {
	dir := testRoot(t)
	src := filepath.Join(dir, "src")
	dst := filepath.Join(dir, "dst")

	if err := os.WriteFile(src, []byte("new"), 0644); err != nil {
		t.Fatalf("write src: %v", err)
	}
	if err := os.WriteFile(dst, []byte("old"), 0644); err != nil {
		t.Fatalf("write dst: %v", err)
	}
	if err := os.Rename(src, dst); err != nil {
		t.Fatalf("rename over existing: %v", err)
	}
	got, err := os.ReadFile(dst)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if string(got) != "new" {
		t.Fatalf("target holds %q, want the source content", got)
	}
	if fi, err := os.Stat(src); !os.IsNotExist(err) {
		// Both a stale positive and a transient non-ENOENT error land here, and
		// they indict different layers; a second look says whether it persists.
		time.Sleep(200 * time.Millisecond)
		fi2, err2 := os.Stat(src)
		// A listing reads no per-path cache and the mount's own forgets
		// within a second, so a name that survives both is back on the filer.
		listed := dirNames(t, dir)
		time.Sleep(2 * time.Second)
		_, errLater := os.Stat(src)
		t.Fatalf("stat of the renamed-away source did not return not-exist: stat=%v err=%v; 200ms later stat=%v err=%v; past the path cache err=%v; %s lists %v",
			describeFileInfo(fi), err, describeFileInfo(fi2), err2, errLater, dir, listed)
	}
}

// dirNames lists a directory for a failure message, reporting the error in
// place of the names rather than failing a test that is already failing.
func dirNames(t *testing.T, dir string) []string {
	t.Helper()
	entries, err := os.ReadDir(dir)
	if err != nil {
		return []string{"readdir: " + err.Error()}
	}
	names := make([]string, 0, len(entries))
	for _, entry := range entries {
		names = append(names, entry.Name())
	}
	return names
}

func TestRenameAcrossDirectories(t *testing.T) {
	dir := testRoot(t)
	from := filepath.Join(dir, "from")
	to := filepath.Join(dir, "to")
	for _, d := range []string{from, to} {
		if err := os.Mkdir(d, 0755); err != nil {
			t.Fatalf("mkdir %s: %v", d, err)
		}
	}

	src := filepath.Join(from, "file")
	dst := filepath.Join(to, "file")
	if err := os.WriteFile(src, []byte("moved"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	if err := os.Rename(src, dst); err != nil {
		t.Fatalf("rename across directories: %v", err)
	}
	got, err := os.ReadFile(dst)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if string(got) != "moved" {
		t.Fatalf("content after move = %q", got)
	}

	// A directory should move too, children and all.
	nested := filepath.Join(from, "sub")
	if err := os.Mkdir(nested, 0755); err != nil {
		t.Fatalf("mkdir sub: %v", err)
	}
	if err := os.WriteFile(filepath.Join(nested, "child"), []byte("c"), 0644); err != nil {
		t.Fatalf("write child: %v", err)
	}
	if err := os.Rename(nested, filepath.Join(to, "sub")); err != nil {
		t.Fatalf("rename directory: %v", err)
	}
	if _, err := os.Stat(filepath.Join(to, "sub", "child")); err != nil {
		t.Fatalf("child did not come along: %v", err)
	}
}

// TestAwkwardNames covers what has to survive the UTF-16 boundary and the
// shapes Windows software tends to produce.
func TestAwkwardNames(t *testing.T) {
	dir := testRoot(t)
	names := []string{
		"café.txt",
		"日本語のファイル.dat",
		"emoji-🐟.bin",
		"with space.txt",
		"with.many.dots.txt",
		"UPPER and lower.TXT",
		"dash-and_underscore.txt",
		"'quoted'.txt",
		"(parens).txt",
		"#hash&amp.txt",
		strings.Repeat("x", 200) + ".txt",
	}
	for _, name := range names {
		t.Run(name, func(t *testing.T) {
			path := filepath.Join(dir, name)
			want := []byte(name)
			if err := os.WriteFile(path, want, 0644); err != nil {
				t.Fatalf("write: %v", err)
			}
			got, err := os.ReadFile(path)
			if err != nil {
				t.Fatalf("read: %v", err)
			}
			if !bytes.Equal(got, want) {
				t.Fatalf("content = %q, want %q", got, want)
			}
		})
	}

	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Fatalf("readdir: %v", err)
	}
	found := make(map[string]bool, len(entries))
	for _, e := range entries {
		found[e.Name()] = true
	}
	for _, name := range names {
		if !found[name] {
			t.Errorf("%q did not come back from readdir", name)
		}
	}
}

func TestDeepDirectoryNesting(t *testing.T) {
	dir := testRoot(t)
	deep := dir
	for i := 0; i < 24; i++ {
		deep = filepath.Join(deep, fmt.Sprintf("level%02d", i))
	}
	if err := os.MkdirAll(deep, 0755); err != nil {
		t.Fatalf("mkdirall depth 24: %v", err)
	}
	leaf := filepath.Join(deep, "leaf.txt")
	if err := os.WriteFile(leaf, []byte("bottom"), 0644); err != nil {
		t.Fatalf("write leaf: %v", err)
	}
	got, err := os.ReadFile(leaf)
	if err != nil {
		t.Fatalf("read leaf: %v", err)
	}
	if string(got) != "bottom" {
		t.Fatalf("leaf content = %q", got)
	}
}

// TestConcurrentSameFile is the interleaving that matters: several handles on
// one file at once, rather than one writer each on separate files.
func TestConcurrentSameFile(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "shared")

	const writers = 4
	const blockSize = 4096
	if err := os.WriteFile(path, make([]byte, writers*blockSize), 0644); err != nil {
		t.Fatalf("preallocate: %v", err)
	}

	var wg sync.WaitGroup
	errs := make(chan error, writers)
	for w := 0; w < writers; w++ {
		wg.Add(1)
		go func(w int) {
			defer wg.Done()
			f, err := os.OpenFile(path, os.O_WRONLY, 0644)
			if err != nil {
				errs <- fmt.Errorf("writer %d open: %w", w, err)
				return
			}
			defer f.Close()
			block := bytes.Repeat([]byte{byte('A' + w)}, blockSize)
			if _, err := f.WriteAt(block, int64(w*blockSize)); err != nil {
				errs <- fmt.Errorf("writer %d write: %w", w, err)
			}
		}(w)
	}
	wg.Wait()
	close(errs)
	for err := range errs {
		t.Fatalf("%v", err)
	}

	got, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if len(got) != writers*blockSize {
		t.Fatalf("size = %d, want %d", len(got), writers*blockSize)
	}
	for w := 0; w < writers; w++ {
		want := bytes.Repeat([]byte{byte('A' + w)}, blockSize)
		if !bytes.Equal(got[w*blockSize:(w+1)*blockSize], want) {
			t.Fatalf("block %d was not written whole", w)
		}
	}
}

func TestConcurrentReaders(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "read-me")
	want := contentFor("concurrent-readers", 1<<20)
	if err := os.WriteFile(path, want, 0644); err != nil {
		t.Fatalf("write: %v", err)
	}

	// WriteFile's own existence probe ran while the file did not exist, and
	// WinFsp may serve that answer from its metadata cache for up to the
	// mount's FileInfoTimeout. Establish visibility once before racing the
	// readers, so the race exercises concurrent reading rather than the
	// cache window.
	deadline := time.Now().Add(3 * time.Second)
	for {
		if _, err := os.Stat(path); err == nil {
			break
		} else if time.Now().After(deadline) {
			t.Fatalf("file never became visible: %v", err)
		}
		time.Sleep(50 * time.Millisecond)
	}

	var wg sync.WaitGroup
	errs := make(chan error, 8)
	for r := 0; r < 8; r++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			got, err := os.ReadFile(path)
			if err != nil {
				errs <- err
				return
			}
			if !bytes.Equal(got, want) {
				errs <- fmt.Errorf("reader saw %d bytes, want %d", len(got), len(want))
			}
		}()
	}
	wg.Wait()
	close(errs)
	for err := range errs {
		t.Fatalf("%v", err)
	}
}

// Hard links are not something WinFsp offers, so the mount has to refuse them
// rather than appear to succeed.
func TestHardLinkUnsupported(t *testing.T) {
	dir := testRoot(t)
	target := filepath.Join(dir, "original")
	if err := os.WriteFile(target, []byte("x"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	if err := os.Link(target, filepath.Join(dir, "hardlink")); err == nil {
		t.Fatal("hard link creation reported success")
	}
}

// Symlinks are refused rather than half-supported: WinFsp needs a reparse
// point to follow one, and an entry it cannot follow reads back empty.
func TestSymlinkUnsupported(t *testing.T) {
	dir := testRoot(t)
	target := filepath.Join(dir, "target.txt")
	if err := os.WriteFile(target, []byte("pointed at"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	if err := os.Symlink(target, filepath.Join(dir, "link.txt")); err == nil {
		t.Fatal("symlink creation reported success")
	}
}

func TestModTimeAdvances(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "touched")
	if err := os.WriteFile(path, []byte("one"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	first, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}

	time.Sleep(1100 * time.Millisecond)
	if err := os.WriteFile(path, []byte("two"), 0644); err != nil {
		t.Fatalf("rewrite: %v", err)
	}
	second, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat again: %v", err)
	}
	if !second.ModTime().After(first.ModTime()) {
		t.Fatalf("mtime did not advance: %v then %v", first.ModTime(), second.ModTime())
	}
}

func TestChtimes(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "dated")
	if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	want := time.Date(2020, 3, 4, 5, 6, 7, 0, time.UTC)
	if err := os.Chtimes(path, want, want); err != nil {
		t.Fatalf("chtimes: %v", err)
	}
	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if diff := info.ModTime().UTC().Sub(want); diff > 2*time.Second || diff < -2*time.Second {
		t.Fatalf("mtime = %v, want about %v", info.ModTime().UTC(), want)
	}
}

func TestStatfsIsSelfConsistent(t *testing.T) {
	if *mountPoint == "" {
		t.Skip("no -mountpoint given; this test needs a live WinFsp mount")
	}
	var free, total, totalFree uint64
	if err := getDiskFreeSpace(*mountPoint, &free, &total, &totalFree); err != nil {
		t.Fatalf("GetDiskFreeSpaceEx: %v", err)
	}
	if total == 0 {
		t.Fatal("drive reports zero total bytes")
	}
	if free > total {
		t.Fatalf("free (%d) exceeds total (%d)", free, total)
	}
	if totalFree > total {
		t.Fatalf("total free (%d) exceeds total (%d)", totalFree, total)
	}
}

// TestDeleteOnClose covers the pattern Windows software uses for temporaries:
// the file goes away when the last handle closes, with no explicit delete. A
// filesystem that ignores the flag leaves the name behind, and the next
// program to create it gets a collision.
func TestDeleteOnClose(t *testing.T) {
	dir := testRoot(t)
	path := filepath.Join(dir, "ephemeral.tmp")

	h, err := createDeleteOnClose(path)
	if err != nil {
		if errors.Is(err, errWindowsOnly) {
			t.Skip("delete-on-close needs the Win32 create call")
		}
		t.Fatalf("open with delete-on-close: %v", err)
	}
	if _, err := os.Stat(path); err != nil {
		closeHandle(h)
		t.Fatalf("file is not visible while its handle is open: %v", err)
	}
	if err := closeHandle(h); err != nil {
		t.Fatalf("close: %v", err)
	}
	if _, err := os.Stat(path); err == nil {
		t.Fatal("file outlived its last handle")
	} else if !os.IsNotExist(err) {
		t.Fatalf("stat after close: %v", err)
	}
	// The name has to be free again, which is what the conformance suite
	// tripped over when an aborted test left its file behind.
	h, err = createDeleteOnClose(path)
	if err != nil {
		t.Fatalf("recreating the same name failed: %v", err)
	}
	if err := closeHandle(h); err != nil {
		t.Fatalf("close after recreate: %v", err)
	}
}

func describeFileInfo(fi os.FileInfo) string {
	if fi == nil {
		return "<nil>"
	}
	return fmt.Sprintf("{size=%d mode=%v mtime=%s}", fi.Size(), fi.Mode(), fi.ModTime().Format(time.RFC3339Nano))
}
