package zddc

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

// TestDefaultToolAt_FromEmbeddedConvention — the canonical default-tool
// rules in defaults.zddc.yaml resolve correctly for the flat top-level
// peers (and their per-party subdirs) without any on-disk .zddc.
func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
	cases := []struct {
		path string
		want string
	}{
		// The committed record (archive subtree → archive tool).
		{j("archive"), "archive"},
		{j("archive", "Acme"), "archive"},
		{j("archive", "Acme", "received"), "archive"},
		{j("archive", "Acme", "issued"), "archive"},
		// Top-level peers.
		{j("ssr"), "tables"},
		{j("mdl"), "tables"},
		{j("rsk"), "tables"},
		{j("incoming"), "classifier"},
		{j("working"), "browse"},
		{j("staging"), "transmittal"},
		{j("reviewing"), "browse"},
		// Per-party subdirs inherit the peer's default_tool.
		{j("mdl", "Acme"), "tables"},
		{j("rsk", "Acme"), "tables"},
		{j("incoming", "Acme"), "classifier"},
		{j("working", "Acme"), "browse"},
		{j("staging", "Acme"), "transmittal"},
		{j("reviewing", "Acme"), "browse"},
	}
	for _, tc := range cases {
		if got := DefaultToolAt(root, tc.path); got != tc.want {
			t.Errorf("DefaultToolAt(%q) = %q, want %q", tc.path[len(root):], got, tc.want)
		}
	}
}

// TestViewAt — default_tool/dir_tool act as sugar for the dir/dir_slash
// shapes, and an explicit views: entry overrides them.
func TestViewAt(t *testing.T) {
	resetCache()
	root := t.TempDir()
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }

	// Sugar: the embedded default_tool/dir_tool surface via ViewAt.
	if v, ok := ViewAt(root, j("mdl", "Acme"), "dir"); !ok || v.Tool != "tables" {
		t.Errorf("ViewAt(mdl/Acme, dir) = (%+v,%v), want tables", v, ok)
	}
	if v, ok := ViewAt(root, j("working", "Acme"), "dir"); !ok || v.Tool != "browse" {
		t.Errorf("ViewAt(working/Acme, dir) = (%+v,%v), want browse", v, ok)
	}
	// No file-shape declared by defaults.
	if _, ok := ViewAt(root, j("working", "Acme"), "file"); ok {
		t.Errorf("ViewAt(working/Acme, file) should be unset by default")
	}

	// Explicit views: overrides default_tool and declares a file shape.
	resetCache()
	dir := j("custom")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := WriteFile(dir, ZddcFile{
		DefaultTool: "browse",
		Views: map[string]ViewSpec{
			"dir":  {Tool: "tables", Config: "table.yaml"},
			"file": {Tool: "form", Config: "form.yaml"},
		},
	}); err != nil {
		t.Fatal(err)
	}
	if v, ok := ViewAt(root, dir, "dir"); !ok || v.Tool != "tables" || v.Config != "table.yaml" {
		t.Errorf("ViewAt(custom, dir) = (%+v,%v), want {tables,table.yaml}", v, ok)
	}
	if v, ok := ViewAt(root, dir, "file"); !ok || v.Tool != "form" || v.Config != "form.yaml" {
		t.Errorf("ViewAt(custom, file) = (%+v,%v), want {form,form.yaml}", v, ok)
	}
}

// TestHistoryAt_Defaults — edit-history defaults on for the live-editing
// peers working/mdl/rsk and the ssr registry (subtree-inheriting). The
// other peers and the WORM archive do not get history.
func TestHistoryAt_Defaults(t *testing.T) {
	resetCache()
	root := t.TempDir()
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
	cases := []struct {
		path string
		want bool
	}{
		{j("working"), true},
		{j("working", "Acme"), true},
		{j("working", "Acme", "notes"), true},
		{j("mdl"), true},
		{j("mdl", "Acme"), true},
		{j("rsk"), true},
		{j("ssr"), true},
		// No history elsewhere.
		{j("staging"), false},
		{j("reviewing"), false},
		{j("incoming"), false},
		{j("archive"), false},
		{j("archive", "Acme", "received"), false},
	}
	for _, tc := range cases {
		if got := HistoryAt(root, tc.path); got != tc.want {
			t.Errorf("HistoryAt(%q) = %v, want %v", tc.path[len(root):], got, tc.want)
		}
	}
}

