package winfsp

import (
	"bytes"
	"flag"
	"fmt"
	"hash/fnv"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"testing"
	"time"
)

var (
	// phase splits the test across a remount: the write phase is run, the
	// mount is torn down and brought back, then the verify phase runs. Reading
	// back through the same live mount proves nothing about durability, since
	// the answer can come from the mount's own caches.
	phase = flag.String("phase", "", "write or verify; empty skips the persistence test")

	// filerAddr enables a second check, that the bytes reached the filer and
	// are servable without the mount in the path at all.
	filerAddr = flag.String("filer", "", "filer host:port to cross-check through, e.g. localhost:8888")

	// persistSubdir is where the fixtures live, under both the mount and the
	// filer's mount root.
	persistSubdir = flag.String("persistdir", "winfsp-persist", "directory under the mount to persist into")
)

type fixture struct {
	relPath string
	size    int
}

// Sizes straddle the boundaries where the write path changes behavior: inline,
// a single chunk, and several chunks.
var fixtures = []fixture{
	{"small.txt", 11},
	{"medium.bin", 300 << 10},
	{"multichunk.bin", 9 << 20},
	{"nested/deep/leaf.bin", 65536},
	{"unicode-café-日本.txt", 64},
}

// contentFor derives bytes from the name, so the write and verify phases agree
// without carrying a manifest between them.
func contentFor(relPath string, size int) []byte {
	h := fnv.New64a()
	h.Write([]byte(relPath))
	state := h.Sum64() | 1
	out := make([]byte, size)
	for i := range out {
		state ^= state << 13
		state ^= state >> 7
		state ^= state << 17
		out[i] = byte(state)
	}
	return out
}

func TestPersistence(t *testing.T) {
	if *mountPoint == "" {
		t.Skip("no -mountpoint given; this test needs a live WinFsp mount")
	}
	switch *phase {
	case "write":
		persistenceWrite(t)
	case "verify":
		persistenceVerify(t)
	default:
		t.Skip("no -phase given; run with -phase=write before a remount and -phase=verify after")
	}
}

func persistenceWrite(t *testing.T) {
	root := filepath.Join(*mountPoint, *persistSubdir)
	if err := os.RemoveAll(root); err != nil && !os.IsNotExist(err) {
		t.Fatalf("clean %s: %v", root, err)
	}
	if err := os.MkdirAll(root, 0755); err != nil {
		t.Fatalf("mkdir %s: %v", root, err)
	}

	for _, f := range fixtures {
		target := filepath.Join(root, filepath.FromSlash(f.relPath))
		if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
			t.Fatalf("mkdir for %s: %v", f.relPath, err)
		}
		// Sync before closing: the mount is killed rather than unmounted, so
		// anything still queued for flush is legitimately lost. Testing
		// durability means testing what survives after an explicit sync.
		if err := writeAndSync(target, contentFor(f.relPath, f.size)); err != nil {
			t.Fatalf("write %s: %v", f.relPath, err)
		}
		t.Logf("wrote %s (%d bytes)", f.relPath, f.size)
	}

	// Closing the files should have pushed them to the filer. Check that
	// directly, so a failure here separates "never left the mount" from
	// "did not survive the remount".
	if *filerAddr == "" {
		return
	}
	for _, f := range fixtures {
		want := contentFor(f.relPath, f.size)
		got, err := fetchFromFiler(*filerAddr, path.Join(*persistSubdir, f.relPath))
		if err != nil {
			t.Errorf("fetch %s from filer: %v", f.relPath, err)
			continue
		}
		if !bytes.Equal(got, want) {
			t.Errorf("filer served %d bytes for %s, want %d", len(got), f.relPath, len(want))
		}
	}
}

func persistenceVerify(t *testing.T) {
	root := filepath.Join(*mountPoint, *persistSubdir)
	if _, err := os.Stat(root); err != nil {
		t.Fatalf("stat %s after remount: %v", root, err)
	}

	for _, f := range fixtures {
		target := filepath.Join(root, filepath.FromSlash(f.relPath))
		got, err := os.ReadFile(target)
		if err != nil {
			t.Errorf("read %s after remount: %v", f.relPath, err)
			continue
		}
		want := contentFor(f.relPath, f.size)
		if len(got) != len(want) {
			t.Errorf("%s is %d bytes after remount, want %d", f.relPath, len(got), len(want))
			continue
		}
		if !bytes.Equal(got, want) {
			t.Errorf("%s survived the remount with different content", f.relPath)
		}
	}

	// The directory structure has to come back too, not just the files.
	entries, err := os.ReadDir(root)
	if err != nil {
		t.Fatalf("readdir %s after remount: %v", root, err)
	}
	if len(entries) == 0 {
		t.Fatal("mount root directory is empty after remount")
	}

	if err := os.RemoveAll(root); err != nil {
		t.Errorf("cleanup: %v", err)
	}
}

func writeAndSync(path string, content []byte) error {
	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		return err
	}
	if _, err := f.Write(content); err != nil {
		f.Close()
		return err
	}
	if err := f.Sync(); err != nil {
		f.Close()
		return err
	}
	return f.Close()
}

func fetchFromFiler(addr, filerPath string) ([]byte, error) {
	endpoint := &url.URL{Scheme: "http", Host: addr, Path: "/" + filerPath}
	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Get(endpoint.String())
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("GET %s: %s", endpoint, resp.Status)
	}
	return io.ReadAll(resp.Body)
}
