package zddc

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

// TestStandardRoles_DocControllerScopedCreate — DC authority comes
// PURELY from the cascade now (no subtree-admin / admins: list). The
// model:
//   - rw at the project level (read + overwrite-existing, no `c`)
//   - rwc at archive/ (can create party subfolders)
//   - When DC mkdirs archive/<party>/, ensure.go writes an auto-own
//     .zddc granting both the creator email AND the document_controller
//     role rwcda there (via auto_own_roles in defaults). This test
//     simulates that .zddc directly so the cascade behaviour can be
//     asserted in isolation.
//   - From the party's auto-own .zddc, the role rwcda cascades down to
//     descendants by default; explicit slot grants (rwcd at incoming/
//     and staging/) shadow it where the workflow needs `d`.
//   - At received/issued (WORM): the WORM mask strips w/d/a from the
//     inherited rwcda; the worm: list restores c → effective cr.
//   - NOT subtree-admin anywhere — no admins: entries for the role.
func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
	resetCache()
	root := t.TempDir()
	// DC authority comes PURELY from the cascade peer grants in
	// internal/zddc/defaults/ — no auto-own / admins: list. DCs are typically
	// in project_team too (the *@example.com wildcard); the defaults
	// restate document_controller at each peer so the within-level union
	// gives the DC the higher grant.
	writeZddc(t, root, `roles:
  document_controller:
    members: ["dc@example.com"]
  project_team:
    members: ["*@example.com"]
`)
	resetCache()

	dc := "dc@example.com"
	j := func(p ...string) string { return filepath.Join(append([]string{root, "Proj"}, p...)...) }

	mustVerbs := func(dir string, want string) {
		t.Helper()
		chain, err := EffectivePolicy(root, dir)
		if err != nil {
			t.Fatalf("EffectivePolicy(%q): %v", dir, err)
		}
		var got VerbSet
		if g, inWorm := WormZoneGrant(chain, dc); inWorm {
			got = (EffectiveVerbs(chain, dc) & VerbR) | (g & VerbsRC)
		} else {
			got = EffectiveVerbs(chain, dc)
		}
		if got.String() != want {
			t.Errorf("doc controller verbs at %s = %q, want %q", dir[len(root):], got.String(), want)
		}
	}

	// Project level: rw (no c).
	mustVerbs(j(), "rw")
	mustVerbs(j("random-folder"), "rw")
	// archive subtree is WORM: read + worm-create only (w/d/a stripped).
	mustVerbs(j("archive"), "rc")
	mustVerbs(j("archive", "Acme"), "rc")
	mustVerbs(j("archive", "Acme", "received"), "rc")
	mustVerbs(j("archive", "Acme", "issued"), "rc")
	// Workspace peers: full authority via the peer-level DC grant.
	mustVerbs(j("working", "Acme"), "rwcda")
	mustVerbs(j("staging", "Acme"), "rwcda")
	mustVerbs(j("reviewing", "Acme"), "rwcda")
	mustVerbs(j("incoming", "Acme"), "rwcd")
	// Registers.
	mustVerbs(j("mdl", "Acme"), "rwcd")
	mustVerbs(j("rsk", "Acme"), "rwcd")
	mustVerbs(j("ssr"), "rwc")

	// NOT subtree-admin anywhere — no admins: grant for the role.
	for _, p := range []string{j(), j("archive"), j("working", "Acme"), j("ssr"), j("mdl", "Acme")} {
		if IsSubtreeAdmin(root, p, Principal{Email: dc, Elevated: true}) {
			t.Errorf("doc controller should NOT be subtree-admin of %s (no admins: list anywhere)", p[len(root):])
		}
	}
}

// TestStandardRoles_DocControllerMultiDC — a second DC added to the
// role gets the SAME rwcda at every party that any DC created,
// because the auto-own .zddc grants the role (not just the creator's
// email) via auto_own_roles in defaults.
func TestStandardRoles_DocControllerMultiDC(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `roles:
  document_controller:
    members: ["dc1@example.com", "dc2@example.com"]
`)
	// dc1 created the party folder; the auto-own .zddc lists both
	// dc1 (creator email) and the document_controller role (from
	// auto_own_roles in defaults).
	partyDir := filepath.Join(root, "Proj", "archive", "Acme")
	if err := os.MkdirAll(partyDir, 0o755); err != nil {
		t.Fatal(err)
	}
	writeZddc(t, partyDir, `acl:
  permissions:
    "dc1@example.com": rwcda
    document_controller: rwcda
created_by: dc1@example.com
`)
	resetCache()

	chain, _ := EffectivePolicy(root, partyDir)
	// dc1 (creator) has rwcda directly.
	if got := EffectiveVerbs(chain, "dc1@example.com"); got.String() != "rwcda" {
		t.Errorf("dc1 (creator) at party = %q, want rwcda", got.String())
	}
	// dc2 (non-creator) ALSO has rwcda via the role grant. This is
	// the whole point of auto_own_roles — peer DCs share authority
	// without admin status.
	if got := EffectiveVerbs(chain, "dc2@example.com"); got.String() != "rwcda" {
		t.Errorf("dc2 (peer) at party = %q, want rwcda (role grant from auto_own_roles)", got.String())
	}
}