// TestDirToolAt — the trailing-slash form floors at "browse" for every
// path (the embedded convention sets dir_tool nowhere), and an on-disk
// .zddc can override it for a subtree.
func TestDirToolAt(t *testing.T) {
	resetCache()
	root := t.TempDir()
	for _, p := range []string{
		filepath.Join(root, "Project-X"),
		filepath.Join(root, "Project-X", "working", "Acme"),
		filepath.Join(root, "Project-X", "mdl", "Acme"),
		filepath.Join(root, "Project-X", "random", "deep", "folder"),
	} {
		if got := DirToolAt(root, p); got != "browse" {
			t.Errorf("DirToolAt(%q) = %q, want browse", p[len(root):], got)
		}
	}
	specialDir := filepath.Join(root, "Special")
	if err := os.MkdirAll(specialDir, 0o755); err != nil {
		t.Fatal(err)
	}
	writeZddc(t, specialDir, "dir_tool: tables\n")
	resetCache()
	if got := DirToolAt(root, filepath.Join(root, "Special")); got != "tables" {
		t.Errorf("DirToolAt(Special) = %q, want tables", got)
	}
	if got := DirToolAt(root, filepath.Join(root, "Special", "deep")); got != "tables" {
		t.Errorf("DirToolAt(Special/deep) = %q, want tables (cascade)", got)
	}
	if got := DirToolAt(root, filepath.Join(root, "Other")); got != "browse" {
		t.Errorf("DirToolAt(Other) = %q, want browse (override scoped to Special)", got)
	}
}

// TestCanonicalFolderAt — structural detection of the canonical slots the
// browse SPA scope-gates context-menu actions against, in the flat-peer
// layout: depth-2 peers, depth-3 <peer>/<party> (workspace/register peers
// report their peer), and depth-4 archive/<party>/{received,issued}.
func TestCanonicalFolderAt(t *testing.T) {
	resetCache()
	root := t.TempDir()
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
	cases := []struct {
		path string
		want string
	}{
		{j("archive"), "archive"},
		{j("ssr"), "ssr"},
		{j("mdl"), "mdl"},
		{j("rsk"), "rsk"},
		{j("incoming"), "incoming"},
		{j("working"), "working"},
		{j("staging"), "staging"},
		{j("reviewing"), "reviewing"},
		// <peer>/<party> reports the peer (not archive).
		{j("mdl", "Acme"), "mdl"},
		{j("working", "Acme"), "working"},
		{j("incoming", "Acme"), "incoming"},
		// archive/<party> is not itself a slot; received/issued are.
		{j("archive", "Acme"), ""},
		{j("archive", "Acme", "received"), "received"},
		{j("archive", "Acme", "issued"), "issued"},
		// Non-slots.
		{root, ""},
		{j(), ""},
		{j("mdl", "Acme", "sub"), ""},
		{j("archive", "Acme", "received", "Acme-0042"), ""},
		{j("random", "dir"), ""},
	}
	for _, tc := range cases {
		if got := CanonicalFolderAt(root, tc.path); got != tc.want {
			t.Errorf("CanonicalFolderAt(%q) = %q, want %q", tc.path[len(root):], got, tc.want)
		}
	}
}

// TestAutoOwnAt_FromEmbeddedConvention — auto_own is declared at the
// <party> level of the workspace peers (incoming/working/staging/
// reviewing); the registers (mdl/rsk/ssr) and the WORM archive don't
// auto-own.
func TestAutoOwnAt_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
	cases := []struct {
		path string
		want bool
	}{
		{j("working", "Acme"), true},
		{j("staging", "Acme"), true},
		{j("reviewing", "Acme"), true},
		{j("incoming", "Acme"), true},
		{j("mdl", "Acme"), false},
		{j("rsk", "Acme"), false},
		{j("archive", "Acme", "received"), false},
		{j("archive", "Acme", "issued"), false},
	}
	for _, tc := range cases {
		if got := AutoOwnAt(root, tc.path); got != tc.want {
			t.Errorf("AutoOwnAt(%q) = %v, want %v", tc.path[len(root):], got, tc.want)
		}
	}
}

// TestVirtualAt_FromEmbeddedConvention — the flat-peer layout has no
// virtual: directories (every peer is physical), so VirtualAt is false
// everywhere unless an operator sets it.
func TestVirtualAt_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
	for _, p := range []string{
		j("ssr"), j("mdl"), j("rsk"), j("working"), j("staging"), j("reviewing"), j("incoming"),
		j("mdl", "Acme"), j("working", "Acme"), j("archive", "Acme", "received"),
	} {
		if VirtualAt(root, p) {
			t.Errorf("VirtualAt(%q) = true, want false (no virtual peers)", p[len(root):])
		}
	}
}

// TestIsDeclaredPath_FromEmbeddedConvention — the top-level peers are
// cascade-declared even on a fresh root; arbitrary names are not.
func TestIsDeclaredPath_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
	cases := []struct {
		path string
		want bool
	}{
		{j("archive"), true},
		{j("ssr"), true},
		{j("incoming"), true},
		{j("working"), true},
		{j("staging"), true},
		{j("reviewing"), true},
		{j("mdl"), true},
		{j("rsk"), true},
		{j("junk"), false},
	}
	for _, tc := range cases {
		if got := IsDeclaredPath(root, tc.path); got != tc.want {
			t.Errorf("IsDeclaredPath(%q) = %v, want %v", tc.path[len(root):], got, tc.want)
		}
	}
}
