Feature: Permission system — role-based access control
  As a CleverAgents operator
  I want a namespace/project/plan/skill permission model
  So that access to resources is controlled by role bindings with default deny

  # ── Role enum values ─────────────────────────────────────────────

  Scenario: PermissionRole enum contains the expected members
    Given the PermissionRole enum
    Then the enum should contain "owner"
    And the enum should contain "admin"
    And the enum should contain "editor"
    And the enum should contain "viewer"

  Scenario: PermissionScope enum contains the expected members
    Given the PermissionScope enum
    Then the enum should contain "namespace"
    And the enum should contain "project"
    And the enum should contain "plan"
    And the enum should contain "skill"

  Scenario: PermissionAction enum contains the expected members
    Given the PermissionAction enum
    Then the enum should contain "read"
    And the enum should contain "write"
    And the enum should contain "execute"
    And the enum should contain "delete"
    And the enum should contain "admin"

  # ── ROLE_PERMISSIONS constant ────────────────────────────────────

  Scenario: OWNER role has all actions
    Given the ROLE_PERMISSIONS constant
    Then the "owner" role should have 5 allowed actions

  Scenario: ADMIN role has all actions
    Given the ROLE_PERMISSIONS constant
    Then the "admin" role should have 5 allowed actions

  Scenario: EDITOR role has read, write, and execute
    Given the ROLE_PERMISSIONS constant
    Then the "editor" role should have 3 allowed actions

  Scenario: VIEWER role has only read
    Given the ROLE_PERMISSIONS constant
    Then the "viewer" role should have 1 allowed actions

  # ── RoleBinding model ────────────────────────────────────────────

  Scenario: Create a valid RoleBinding
    Given a role binding for principal "alice" with role "editor" on "project" scope "proj-1"
    Then the binding principal should be "alice"
    And the binding role should be "editor"
    And the binding scope should be "project"
    And the binding scope_id should be "proj-1"

  # ── PermissionPolicy model ──────────────────────────────────────

  Scenario: Default policy has default_deny enabled
    Given a default PermissionPolicy
    Then default_deny should be true
    And allow_overrides should be empty

  Scenario: Policy with allow overrides
    Given a PermissionPolicy with an override for "bob" as "admin" on "namespace" scope "ns-1"
    Then the policy should have 1 allow override

  # ── Local mode permission check ─────────────────────────────────

  Scenario: Local mode always permits actions
    Given a PermissionService in local mode
    When I check permission for "alice" to "write" on "project" scope "proj-1"
    Then the permission check result should be true
    And the reason should contain "local mode"

  Scenario: Local mode permits admin action
    Given a PermissionService in local mode
    When I check permission for "alice" to "admin" on "namespace" scope "ns-1"
    Then the permission check result should be true

  Scenario: Local mode permits delete action
    Given a PermissionService in local mode
    When I check permission for "bob" to "delete" on "plan" scope "plan-42"
    Then the permission check result should be true

  # ── Server mode — default deny ──────────────────────────────────

  Scenario: Server mode denies action when no binding exists
    Given a PermissionService in server mode with no bindings
    When I check permission for "alice" to "write" on "project" scope "proj-1"
    Then the permission check result should be false
    And the reason should contain "denied by default-deny"

  Scenario: Server mode denies action for missing role
    Given a PermissionService in server mode with no bindings
    When I check permission for "unknown-user" to "read" on "skill" scope "sk-99"
    Then the permission check result should be false

  # ── Server mode — role binding grants ───────────────────────────

  Scenario: Server mode allows action when binding grants the role
    Given a PermissionService in server mode
    And a role binding for principal "alice" with role "editor" on "project" scope "proj-1"
    When I check permission for "alice" to "read" on "project" scope "proj-1"
    Then the permission check result should be true
    And the reason should contain "allowed by role"

  Scenario: Server mode denies action not in role permissions
    Given a PermissionService in server mode
    And a role binding for principal "alice" with role "viewer" on "project" scope "proj-1"
    When I check permission for "alice" to "write" on "project" scope "proj-1"
    Then the permission check result should be false

  # ── Server mode — allow overrides ───────────────────────────────

  Scenario: Allow override grants access despite no regular binding
    Given a PermissionService in server mode with no bindings
    And a policy override for "bob" as "admin" on "namespace" scope "ns-1"
    When I check permission for "bob" to "admin" on "namespace" scope "ns-1"
    Then the permission check result should be true
    And the reason should contain "override"

  # ── Default local role mapping ──────────────────────────────────

  Scenario: DEFAULT_LOCAL_ROLE_MAPPING grants OWNER to wildcard
    Given the DEFAULT_LOCAL_ROLE_MAPPING constant
    Then the wildcard entry should map to "owner"

  # ── PermissionCheck model ───────────────────────────────────────

  Scenario: PermissionCheck captures all fields
    Given a PermissionCheck for "carol" action "execute" scope "plan" scope_id "plan-7" result true reason "ok"
    Then the check principal should be "carol"
    And the check action should be "execute"
    And the check scope should be "plan"
    And the check scope_id should be "plan-7"
    And the check result should be true
    And the check reason should be "ok"

  # ── enforce_permission decorator ────────────────────────────────

  Scenario: enforce_permission decorator allows in local mode
    Given a function decorated with enforce_permission for "write" on "project" scope "proj-1"
    When I call the decorated function in local mode
    Then it should succeed without raising

  Scenario: enforce_permission decorator raises in server mode when denied
    Given a function decorated with enforce_permission for "delete" on "namespace" scope "ns-1"
    When I call the decorated function in server mode with no bindings
    Then it should raise a PermissionError

  # ── Binding management ──────────────────────────────────────────

  Scenario: Add and remove bindings on the service
    Given a PermissionService in server mode with no bindings
    And I add a binding for "dave" as "editor" on "skill" scope "sk-1"
    When I check permission for "dave" to "write" on "skill" scope "sk-1"
    Then the permission check result should be true
    When I remove the binding for "dave" as "editor" on "skill" scope "sk-1"
    And I check permission for "dave" to "write" on "skill" scope "sk-1"
    Then the permission check result should be false

  Scenario: Remove non-existent binding returns false
    Given a PermissionService in server mode with no bindings
    When I try to remove a binding for "ghost" as "viewer" on "plan" scope "p-0"
    Then the remove result should be false

  # ── get_role_bindings ───────────────────────────────────────────

  Scenario: get_role_bindings in local mode returns synthetic OWNER binding
    Given a PermissionService in local mode
    When I get role bindings for "anyone" on "project" scope "proj-1"
    Then I should receive 1 binding
    And the first binding role should be "owner"

  Scenario: get_role_bindings in server mode returns matching bindings
    Given a PermissionService in server mode with no bindings
    And I add a binding for "eve" as "viewer" on "project" scope "proj-2"
    And I add a binding for "eve" as "editor" on "project" scope "proj-2"
    When I get role bindings for "eve" on "project" scope "proj-2"
    Then I should receive 2 bindings

  # ── Policy default_deny=False ───────────────────────────────────

  Scenario: Server mode with default_deny disabled permits unbound action
    Given a PermissionService in server mode with default_deny disabled
    When I check permission for "frank" to "read" on "namespace" scope "ns-9"
    Then the permission check result should be true
    And the reason should contain "default-deny disabled"

  # ── Scope mismatch ──────────────────────────────────────────────

  Scenario: Binding on project scope does not grant access at namespace scope
    Given a PermissionService in server mode
    And a role binding for principal "alice" with role "editor" on "project" scope "proj-1"
    When I check permission for "alice" to "read" on "namespace" scope "proj-1"
    Then the permission check result should be false

  Scenario: Binding on skill scope does not grant access at plan scope
    Given a PermissionService in server mode
    And a role binding for principal "bob" with role "admin" on "skill" scope "sk-1"
    When I check permission for "bob" to "admin" on "plan" scope "sk-1"
    Then the permission check result should be false

  # ── Duplicate binding prevention ────────────────────────────────

  Scenario: Adding a duplicate binding is rejected
    Given a PermissionService in server mode with no bindings
    And I add a binding for "dave" as "editor" on "project" scope "proj-1"
    When I try to add a duplicate binding for "dave" as "editor" on "project" scope "proj-1"
    Then the add result should be false

  # ── RoleBinding validation ──────────────────────────────────────

  Scenario: RoleBinding rejects empty principal
    When I try to create a role binding with empty principal
    Then a permission validation error should be raised

  Scenario: RoleBinding rejects empty scope_id
    When I try to create a role binding with empty scope_id
    Then a permission validation error should be raised

  # ── PermissionCheck with result=False ───────────────────────────

  Scenario: PermissionCheck accepts result false
    Given a PermissionCheck for "denied-user" action "write" scope "project" scope_id "proj-1" result false reason "denied"
    Then the check result should be false
    And the check reason should be "denied"

  # ── enforce_permission with pre-loaded service ──────────────────

  Scenario: enforce_permission decorator uses pre-loaded service with bindings
    Given a pre-loaded permission service with "alice" as "editor" on "project" scope "proj-1" for action "write"
    When I call the pre-loaded decorated function in server mode
    Then it should succeed without raising

  Scenario: enforce_permission decorator uses module-level default service
    Given a module-level default service with "bob" as "admin" on "namespace" scope "ns-1"
    And a function decorated with enforce_permission for "admin" on "namespace" scope "ns-1"
    When I call the decorated function in server mode via module default
    Then it should succeed without raising

  # ── Input validation — check_permission ─────────────────────────

  Scenario: check_permission rejects empty principal
    Given a PermissionService in local mode
    When I check permission with empty principal
    Then a permission ValueError should mention "principal"

  Scenario: check_permission rejects whitespace-only principal
    Given a PermissionService in local mode
    When I check permission with whitespace-only principal
    Then a permission ValueError should mention "principal"

  Scenario: check_permission rejects empty scope_id
    Given a PermissionService in local mode
    When I check permission with empty scope_id
    Then a permission ValueError should mention "scope_id"

  Scenario: check_permission rejects whitespace-only scope_id
    Given a PermissionService in local mode
    When I check permission with whitespace-only scope_id
    Then a permission ValueError should mention "scope_id"

  Scenario: check_permission strips leading/trailing whitespace from principal
    Given a PermissionService in local mode
    When I check permission for "  alice  " to "read" on "project" scope "proj-1"
    Then the permission check result should be true

  Scenario: check_permission strips leading/trailing whitespace from scope_id
    Given a PermissionService in local mode
    When I check permission for "alice" to "read" on "project" scope "  proj-1  "
    Then the permission check result should be true

  # ── Input validation — get_role_bindings ────────────────────────

  Scenario: get_role_bindings rejects empty principal
    Given a PermissionService in local mode
    When I get role bindings with empty principal
    Then a permission ValueError should mention "principal"

  Scenario: get_role_bindings rejects whitespace-only scope_id
    Given a PermissionService in local mode
    When I get role bindings with whitespace-only scope_id
    Then a permission ValueError should mention "scope_id"