// TestStandardRoles_ProjectTeamReadOnlyExceptOwned — project_team gets
// r across the project, but the per-user working home's auto-own .zddc
// (rwcda for the creator) wins via deepest-match, so a team member has
// full rights in their own home and read-only elsewhere.
func TestStandardRoles_ProjectTeamReadOnlyExceptOwned(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `roles:
  project_team:
    members: ["*@example.com"]
`)
	// Simulate the auto-own .zddc the file API would write at
	// archive/Acme/working/alice@example.com/ (fenced via
	// acl.inherit:false, creator-owned).
	homeDir := filepath.Join(root, "Proj", "archive", "Acme", "working", "alice@example.com")
	if err := os.MkdirAll(homeDir, 0o755); err != nil {
		t.Fatal(err)
	}
	writeZddc(t, homeDir, `acl:
  inherit: false
  permissions:
    "alice@example.com": rwcda
created_by: alice@example.com
`)
	resetCache()

	alice := "alice@example.com"
	bob := "bob@example.com"

	// Alice (team member) inside her own home → rwcda.
	chain, _ := EffectivePolicy(root, homeDir)
	if got := EffectiveVerbs(chain, alice); got.String() != "rwcda" {
		t.Errorf("alice in own home = %q, want rwcda", got.String())
	}
	// Bob (team member) inside Alice's fenced home → nothing (fence
	// blocks the project-level project_team:r; bob isn't named in the
	// fenced .zddc).
	if got := EffectiveVerbs(chain, bob); got != 0 {
		t.Errorf("bob in alice's fenced home = %q, want empty (fence blocks inherited grants)", got.String())
	}
	// Alice elsewhere in the project (not her home, not WORM) → r.
	chain2, _ := EffectivePolicy(root, filepath.Join(root, "Proj", "archive", "Acme"))
	if got := EffectiveVerbs(chain2, alice); got.String() != "r" {
		t.Errorf("alice in archive/Acme = %q, want r", got.String())
	}
	// Alice CANNOT write to incoming/ — that's the counterparty's drop
	// zone, QC'd by the document controller. project_team gets read only.
	chain3, _ := EffectivePolicy(root, filepath.Join(root, "Proj", "archive", "Acme", "incoming"))
	if got := EffectiveVerbs(chain3, alice); got.String() != "r" {
		t.Errorf("alice in incoming/ = %q, want r (no create/write for project_team)", got.String())
	}
}

// TestStandardRoles_ProjectTeamInFlightRatchet — the one-way handoff
// from working/ → staging/ → issued/ as the team member sees it:
// full work in working (cr at the slot + rwcda inside the fenced
// home), drop-only in staging (cr — no modify after the drop), drop
// inside auto-own iteration folder in reviewing (cr at the slot,
// rwcda inside the auto-owned sub-folder), read-only in received/
// issued (WORM zones) and incoming/ (counterparty drop zone).
func TestStandardRoles_ProjectTeamInFlightRatchet(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `roles:
  project_team:
    members: ["*@example.com"]
`)
	alice := "alice@example.com"

	mustVerbs := func(dir string, want string) {
		t.Helper()
		chain, err := EffectivePolicy(root, dir)
		if err != nil {
			t.Fatalf("EffectivePolicy(%q): %v", dir, err)
		}
		var got VerbSet
		if g, inWorm := WormZoneGrant(chain, alice); inWorm {
			got = (EffectiveVerbs(chain, alice) & VerbR) | (g & VerbsRC)
		} else {
			got = EffectiveVerbs(chain, alice)
		}
		if got.String() != want {
			t.Errorf("project_team alice at %s = %q, want %q", dir[len(root):], got.String(), want)
		}
	}

	j := func(p ...string) string { return filepath.Join(append([]string{root, "Proj"}, p...)...) }
	mustVerbs(j("working", "Acme"), "rc")               // create + read at the workspace
	mustVerbs(j("staging", "Acme"), "rc")               // drop + read, no modify
	mustVerbs(j("reviewing", "Acme"), "rc")             // create iteration folders
	mustVerbs(j("archive", "Acme", "received"), "r")    // WORM — read pass-through
	mustVerbs(j("archive", "Acme", "issued"), "r")      // WORM — same
	mustVerbs(j("incoming", "Acme"), "r")               // counterparty drop zone — read only
}

