package zddc

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

func TestIsAutoOwnPath(t *testing.T) {
	root := "/srv/zddc"
	cases := map[string]bool{
		// Project-root canonical positions.
		"/srv/zddc/Project/working":            true,
		"/srv/zddc/Project/staging":            true,
		"/srv/zddc/Project/Working":            true, // case-fold
		"/srv/zddc/Project/STAGING":            true, // case-fold
		"/srv/zddc/Project/archive":            false,
		"/srv/zddc/Project/reviewing":          false,
		"/srv/zddc/Project/random":             false,

		// Per-party position.
		"/srv/zddc/Project/archive/ACME/incoming": true,
		"/srv/zddc/Project/archive/ACME/Incoming": true, // case-fold
		"/srv/zddc/Project/Archive/ACME/incoming": true, // case-fold archive
		"/srv/zddc/Project/archive/ACME/received": false,
		"/srv/zddc/Project/archive/ACME/issued":   false,
		"/srv/zddc/Project/archive/ACME/mdl":      false,

		// Wrong depth — incoming inside something other than archive/<party>/.
		"/srv/zddc/Project/working/incoming":           false,
		"/srv/zddc/Project/random/sub/incoming":        false,
		"/srv/zddc/Project/incoming":                   false, // depth 1 with incoming
		"/srv/zddc/Project/archive/incoming":           false, // depth 2
		"/srv/zddc/Project/archive/ACME/incoming/sub":  false, // child of incoming, not incoming itself

		// Outside root.
		"/elsewhere/working": false,
		// Root itself or one above.
		"/srv/zddc":         false,
		"/srv/zddc/Project": false,
	}
	for in, want := range cases {
		if got := IsAutoOwnPath(in, root); got != want {
			t.Errorf("IsAutoOwnPath(%q, %q) = %v, want %v", in, root, got, want)
		}
	}
}

func TestIsWormPath(t *testing.T) {
	cases := map[string]bool{
		"":                                     false,
		"/":                                    false,
		"/Project/archive/ACME/issued":         true,
		"/Project/archive/ACME/issued/":        true,
		"/Project/archive/ACME/issued/foo.pdf": true,
		"/Project/archive/ACME/received/x":     true,
		"/Project/archive/ACME/Issued/x":       true, // case-fold
		"/Project/Archive/ACME/issued/x":       true, // case-fold
		"/Project/archive/ACME/ISSUED/x":       true, // case-fold

		// Per-party MDL/incoming aren't WORM.
		"/Project/archive/ACME/incoming/x": false,
		"/Project/archive/ACME/mdl/x":      false,

		// Bare "issued" or "received" not under archive/<party>/ — no WORM.
		"/Project/issued/x":          false,
		"/Project/received/x":        false,
		"/Project/working/issued.md": false, // file basename, not a path segment match
		"/Project/working/issued":    false, // "working" is not "archive"

		// Self-folder is symmetric (any party name works).
		"/Project/archive/Self-Org/issued/x.pdf": true,

		// Nested or deep.
		"/multi/Project/archive/Vendor/received/sub/file.pdf": true,
	}
	for in, want := range cases {
		if got := IsWormPath(in); got != want {
			t.Errorf("IsWormPath(%q) = %v, want %v", in, got, want)
		}
	}
}

func TestWormFolderLevelIndex(t *testing.T) {
	// Path /Project/archive/ACME/issued/foo.pdf
	//   parts: [Project, archive, ACME, issued, foo.pdf]
	//   issued is segment index 3, chain index 4.
	if got := WormFolderLevelIndex("/Project/archive/ACME/issued/foo.pdf", 6); got != 4 {
		t.Errorf("issued at depth 4: got %d, want 4", got)
	}
	// Same path, but the chain only has 4 levels (numLevels=4 → idx must be < 4).
	if got := WormFolderLevelIndex("/Project/archive/ACME/issued/foo.pdf", 4); got != -1 {
		t.Errorf("clamp: got %d, want -1", got)
	}
	// No WORM segment.
	if got := WormFolderLevelIndex("/Project/working/foo.md", 5); got != -1 {
		t.Errorf("no worm: got %d, want -1", got)
	}
	// Empty.
	if got := WormFolderLevelIndex("", 5); got != -1 {
		t.Errorf("empty: got %d, want -1", got)
	}
	// Nested archive/<party>/issued — deepest wins.
	// parts:    [P, archive, A, received, archive, B, issued, x]
	// indices:   0   1        2  3         4        5  6        7
	// outer match: i=1 (archive), parts[3]=received → segment idx 3, chain idx 4
	// inner match: i=4 (archive), parts[6]=issued   → segment idx 6, chain idx 7
	// deepest = 7.
	if got := WormFolderLevelIndex("/P/archive/A/received/archive/B/issued/x", 12); got != 7 {
		t.Errorf("nested: got %d, want 7", got)
	}
}

