package fs

import (
	"os"
	"path/filepath"
	"runtime"
	"testing"
)

func mkdir(t *testing.T, parts ...string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Join(parts...), 0o755); err != nil {
		t.Fatal(err)
	}
}

func TestResolveCanonical_RootAndEmpty(t *testing.T) {
	root := t.TempDir()
	for _, in := range []string{"/", "", "//"} {
		abs, url, ok := ResolveCanonical(root, in)
		if !ok {
			t.Fatalf("%q: ok=false", in)
		}
		if abs != root || url != "/" {
			t.Fatalf("%q: abs=%q url=%q", in, abs, url)
		}
	}
}

func TestResolveCanonical_ExactCase(t *testing.T) {
	root := t.TempDir()
	mkdir(t, root, "archive", "incoming")
	abs, url, ok := ResolveCanonical(root, "/archive/incoming")
	if !ok || url != "/archive/incoming" {
		t.Fatalf("ok=%v url=%q", ok, url)
	}
	if abs != filepath.Join(root, "archive", "incoming") {
		t.Fatalf("abs=%q", abs)
	}
}

func TestResolveCanonical_MixedCaseURLLowercaseOnDisk(t *testing.T) {
	root := t.TempDir()
	mkdir(t, root, "archive", "incoming")
	abs, url, ok := ResolveCanonical(root, "/Archive/Incoming")
	if !ok || url != "/archive/incoming" {
		t.Fatalf("ok=%v url=%q", ok, url)
	}
	if abs != filepath.Join(root, "archive", "incoming") {
		t.Fatalf("abs=%q", abs)
	}
}

func TestResolveCanonical_OnlyMixedCaseExists(t *testing.T) {
	root := t.TempDir()
	mkdir(t, root, "Archive", "Incoming")
	abs, url, ok := ResolveCanonical(root, "/archive/incoming")
	if !ok || url != "/Archive/Incoming" {
		t.Fatalf("ok=%v url=%q", ok, url)
	}
	if abs != filepath.Join(root, "Archive", "Incoming") {
		t.Fatalf("abs=%q", abs)
	}
}

func TestResolveCanonical_BothCasesExistLowercaseWins(t *testing.T) {
	if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
		t.Skip("filesystem may be case-insensitive; tiebreak only meaningful on case-sensitive FS")
	}
	root := t.TempDir()
	mkdir(t, root, "Archive")
	mkdir(t, root, "archive")
	if err := os.WriteFile(filepath.Join(root, "Archive", "marker"), []byte("upper"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "archive", "marker"), []byte("lower"), 0o644); err != nil {
		t.Fatal(err)
	}

	for _, in := range []string{"/Archive/marker", "/archive/marker", "/aRcHiVe/marker"} {
		abs, url, ok := ResolveCanonical(root, in)
		if !ok {
			t.Fatalf("%q: ok=false", in)
		}
		if url != "/archive/marker" {
			t.Fatalf("%q: url=%q (want /archive/marker)", in, url)
		}
		body, err := os.ReadFile(abs)
		if err != nil {
			t.Fatalf("%q: read %s: %v", in, abs, err)
		}
		if string(body) != "lower" {
			t.Fatalf("%q: body=%q (want \"lower\" — lowercase variant must win)", in, body)
		}
	}
}

func TestResolveCanonical_NonexistentSegmentPreservesRemainder(t *testing.T) {
	root := t.TempDir()
	mkdir(t, root, "archive")
	abs, url, ok := ResolveCanonical(root, "/Archive/.archive/TR-001.html")
	if !ok {
		t.Fatal("ok=false")
	}
	// Walk canonicalizes "Archive" to "archive"; the virtual ".archive"
	// segment doesn't exist on disk, so the remainder passes through
	// unchanged so the dispatcher's virtual-prefix routing still fires.
	if url != "/archive/.archive/TR-001.html" {
		t.Fatalf("url=%q", url)
	}
	if abs != filepath.Join(root, "archive", ".archive", "TR-001.html") {
		t.Fatalf("abs=%q", abs)
	}
}

func TestResolveCanonical_FileSegmentTerminatesWalk(t *testing.T) {
	root := t.TempDir()
	mkdir(t, root, "archive")
	if err := os.WriteFile(filepath.Join(root, "archive", "Doc.PDF"), []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	abs, url, ok := ResolveCanonical(root, "/Archive/doc.pdf")
	if !ok {
		t.Fatal("ok=false")
	}
	// On Linux Doc.PDF exists but doc.pdf does not — exact-case tier
	// finds Doc.PDF and uses it.
	if url != "/archive/Doc.PDF" {
		t.Fatalf("url=%q", url)
	}
	_ = abs
}

func TestResolveCanonical_RejectsEscape(t *testing.T) {
	root := t.TempDir()
	mkdir(t, root, "archive")
	// filepath.Clean reduces "/archive/../.." to "/.."; Resolve sees
	// segments that don't exist on disk and walks them verbatim. The
	// final containment check must reject the result.
	_, _, ok := ResolveCanonical(root, "/archive/../../etc")
	if ok {
		t.Fatal("expected ok=false for escape path")
	}
}

func TestResolveCanonical_TrailingSlashesNormalized(t *testing.T) {
	root := t.TempDir()
	mkdir(t, root, "archive", "incoming")
	_, url, ok := ResolveCanonical(root, "/Archive/Incoming/")
	if !ok {
		t.Fatal("ok=false")
	}
	if url != "/archive/incoming" {
		t.Fatalf("url=%q", url)
	}
}
