package zddc

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

// TestMatchGlob_LiteralWinsOverWildcard — a literal key always beats
// the "*" fallback even when both are present.
func TestMatchGlob_LiteralWinsOverWildcard(t *testing.T) {
	m := map[string]ZddcFile{
		"archive": {Title: "literal"},
		"*":       {Title: "wildcard"},
	}
	got := matchGlob(m, "archive")
	if got == nil || got.Title != "literal" {
		t.Errorf("matchGlob(archive) = %+v, want literal", got)
	}
}

// TestMatchGlob_CaseInsensitiveLiteral — operator writes lowercase,
// on-disk segment is PascalCase. Match should still hit.
func TestMatchGlob_CaseInsensitiveLiteral(t *testing.T) {
	m := map[string]ZddcFile{"archive": {Title: "ok"}}
	got := matchGlob(m, "Archive")
	if got == nil || got.Title != "ok" {
		t.Errorf("matchGlob(Archive) = %+v, want case-insensitive match", got)
	}
}

// TestMatchGlob_WildcardFallback — no literal key, the * key matches
// any segment.
func TestMatchGlob_WildcardFallback(t *testing.T) {
	m := map[string]ZddcFile{"*": {Title: "any"}}
	got := matchGlob(m, "Project-1")
	if got == nil || got.Title != "any" {
		t.Errorf("matchGlob(Project-1) under * = %+v, want wildcard match", got)
	}
}

// TestMatchGlob_NoMatch — no literal, no wildcard → nil.
func TestMatchGlob_NoMatch(t *testing.T) {
	m := map[string]ZddcFile{"archive": {}}
	if got := matchGlob(m, "working"); got != nil {
		t.Errorf("matchGlob(working) = %+v, want nil", got)
	}
}

// TestEffectivePolicy_PathsContributesViaWildcard — a root-level
// paths: tree with "*" / archive / incoming applies to a deep path
// even when none of those directories have a real .zddc.
func TestEffectivePolicy_PathsContributesViaWildcard(t *testing.T) {
	resetCache()
	root := t.TempDir()
	// Root .zddc declares behaviour for any-project / archive /
	// any-party / incoming via a nested paths: tree. The actual
	// directories may not exist; the walker fires regardless.
	writeZddc(t, root, `title: root
paths:
  "*":
    paths:
      archive:
        display:
          incoming: "Inbox"
        paths:
          "*":
            paths:
              incoming:
                title: "incoming-leaf"
`)
	leaf := filepath.Join(root, "Project-1", "archive", "PartyA", "incoming")
	if err := os.MkdirAll(leaf, 0o755); err != nil {
		t.Fatal(err)
	}
	chain, err := EffectivePolicy(root, leaf)
	if err != nil {
		t.Fatal(err)
	}
	// chain.Levels: [root, Project-1, archive, PartyA, incoming] → 5
	if got, want := len(chain.Levels), 5; got != want {
		t.Fatalf("len(chain.Levels) = %d, want %d", got, want)
	}
	// At /Project-1/archive/ the display override should now be
	// merged in via the ancestor paths.
	if got := chain.Levels[2].Display["incoming"]; got != "Inbox" {
		t.Errorf("Levels[2].Display[incoming] = %q, want Inbox", got)
	}
	// At the leaf, the deepest paths: contribution sets title.
	if got := chain.Levels[4].Title; got != "incoming-leaf" {
		t.Errorf("Levels[4].Title = %q, want incoming-leaf", got)
	}
}

// TestEffectivePolicy_PathsOnDiskWins — virtual contributions are
// lower specificity than on-disk; on-disk overrides per-field.
func TestEffectivePolicy_PathsOnDiskWins(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `paths:
  archive:
    title: "from-virtual"
`)
	archive := filepath.Join(root, "archive")
	if err := os.MkdirAll(archive, 0o755); err != nil {
		t.Fatal(err)
	}
	writeZddc(t, archive, `title: "from-on-disk"`)

	chain, err := EffectivePolicy(root, archive)
	if err != nil {
		t.Fatal(err)
	}
	// chain.Levels[1] is /archive — should reflect on-disk wins.
	if got := chain.Levels[1].Title; got != "from-on-disk" {
		t.Errorf("Levels[1].Title = %q, want %q (on-disk overrides virtual)",
			got, "from-on-disk")
	}
}

// TestEffectivePolicy_PathsAbsentIsBitIdentical — without a paths:
// declaration anywhere, the walker reduces to the prior linear
// cascade behaviour.
func TestEffectivePolicy_PathsAbsentIsBitIdentical(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `title: "root"
admins:
  - admin@example.com
`)
	leaf := filepath.Join(root, "Project-1", "archive")
	if err := os.MkdirAll(leaf, 0o755); err != nil {
		t.Fatal(err)
	}
	writeZddc(t, leaf, `title: "leaf"`)
	chain, err := EffectivePolicy(root, leaf)
	if err != nil {
		t.Fatal(err)
	}
	if got := chain.Levels[0].Title; got != "root" {
		t.Errorf("Levels[0].Title = %q, want %q", got, "root")
	}
	if got := chain.Levels[2].Title; got != "leaf" {
		t.Errorf("Levels[2].Title = %q, want %q", got, "leaf")
	}
	// Mid level has no .zddc and no paths: contribution → empty.
	if got := chain.Levels[1].Title; got != "" {
		t.Errorf("Levels[1].Title = %q, want empty", got)
	}
}

// TestEffectivePolicy_InheritFalseDropsAncestorPaths — inherit:false
// at the on-disk root drops contributions from the embedded layer
// (already covered in defaults_test.go) AND drops contributions
// from ancestor paths: that hadn't yet matched. Here we set it on
// the project level: an embedded paths: contribution that would
// have applied to deeper levels is suppressed.
func TestEffectivePolicy_InheritFalseDropsAncestorPaths(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `paths:
  Project-1:
    title: "ancestor-virtual"
    paths:
      sub:
        title: "ancestor-deep"
`)
	sub := filepath.Join(root, "Project-1", "sub")
	if err := os.MkdirAll(sub, 0o755); err != nil {
		t.Fatal(err)
	}
	// Project-1/.zddc sets inherit:false. Effect: the ancestor's
	// /Project-1/sub/ contribution does NOT reach the sub level.
	writeZddc(t, filepath.Join(root, "Project-1"),
		`title: "standalone"
inherit: false
`)
	chain, err := EffectivePolicy(root, sub)
	if err != nil {
		t.Fatal(err)
	}
	if got := chain.Levels[1].Title; got != "standalone" {
		t.Errorf("Levels[1].Title = %q, want standalone (inherit:false should drop ancestor virtual contribution)",
			got)
	}
	if got := chain.Levels[2].Title; got != "" {
		t.Errorf("Levels[2].Title = %q, want empty (ancestor paths: blocked by inherit:false)",
			got)
	}
}