func TestWormMaskStripsWDA(t *testing.T) {
	rwcda, _ := ParseVerbSet("rwcda")
	masked := WormMask(rwcda)
	if got := masked.String(); got != "rc" {
		t.Errorf("WormMask(rwcda) = %q, want rc", got)
	}

	rw, _ := ParseVerbSet("rw")
	if got := WormMask(rw).String(); got != "r" {
		t.Errorf("WormMask(rw) = %q, want r", got)
	}

	cd, _ := ParseVerbSet("cd")
	if got := WormMask(cd).String(); got != "c" {
		t.Errorf("WormMask(cd) = %q, want c", got)
	}

	if got := WormMask(0).String(); got != "" {
		t.Errorf("WormMask(0) = %q, want empty", got)
	}
}

func TestResolveCanonicalCaseFold(t *testing.T) {
	dir := t.TempDir()
	if err := os.MkdirAll(filepath.Join(dir, "Working"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(dir, "ARCHIVE"), 0o755); err != nil {
		t.Fatal(err)
	}
	// A regular file with a canonical name must NOT be returned (we only resolve directories).
	if err := os.WriteFile(filepath.Join(dir, "staging"), []byte{}, 0o644); err != nil {
		t.Fatal(err)
	}

	cases := map[string]string{
		"working":   "Working", // PascalCase wins because it exists on disk
		"WORKING":   "Working",
		"Working":   "Working",
		"archive":   "ARCHIVE",
		"reviewing": "", // not present
		"staging":   "", // present as a file, not a directory — must skip
	}
	for logical, want := range cases {
		got, err := ResolveCanonical(dir, logical)
		if err != nil {
			t.Errorf("ResolveCanonical(%q): %v", logical, err)
			continue
		}
		if got != want {
			t.Errorf("ResolveCanonical(%q) = %q, want %q", logical, got, want)
		}
	}
}

func TestResolveCanonicalMissingParent(t *testing.T) {
	got, err := ResolveCanonical(filepath.Join(t.TempDir(), "does-not-exist"), "working")
	if err != nil {
		t.Errorf("expected nil error for missing parent, got %v", err)
	}
	if got != "" {
		t.Errorf("expected empty result for missing parent, got %q", got)
	}
}

func TestCanonicalLists(t *testing.T) {
	hasAll := func(have, want []string) bool {
		set := map[string]bool{}
		for _, n := range have {
			set[n] = true
		}
		for _, n := range want {
			if !set[n] {
				return false
			}
		}
		return true
	}
	if !hasAll(ProjectRootFolders, []string{"archive", "working", "staging", "reviewing"}) {
		t.Errorf("ProjectRootFolders = %v, missing entries", ProjectRootFolders)
	}
	if !hasAll(PartyFolders, []string{"mdl", "incoming", "received", "issued"}) {
		t.Errorf("PartyFolders = %v, missing entries", PartyFolders)
	}
	if !hasAll(AutoOwnCanonicalNames, []string{"working", "staging", "incoming"}) {
		t.Errorf("AutoOwnCanonicalNames = %v, missing entries", AutoOwnCanonicalNames)
	}
	if !hasAll(VirtualOnlyCanonicalNames, []string{"reviewing"}) {
		t.Errorf("VirtualOnlyCanonicalNames = %v, missing entries", VirtualOnlyCanonicalNames)
	}
}

func TestIsProjectRootFolder(t *testing.T) {
	cases := map[string]bool{
		// Canonical positions, lowercase.
		"Proj/archive":   true,
		"Proj/working":   true,
		"Proj/staging":   true,
		"Proj/reviewing": true,
		// Case-fold.
		"Proj/Working":   true,
		"Proj/STAGING":   true,
		"Proj/Reviewing": true,
		// Trailing slash tolerated (handler trims but be defensive).
		"Proj/working/": true,
		// Non-canonical second segment.
		"Proj/random":  false,
		"Proj/archive2": false,
		// Wrong depth — root, single segment, or deeper.
		"":                          false,
		"Proj":                      false,
		"Proj/working/casey":        false,
		"Proj/archive/ACME":         false,
		"Proj/archive/ACME/issued":  false,
	}
	for path, want := range cases {
		got := IsProjectRootFolder(path)
		if got != want {
			t.Errorf("IsProjectRootFolder(%q) = %v, want %v", path, got, want)
		}
	}
}
