package fs

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

	"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)

func setupTreeRoot(t *testing.T) string {
	t.Helper()
	root := t.TempDir()
	// Permissive root .zddc so subdirectory ACL checks pass.
	if err := os.WriteFile(filepath.Join(root, ".zddc"),
		[]byte("acl:\n  permissions:\n    \"*@example.com\": rwcda\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zddc.InvalidateCache(root)
	return root
}

// Listing a cascade-declared peer that doesn't exist on disk yet returns
// an empty slice instead of os.ErrNotExist, so fresh projects don't 404.
// (The tables peers mdl/rsk/ssr instead surface their synthetic spec
// entries; see TestListDirectory_MdlAggregate.)
func TestListDirectory_DeclaredPeerEmptyWhenMissing(t *testing.T) {
	root := setupTreeRoot(t)
	if err := os.MkdirAll(filepath.Join(root, "Proj"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "Proj", ".zddc"),
		[]byte("acl:\n  permissions:\n    \"*@example.com\": rwcda\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zddc.InvalidateCache(root)

	for _, peer := range []string{"working", "staging", "reviewing", "incoming"} {
		dirPath := "Proj/" + peer
		got, err := ListDirectory(context.Background(), nil, root,
			dirPath, "alice@example.com", "/"+dirPath+"/", false, false)
		if err != nil {
			t.Errorf("ListDirectory(%s) on missing peer: err = %v, want nil", dirPath, err)
			continue
		}
		if len(got) != 0 {
			t.Errorf("ListDirectory(%s) on missing peer: got %+v, want empty", dirPath, got)
		}
	}
}

// Non-canonical paths still 404 (return os.ErrNotExist) — the fallback
// only applies to cascade-declared paths.
func TestListDirectory_NonCanonicalMissing_StillNotFound(t *testing.T) {
	root := setupTreeRoot(t)
	if err := os.MkdirAll(filepath.Join(root, "Proj"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "Proj", ".zddc"),
		[]byte("acl:\n  permissions:\n    \"*@example.com\": rwcda\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zddc.InvalidateCache(root)

	_, err := ListDirectory(context.Background(), nil, root,
		"Proj/random-folder-that-doesnt-exist", "alice@example.com",
		"/Proj/random-folder-that-doesnt-exist/", false, false)
	if !os.IsNotExist(err) {
		t.Errorf("ListDirectory on a non-canonical missing path: err = %v, want IsNotExist", err)
	}
}

// The mdl/ peer root lists its party subdirs (folder-nav) plus the
// synthetic table.yaml/form.yaml spec entries. The tables tool builds
// the cross-party aggregate by recursing into the party subdirs
// client-side; the server just lists normally + advertises the spec.
func TestListDirectory_MdlAggregate(t *testing.T) {
	root := setupTreeRoot(t)
	mk := func(p string) {
		if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(p, []byte("id: x\n"), 0o644); err != nil {
			t.Fatal(err)
		}
	}
	mk(filepath.Join(root, "Proj", "mdl", "Acme", "D-001.yaml"))
	mk(filepath.Join(root, "Proj", "mdl", "Beta", "D-009.yaml"))
	zddc.InvalidateCache(root)

	got, err := ListDirectory(context.Background(), nil, root,
		"Proj/mdl", "alice@example.com", "/Proj/mdl/", false, false)
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	names := map[string]bool{}
	for _, fi := range got {
		names[fi.Name] = true
	}
	for _, want := range []string{"Acme/", "Beta/", "table.yaml", "form.yaml"} {
		if !names[want] {
			t.Errorf("mdl/ listing missing %q; got %+v", want, got)
		}
	}
}

// TestListDirectory_VerbsPerEntry — every entry in a directory listing
// carries `verbs`, the canonical "rwcda" subset granted to the caller
// at that entry's URL. Files and dirs are gated against different
// chains (files use parent's, dirs use their own), so a fenced subdir
// surfaces a different verb set than its file siblings.
func TestListDirectory_VerbsPerEntry(t *testing.T) {
	root := t.TempDir()
	// Root grants alice read across the project; bob nothing.
	if err := os.WriteFile(filepath.Join(root, ".zddc"),
		[]byte("acl:\n  permissions:\n    \"alice@example.com\": rw\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(root, "Proj", "sub"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "Proj", "doc.md"), []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	// Subdir extends alice's grant to include create — confirms the
	// dir entry's verbs come from its OWN chain, not parent's.
	if err := os.WriteFile(filepath.Join(root, "Proj", "sub", ".zddc"),
		[]byte("acl:\n  permissions:\n    \"alice@example.com\": rwc\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zddc.InvalidateCache(root)

	got, err := ListDirectory(context.Background(), nil, root,
		"Proj", "alice@example.com", "/Proj/", false, false)
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	wantVerbs := map[string]string{
		"doc.md": "rw",  // file: parent chain (project root → rw)
		"sub/":   "rwc", // dir: own chain (extends to rwc)
	}
	for _, fi := range got {
		want, ok := wantVerbs[fi.Name]
		if !ok {
			continue
		}
		if fi.Verbs != want {
			t.Errorf("entry %s verbs = %q, want %q", fi.Name, fi.Verbs, want)
		}
		if !fi.IsDir {
			wantWritable := want == "rw"
			if fi.Writable != wantWritable {
				t.Errorf("entry %s Writable = %v, want %v", fi.Name, fi.Writable, wantWritable)
			}
		}
	}
}

// TestListDirectory_VerbsActiveAdminBypass — an elevated admin sees the
// full "rwcda" verb set on every entry regardless of explicit ACL
// grants. Mirrors the InternalDecider's single bypass branch.
func TestListDirectory_VerbsActiveAdminBypass(t *testing.T) {
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, ".zddc"),
		[]byte("admins:\n  - admin@example.com\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Join(root, "Proj"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "Proj", "doc.md"), []byte("x"), 0o644); err != nil {
		t.Fatal(err)
	}
	zddc.InvalidateCache(root)

	got, err := ListDirectory(context.Background(), nil, root,
		"Proj", "admin@example.com", "/Proj/", false, true /* elevated */)
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	for _, fi := range got {
		if fi.Verbs != "rwcda" {
			t.Errorf("elevated admin %s verbs = %q, want rwcda", fi.Name, fi.Verbs)
		}
	}

	got, err = ListDirectory(context.Background(), nil, root,
		"Proj", "admin@example.com", "/Proj/", false, false)
	if err != nil {
		t.Fatalf("list un-elevated: %v", err)
	}
	for _, fi := range got {
		if fi.Verbs == "rwcda" {
			t.Errorf("un-elevated admin %s verbs = %q, should not be full grant", fi.Name, fi.Verbs)
		}
	}
}