// TestStandardRoles_DocControllerStagingDelete — DC needs `d` at
// staging/ to perform the staging-to-issued transfer (cut, not copy);
// the explicit document_controller: rwcd grant supplies it. Mirrors
// the incoming/ pattern (line 286-288 of internal/zddc/defaults/) where
// the QC + transfer-out workflow needed the same.
func TestStandardRoles_DocControllerStagingDelete(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `roles:
  document_controller:
    members: ["dc@example.com"]
`)
	dc := "dc@example.com"
	chain, err := EffectivePolicy(root, filepath.Join(root, "Proj", "staging", "Acme"))
	if err != nil {
		t.Fatalf("EffectivePolicy: %v", err)
	}
	got := EffectiveVerbs(chain, dc)
	for _, v := range []VerbSet{VerbR, VerbW, VerbC, VerbD} {
		if !got.Has(v) {
			t.Errorf("doc controller at staging/ missing verb %q in %q (need rwcd for transfer-to-issued)", v.String(), got.String())
		}
	}
}

// TestStandardRoles_ObserverReadOnlyEverywhere — observer is the
// project-wide read-only role for auditors / regulators / external
// viewers. Unlike project_team, an observer must not contribute
// content anywhere: no create at archive/, no create at working/,
// no worm-create at received/issued, and not subtree-admin of
// anything. Read passes through WORM zones (worm: lists strip w/d/a
// but never r).
func TestStandardRoles_ObserverReadOnlyEverywhere(t *testing.T) {
	resetCache()
	root := t.TempDir()
	writeZddc(t, root, `roles:
  observer:
    members: ["auditor@example.com"]
`)
	obs := "auditor@example.com"

	mustVerbs := func(dir string, want string) {
		t.Helper()
		chain, err := EffectivePolicy(root, dir)
		if err != nil {
			t.Fatalf("EffectivePolicy(%q): %v", dir, err)
		}
		// Mirror InternalDecider.Allow's WORM-aware composition so the
		// assertion covers received/issued correctly.
		var got VerbSet
		if g, inWorm := WormZoneGrant(chain, obs); inWorm {
			got = (EffectiveVerbs(chain, obs) & VerbR) | (g & VerbsRC)
		} else {
			got = EffectiveVerbs(chain, obs)
		}
		if got.String() != want {
			t.Errorf("observer verbs at %s = %q, want %q", dir[len(root):], got.String(), want)
		}
	}

	// Project level: read-only.
	mustVerbs(filepath.Join(root, "Proj"), "r")
	// A random subfolder under the project still read-only.
	mustVerbs(filepath.Join(root, "Proj", "random-folder"), "r")
	// archive/ — read-only (no create at the party-folder level).
	mustVerbs(filepath.Join(root, "Proj", "archive"), "r")
	// incoming/ — read-only (no create even though incoming/ has
	// drop_target and auto_own; the cascade ACL still gates create).
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "incoming"), "r")
	// In-flight lifecycle slots — read-only.
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "working"), "r")
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "staging"), "r")
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "reviewing"), "r")
	// WORM zones — read passes through; no worm-create (observer is
	// not in the worm: list).
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "received"), "r")
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "issued"), "r")

	// Observer is not subtree-admin of anything in the project — even
	// when notionally elevated, the role carries no admin grant.
	if IsSubtreeAdmin(root, filepath.Join(root, "Proj"), Principal{Email: obs, Elevated: true}) {
		t.Errorf("observer should NOT be subtree-admin of the project root")
	}
	if IsSubtreeAdmin(root, filepath.Join(root, "Proj", "archive"), Principal{Email: obs, Elevated: true}) {
		t.Errorf("observer should NOT be subtree-admin of archive/")
	}
	if IsSubtreeAdmin(root, filepath.Join(root, "Proj", "archive", "Acme"), Principal{Email: obs, Elevated: true}) {
		t.Errorf("observer should NOT be subtree-admin of archive/<party>/")
	}
}
