package zddc

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

// TestDefaultToolAt_FromEmbeddedConvention — the canonical default-
// tool rules in defaults.zddc.yaml should resolve correctly for the
// well-known paths without any on-disk .zddc.
func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	cases := []struct {
		path string
		want string
	}{
		{filepath.Join(root, "Project-X", "archive"), "archive"},
		{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), "tables"},
		{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), "classifier"},
		{filepath.Join(root, "Project-X", "archive", "Acme", "received"), "archive"},
		{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), "archive"},
		{filepath.Join(root, "Project-X", "working"), "mdedit"},
		{filepath.Join(root, "Project-X", "working", "alice@example.com"), "mdedit"},
		{filepath.Join(root, "Project-X", "staging"), "transmittal"},
		{filepath.Join(root, "Project-X", "reviewing"), "mdedit"},
	}
	for _, tc := range cases {
		got := DefaultToolAt(root, tc.path)
		if got != tc.want {
			t.Errorf("DefaultToolAt(%q) = %q, want %q",
				tc.path[len(root):], got, tc.want)
		}
	}
}

// TestAutoOwnAt_FromEmbeddedConvention — auto_own should be true for
// working/incoming/staging (per the convention) and false elsewhere.
func TestAutoOwnAt_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	cases := []struct {
		path string
		want bool
	}{
		{filepath.Join(root, "Project-X", "working"), true},
		{filepath.Join(root, "Project-X", "working", "alice@example.com"), true},
		{filepath.Join(root, "Project-X", "staging"), true},
		{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
		{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
		{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), false},
		{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), false},
	}
	for _, tc := range cases {
		got := AutoOwnAt(root, tc.path)
		if got != tc.want {
			t.Errorf("AutoOwnAt(%q) = %v, want %v",
				tc.path[len(root):], got, tc.want)
		}
	}
}

// TestVirtualAt_FromEmbeddedConvention — reviewing/ and mdl/ are
// declared virtual; everything else (including working/staging/
// incoming) materialises on disk.
func TestVirtualAt_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	cases := []struct {
		path string
		want bool
	}{
		{filepath.Join(root, "Project-X", "reviewing"), true},
		{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), true},
		{filepath.Join(root, "Project-X", "working"), false},
		{filepath.Join(root, "Project-X", "staging"), false},
		{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), false},
		{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
	}
	for _, tc := range cases {
		got := VirtualAt(root, tc.path)
		if got != tc.want {
			t.Errorf("VirtualAt(%q) = %v, want %v",
				tc.path[len(root):], got, tc.want)
		}
	}
}

// TestIsDeclaredPath_FromEmbeddedConvention — canonical paths under
// the convention are declared even on a fresh root; arbitrary paths
// are not.
func TestIsDeclaredPath_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	cases := []struct {
		path string
		want bool
	}{
		{filepath.Join(root, "Project-X", "archive"), true},
		{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
		{filepath.Join(root, "Project-X", "working"), true},
		{filepath.Join(root, "Project-X", "reviewing"), true},
		{filepath.Join(root, "Project-X", "junk"), false}, // not in convention
	}
	for _, tc := range cases {
		got := IsDeclaredPath(root, tc.path)
		if got != tc.want {
			t.Errorf("IsDeclaredPath(%q) = %v, want %v",
				tc.path[len(root):], got, tc.want)
		}
	}
}

// TestChildrenDeclaredAt_FromEmbeddedConvention — at a project
// root, the four canonical children should be enumerated.
func TestChildrenDeclaredAt_FromEmbeddedConvention(t *testing.T) {
	resetCache()
	root := t.TempDir()
	got := ChildrenDeclaredAt(root, filepath.Join(root, "Project-X"))
	want := map[string]bool{
		"archive": true, "working": true, "staging": true, "reviewing": true,
	}
	if len(got) != len(want) {
		t.Errorf("ChildrenDeclaredAt = %v, want exactly %v keys", got, want)
	}
	for _, n := range got {
		if !want[n] {
			t.Errorf("unexpected child %q", n)
		}
	}
}

// TestOperatorOverride_DefaultsAreSurfaceable — operator can override
// any of the canonical tool defaults by mirroring the structure in an
// on-disk .zddc. The override wins.
func TestOperatorOverride_DefaultsAreSurfaceable(t *testing.T) {
	resetCache()
	root := t.TempDir()
	if err := os.MkdirAll(filepath.Join(root, "Special", "working"), 0o755); err != nil {
		t.Fatal(err)
	}
	// Operator declares that Special/working uses classifier
	// instead of the embedded-default mdedit.
	writeZddc(t, filepath.Join(root, "Special", "working"),
		"default_tool: classifier\n")

	if got := DefaultToolAt(root, filepath.Join(root, "Special", "working")); got != "classifier" {
		t.Errorf("operator override should set default_tool=classifier, got %q", got)
	}
	// Default still applies at other projects.
	if got := DefaultToolAt(root, filepath.Join(root, "Project-Y", "working")); got != "mdedit" {
		t.Errorf("default convention should hold at unchanged paths, got %q", got)
	}
}

// TestDefaultToolAt_PropagatesToDescendants — once an ancestor sets
// default_tool, descendants inherit it unless they override. So a
// path under working/ that isn't explicitly declared in paths: still
// gets mdedit as its default tool.
func TestDefaultToolAt_PropagatesToDescendants(t *testing.T) {
	resetCache()
	root := t.TempDir()
	// Deep path under working/ — not explicitly mentioned in paths:.
	deep := filepath.Join(root, "Project-X", "working", "alice@example.com", "notes", "sub", "deep")
	if got := DefaultToolAt(root, deep); got != "mdedit" {
		t.Errorf("DefaultToolAt(%q) = %q, want mdedit (cascade propagation)",
			deep[len(root):], got)
	}
}

// TestAutoOwnAt_DescendantCanDisable — explicit auto_own:false at a
// descendant overrides an ancestor's auto_own:true.
func TestAutoOwnAt_DescendantCanDisable(t *testing.T) {
	resetCache()
	root := t.TempDir()
	deepDir := filepath.Join(root, "Project-X", "working", "alice@example.com")
	if err := os.MkdirAll(deepDir, 0o755); err != nil {
		t.Fatal(err)
	}
	writeZddc(t, deepDir, "auto_own: false\n")
	if got := AutoOwnAt(root, deepDir); got != false {
		t.Errorf("AutoOwnAt(%q) = %v, want false (descendant override)", deepDir, got)
	}
	// Ancestor still has it true.
	ancestor := filepath.Join(root, "Project-X", "working")
	if got := AutoOwnAt(root, ancestor); got != true {
		t.Errorf("AutoOwnAt(%q) = %v, want true (ancestor untouched)", ancestor, got)
	}
}

// TestInheritFalse_BlocksEmbeddedDefaults — at the on-disk root,
// inherit:false stops the embedded layer from contributing. The
// canonical paths are then no longer declared.
func TestInheritFalse_BlocksEmbeddedDefaults(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, "inherit: false\n")
	// Without the embedded defaults' paths: tree, IsDeclaredPath
	// returns false for previously-canonical paths.
	if IsDeclaredPath(root, filepath.Join(root, "Project-X", "archive")) {
		t.Errorf("with inherit:false at root, archive should not be a declared path")
	}
	if DefaultToolAt(root, filepath.Join(root, "Project-X", "working")) != "" {
		t.Errorf("with inherit:false at root, default_tool should be empty for working")
	}
}
