package zddc

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

// TestStandardRoles_DocControllerScopedCreate — with document_controller
// populated at the on-disk root, the role gets:
//   - rw at the project level (read + overwrite-existing), but NOT c
//     (so it can't make arbitrary folders)
//   - rwc at archive/ (can create party subfolders)
//   - subtree-admin at working/ and staging/ (full create + manage)
//   - inside received/issued (WORM): masked to r + worm-restored c
func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
	resetCache()
	root := t.TempDir()
	// Deployment populates the standard roles. Roles UNION with the
	// embedded (empty) definitions, so this is the effective member set.
	writeZddc(t, root, `roles:
  document_controller:
    members: ["dc@example.com"]
  project_team:
    members: ["*@example.com"]
`)
	dc := "dc@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.
		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(filepath.Join(root, "Proj"), "rw")
	// A random subfolder under the project inherits rw (no c).
	mustVerbs(filepath.Join(root, "Proj", "random-folder"), "rw")
	// archive/: rwc (can create party folders).
	mustVerbs(filepath.Join(root, "Proj", "archive"), "rwc")
	// incoming/: rwcd — the QC + transfer-out workflow needs delete.
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "incoming"), "rwcd")
	// received/ (WORM): rw masked to r, plus worm-restored c → "rc".
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "received"), "rc")
	mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "issued"), "rc")

	// Subtree-admin at working/ and staging/ (via admins: [document_controller]
	// in the embedded cascade — role-aware now).
	if !IsSubtreeAdmin(root, filepath.Join(root, "Proj", "working"), Principal{Email: dc, Elevated: true}) {
		t.Errorf("doc controller should be subtree-admin of working/")
	}
	if !IsSubtreeAdmin(root, filepath.Join(root, "Proj", "staging"), Principal{Email: dc, Elevated: true}) {
		t.Errorf("doc controller should be subtree-admin of staging/")
	}
	// NOT subtree-admin of archive/ (so WORM still binds them there).
	if IsSubtreeAdmin(root, filepath.Join(root, "Proj", "archive"), Principal{Email: dc, Elevated: true}) {
		t.Errorf("doc controller should NOT be subtree-admin of archive/")
	}
	// Subtree-admin reaches inside a fenced per-user working home.
	if !IsSubtreeAdmin(root, filepath.Join(root, "Proj", "working", "alice@example.com"), Principal{Email: dc, Elevated: true}) {
		t.Errorf("doc controller (subtree-admin of working/) should reach inside a fenced user home")
	}
}

// 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
	// working/alice@example.com/ (fenced via acl.inherit:false,
	// creator-owned).
	homeDir := filepath.Join(root, "Proj", "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())
	}
}
