"""
Tests for Phase 46: Role-Based Access Control (RBAC).

Covers: role creation, built-in roles, permission resolution with inheritance,
role assignment/removal, permission checks, audit log, organization support,
edge cases, and permission groups.
"""

import json
import time
import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all RBAC storage to tmp_path so tests are fully isolated."""
    rbac = tmp_path / "rbac"
    assignments = rbac / "assignments"
    orgs = rbac / "orgs"
    for d in (rbac, assignments, orgs):
        d.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.access_control as ac
    monkeypatch.setattr(ac, "RBAC_DIR", rbac)
    monkeypatch.setattr(ac, "ROLES_FILE", rbac / "roles.json")
    monkeypatch.setattr(ac, "ASSIGNMENTS_DIR", assignments)
    monkeypatch.setattr(ac, "ORGS_DIR", orgs)
    monkeypatch.setattr(ac, "ACCESS_LOG_FILE", rbac / "access_log.jsonl")


# ── 1. Built-in Roles ─────────────────────────────────────────────────────


class TestBuiltinRoles:
    def test_viewer_role_exists(self):
        from brynq.access_control import get_role
        role = get_role("viewer")
        assert role is not None
        assert role["builtin"] is True
        assert "task.view" in role["permissions"]
        assert "channel.read" in role["permissions"]
        assert "agent.view" in role["permissions"]

    def test_member_inherits_viewer(self):
        from brynq.access_control import get_role
        role = get_role("member")
        assert role["inherits"] == "viewer"
        assert "task.create" in role["permissions"]

    def test_moderator_inherits_member(self):
        from brynq.access_control import get_role
        role = get_role("moderator")
        assert role["inherits"] == "member"
        assert "agent.manage" in role["permissions"]

    def test_admin_has_wildcard(self):
        from brynq.access_control import get_role
        role = get_role("admin")
        assert "*" in role["permissions"]

    def test_owner_inherits_admin(self):
        from brynq.access_control import get_role
        role = get_role("owner")
        assert role["inherits"] == "admin"
        assert "billing.manage" in role["permissions"]
        assert "role.manage" in role["permissions"]
        assert "platform.configure" in role["permissions"]


# ── 2. Role Creation ──────────────────────────────────────────────────────


class TestRoleCreation:
    def test_create_custom_role(self):
        from brynq.access_control import create_role, get_role
        result = create_role("tester", ["task.view", "task.create"], description="QA role")
        assert result["name"] == "tester"
        assert result["builtin"] is False
        assert "task.view" in result["permissions"]
        # Persisted
        loaded = get_role("tester")
        assert loaded is not None
        assert loaded["name"] == "tester"

    def test_create_role_with_inheritance(self):
        from brynq.access_control import create_role, get_role_permissions
        create_role("senior", ["task.assign"], inherits="member")
        perms = get_role_permissions("senior")
        # Should include own + member + viewer permissions
        assert "task.assign" in perms
        assert "task.create" in perms  # from member
        assert "task.view" in perms    # from viewer via member

    def test_create_role_invalid_name(self):
        from brynq.access_control import create_role
        result = create_role("", ["task.view"])
        assert "error" in result

    def test_create_role_invalid_permissions_type(self):
        from brynq.access_control import create_role
        result = create_role("bad", "task.view")
        assert "error" in result

    def test_create_role_invalid_inherits(self):
        from brynq.access_control import create_role
        result = create_role("orphan", ["task.view"], inherits="nonexistent")
        assert "error" in result

    def test_delete_custom_role(self):
        from brynq.access_control import create_role, delete_role, get_role
        create_role("temp_role", ["task.view"])
        assert get_role("temp_role") is not None
        result = delete_role("temp_role")
        assert result == {"deleted": "temp_role"}
        assert get_role("temp_role") is None

    def test_cannot_delete_builtin_role(self):
        from brynq.access_control import delete_role
        result = delete_role("admin")
        assert "error" in result
        assert "built-in" in result["error"]

    def test_delete_nonexistent_role(self):
        from brynq.access_control import delete_role
        result = delete_role("ghost")
        assert "error" in result

    def test_list_roles_includes_builtins(self):
        from brynq.access_control import list_roles
        roles = list_roles()
        names = {r["name"] for r in roles}
        assert {"viewer", "member", "moderator", "admin", "owner"} <= names


# ── 3. Permission Resolution ──────────────────────────────────────────────


class TestPermissionResolution:
    def test_viewer_permissions(self):
        from brynq.access_control import get_role_permissions
        perms = get_role_permissions("viewer")
        assert set(perms) == {"task.view", "channel.read", "agent.view"}

    def test_member_resolves_full_chain(self):
        from brynq.access_control import get_role_permissions
        perms = get_role_permissions("member")
        expected = {"task.view", "channel.read", "agent.view",
                    "task.create", "task.accept", "channel.post", "dm.send"}
        assert set(perms) == expected

    def test_admin_resolves_to_all_permissions(self):
        from brynq.access_control import get_role_permissions, ALL_PERMISSIONS
        perms = get_role_permissions("admin")
        assert set(perms) >= ALL_PERMISSIONS

    def test_owner_resolves_superset_of_admin(self):
        from brynq.access_control import get_role_permissions
        admin_perms = set(get_role_permissions("admin"))
        owner_perms = set(get_role_permissions("owner"))
        assert owner_perms >= admin_perms

    def test_nonexistent_role_returns_empty(self):
        from brynq.access_control import get_role_permissions
        perms = get_role_permissions("nonexistent_role")
        assert perms == []


# ── 4. Role Assignment ────────────────────────────────────────────────────


class TestRoleAssignment:
    def test_assign_role(self):
        from brynq.access_control import assign_role, get_roles
        result = assign_role("agent-1", "viewer")
        assert "agent-1" in result["entity_id"]
        assert "viewer" in result["roles"]
        assert get_roles("agent-1") == ["viewer"]

    def test_assign_multiple_roles(self):
        from brynq.access_control import assign_role, get_roles
        assign_role("agent-2", "viewer")
        assign_role("agent-2", "moderator")
        roles = get_roles("agent-2")
        assert "viewer" in roles
        assert "moderator" in roles

    def test_assign_duplicate_is_idempotent(self):
        from brynq.access_control import assign_role, get_roles
        assign_role("agent-3", "member")
        assign_role("agent-3", "member")
        assert get_roles("agent-3").count("member") == 1

    def test_assign_nonexistent_role_fails(self):
        from brynq.access_control import assign_role
        result = assign_role("agent-4", "superadmin")
        assert "error" in result

    def test_assign_preserves_entity_type(self):
        from brynq.access_control import assign_role
        result = assign_role("user-1", "admin", entity_type="user")
        assert result["entity_type"] == "user"

    def test_remove_role(self):
        from brynq.access_control import assign_role, remove_role, get_roles
        assign_role("agent-5", "viewer")
        assign_role("agent-5", "member")
        remove_role("agent-5", "viewer")
        assert get_roles("agent-5") == ["member"]

    def test_remove_role_not_assigned(self):
        from brynq.access_control import assign_role, remove_role
        assign_role("agent-6", "viewer")
        result = remove_role("agent-6", "admin")
        assert "error" in result

    def test_get_roles_empty_entity(self):
        from brynq.access_control import get_roles
        assert get_roles("unknown-entity") == []


# ── 5. Permission Checking ────────────────────────────────────────────────


class TestPermissionChecking:
    def test_has_permission_true(self):
        from brynq.access_control import assign_role, has_permission
        assign_role("agent-10", "member")
        assert has_permission("agent-10", "task.create") is True
        assert has_permission("agent-10", "task.view") is True  # inherited from viewer

    def test_has_permission_false(self):
        from brynq.access_control import assign_role, has_permission
        assign_role("agent-11", "viewer")
        assert has_permission("agent-11", "task.create") is False
        assert has_permission("agent-11", "billing.manage") is False

    def test_admin_has_all_permissions(self):
        from brynq.access_control import assign_role, has_permission
        assign_role("agent-12", "admin")
        assert has_permission("agent-12", "task.create") is True
        assert has_permission("agent-12", "billing.manage") is True
        assert has_permission("agent-12", "platform.configure") is True
        assert has_permission("agent-12", "anything.at.all") is True  # wildcard

    def test_check_permission_returns_tuple(self):
        from brynq.access_control import assign_role, check_permission
        assign_role("agent-13", "viewer")
        allowed, reason = check_permission("agent-13", "task.view")
        assert allowed is True
        assert "granted" in reason

    def test_check_permission_denied_with_reason(self):
        from brynq.access_control import assign_role, check_permission
        assign_role("agent-14", "viewer")
        allowed, reason = check_permission("agent-14", "billing.manage")
        assert allowed is False
        assert "viewer" in reason

    def test_check_permission_no_roles(self):
        from brynq.access_control import check_permission
        allowed, reason = check_permission("nobody", "task.view")
        assert allowed is False
        assert "no assigned roles" in reason

    def test_has_permission_no_roles(self):
        from brynq.access_control import has_permission
        assert has_permission("ghost-agent", "task.view") is False


# ── 6. Audit Log ──────────────────────────────────────────────────────────


class TestAuditLog:
    def test_check_permission_logs_entry(self):
        from brynq.access_control import assign_role, check_permission, get_access_log
        assign_role("agent-20", "viewer")
        check_permission("agent-20", "task.view")
        log = get_access_log(entity_id="agent-20")
        assert len(log) >= 1
        entry = log[0]
        assert entry["entity_id"] == "agent-20"
        assert entry["permission"] == "task.view"
        assert entry["allowed"] is True

    def test_log_records_denial(self):
        from brynq.access_control import assign_role, check_permission, get_access_log
        assign_role("agent-21", "viewer")
        check_permission("agent-21", "billing.manage")
        log = get_access_log(entity_id="agent-21")
        denied = [e for e in log if not e["allowed"]]
        assert len(denied) >= 1

    def test_log_filter_by_permission(self):
        from brynq.access_control import assign_role, check_permission, get_access_log
        assign_role("agent-22", "member")
        check_permission("agent-22", "task.view")
        check_permission("agent-22", "task.create")
        check_permission("agent-22", "billing.manage")
        log = get_access_log(permission="task.create")
        assert all(e["permission"] == "task.create" for e in log)
        assert len(log) >= 1

    def test_log_limit(self):
        from brynq.access_control import assign_role, check_permission, get_access_log
        assign_role("agent-23", "admin")
        for _ in range(10):
            check_permission("agent-23", "task.view")
        log = get_access_log(entity_id="agent-23", limit=5)
        assert len(log) == 5

    def test_log_most_recent_first(self):
        from brynq.access_control import assign_role, check_permission, get_access_log
        assign_role("agent-24", "viewer")
        check_permission("agent-24", "task.view")
        check_permission("agent-24", "channel.read")
        log = get_access_log(entity_id="agent-24")
        assert log[0]["permission"] == "channel.read"
        assert log[1]["permission"] == "task.view"


# ── 7. Organization Support ───────────────────────────────────────────────


class TestOrganizationSupport:
    def test_create_org(self):
        from brynq.access_control import create_org
        org = create_org("Acme Corp", "owner-1")
        assert org["name"] == "Acme Corp"
        assert org["owner_id"] == "owner-1"
        assert len(org["members"]) == 1
        assert org["members"][0]["entity_id"] == "owner-1"
        assert org["members"][0]["role"] == "owner"

    def test_create_org_invalid_name(self):
        from brynq.access_control import create_org
        result = create_org("", "owner-x")
        assert "error" in result

    def test_add_member(self):
        from brynq.access_control import create_org, add_member, list_members
        org = create_org("Team A", "owner-2")
        add_member(org["org_id"], "agent-30", role="member")
        members = list_members(org["org_id"])
        assert len(members) == 2
        ids = {m["entity_id"] for m in members}
        assert "agent-30" in ids

    def test_add_member_updates_role_if_exists(self):
        from brynq.access_control import create_org, add_member, list_members
        org = create_org("Team B", "owner-3")
        add_member(org["org_id"], "agent-31", role="viewer")
        add_member(org["org_id"], "agent-31", role="moderator")
        members = list_members(org["org_id"])
        agent_member = [m for m in members if m["entity_id"] == "agent-31"][0]
        assert agent_member["role"] == "moderator"

    def test_add_member_nonexistent_org(self):
        from brynq.access_control import add_member
        result = add_member("fake-org", "agent-32")
        assert "error" in result

    def test_add_member_nonexistent_role(self):
        from brynq.access_control import create_org, add_member
        org = create_org("Team C", "owner-4")
        result = add_member(org["org_id"], "agent-33", role="superadmin")
        assert "error" in result

    def test_remove_member(self):
        from brynq.access_control import create_org, add_member, remove_member, list_members
        org = create_org("Team D", "owner-5")
        add_member(org["org_id"], "agent-34")
        remove_member(org["org_id"], "agent-34")
        members = list_members(org["org_id"])
        assert len(members) == 1
        assert members[0]["entity_id"] == "owner-5"

    def test_cannot_remove_owner(self):
        from brynq.access_control import create_org, remove_member
        org = create_org("Team E", "owner-6")
        result = remove_member(org["org_id"], "owner-6")
        assert "error" in result
        assert "owner" in result["error"].lower()

    def test_remove_nonmember(self):
        from brynq.access_control import create_org, remove_member
        org = create_org("Team F", "owner-7")
        result = remove_member(org["org_id"], "ghost-agent")
        assert "error" in result

    def test_remove_member_nonexistent_org(self):
        from brynq.access_control import remove_member
        result = remove_member("fake-org", "agent-x")
        assert "error" in result

    def test_list_members_nonexistent_org(self):
        from brynq.access_control import list_members
        assert list_members("no-such-org") == []

    def test_org_membership_grants_permissions(self):
        from brynq.access_control import create_org, add_member, has_permission
        org = create_org("Team G", "owner-8")
        add_member(org["org_id"], "agent-35", role="moderator")
        # Moderator inherits member inherits viewer, so should have task.view
        assert has_permission("agent-35", "task.view") is True
        assert has_permission("agent-35", "agent.manage") is True

    def test_get_org(self):
        from brynq.access_control import create_org, get_org
        org = create_org("Team H", "owner-9")
        loaded = get_org(org["org_id"])
        assert loaded is not None
        assert loaded["name"] == "Team H"

    def test_get_org_nonexistent(self):
        from brynq.access_control import get_org
        assert get_org("nonexistent") is None


# ── 8. Permission Groups ──────────────────────────────────────────────────


class TestPermissionGroups:
    def test_list_permissions_returns_all_groups(self):
        from brynq.access_control import list_permissions
        groups = list_permissions()
        assert "task" in groups
        assert "channel" in groups
        assert "agent" in groups
        assert "billing" in groups
        assert "platform" in groups
        assert "role" in groups
        assert "dm" in groups

    def test_list_permissions_values_are_lists(self):
        from brynq.access_control import list_permissions
        groups = list_permissions()
        for category, perms in groups.items():
            assert isinstance(perms, list)
            assert all(isinstance(p, str) for p in perms)
            assert all(p.startswith(category + ".") for p in perms)


# ── 9. Edge Cases ─────────────────────────────────────────────────────────


class TestEdgeCases:
    def test_circular_inheritance_does_not_crash(self):
        from brynq.access_control import create_role, get_role_permissions
        # Create B first (no inheritance), then A -> B, then update B -> A
        create_role("role_b", ["channel.read"])
        create_role("role_a", ["task.view"], inherits="role_b")
        # Now update role_b to inherit role_a (creating a cycle)
        create_role("role_b", ["channel.read"], inherits="role_a")
        perms = get_role_permissions("role_a")
        # Should resolve without infinite recursion, collecting what it can
        assert "task.view" in perms
        assert "channel.read" in perms

    def test_combined_direct_and_org_permissions(self):
        from brynq.access_control import (
            assign_role, create_org, add_member, has_permission,
        )
        assign_role("agent-40", "viewer")  # direct: task.view, channel.read, agent.view
        org = create_org("Org Z", "some-owner")
        add_member(org["org_id"], "agent-40", role="moderator")  # org: moderator chain
        # Should have both direct and org-inherited permissions
        assert has_permission("agent-40", "task.view") is True  # direct
        assert has_permission("agent-40", "agent.manage") is True  # from org moderator

    def test_custom_role_overwrite(self):
        from brynq.access_control import create_role, get_role
        create_role("evolving", ["task.view"])
        create_role("evolving", ["task.view", "task.create", "billing.view"])
        role = get_role("evolving")
        assert "billing.view" in role["permissions"]

    def test_access_log_empty_when_no_checks(self):
        from brynq.access_control import get_access_log
        log = get_access_log()
        assert log == []
