"""RBAC: seed command, scope helper, object-level permission, role enforcement."""
import pytest
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient

from apps.companies.models import Organization, OrganizationMembership
from apps.core.models import Permission, Role, UserRole
from apps.core.permissions import IsOwnerOrFullScope, user_has_full_scope

User = get_user_model()


def _user(email):
    return User.objects.create_user(email=email, password="pass123")


def _org():
    return Organization.objects.create(name="RBAC Unit Test Org")


def _assign(user, *codenames, organization=None):
    """Give `user` a role carrying exactly the given `domain.action` perms,
    within `organization` (creates one and joins the user if not given)."""
    if organization is None:
        organization = _org()
    OrganizationMembership.objects.get_or_create(organization=organization, user=user)
    role = Role.objects.create(name=f"r-{user.email}", slug=f"r-{user.pk}")
    for c in codenames:
        d, a = c.split(".", 1)
        perm, _ = Permission.objects.get_or_create(domain=d, action=a, defaults={"label": c})
        role.permissions.add(perm)
    UserRole.objects.create(user=user, role=role, organization=organization)
    return user


# ---- seed_rbac command ----

@pytest.mark.django_db
def test_seed_rbac_idempotent():
    from django.core.management import call_command
    call_command("seed_rbac")
    perms_after_first = Permission.objects.count()
    roles = {r.slug: r.permissions.count() for r in Role.objects.filter(
        slug__in=["manager", "staff", "visitor"])}
    assert set(roles) == {"manager", "staff", "visitor"}
    assert roles["manager"] > roles["staff"] > roles["visitor"]
    # Re-run: no new permissions, role perm counts unchanged.
    call_command("seed_rbac")
    assert Permission.objects.count() == perms_after_first
    assert {r.slug: r.permissions.count() for r in Role.objects.filter(
        slug__in=["manager", "staff", "visitor"])} == roles


@pytest.mark.django_db
def test_role_manager_relationship_is_exposed_and_updateable():
    manager = Role.objects.create(name="Research Manager", slug="research-manager")
    researcher = Role.objects.create(name="Researcher", slug="researcher")
    admin = User.objects.create_superuser(email="rbac-admin@x.com", password="p")
    client = APIClient()
    client.force_authenticate(user=admin)

    response = client.patch(
        f"/api/rbac/roles/{researcher.pk}/",
        {"manager_role_id": manager.pk},
        format="json",
    )

    assert response.status_code == 200
    assert response.json()["manager_role"] == {
        "id": manager.pk,
        "name": "Research Manager",
        "slug": "research-manager",
    }
    researcher.refresh_from_db()
    assert researcher.manager_role == manager


@pytest.mark.django_db
def test_role_manager_relationship_rejects_cycles():
    manager = Role.objects.create(name="Research Manager", slug="research-manager")
    researcher = Role.objects.create(
        name="Researcher",
        slug="researcher",
        manager_role=manager,
    )
    admin = User.objects.create_superuser(email="rbac-admin-cycle@x.com", password="p")
    client = APIClient()
    client.force_authenticate(user=admin)

    response = client.patch(
        f"/api/rbac/roles/{manager.pk}/",
        {"manager_role_id": researcher.pk},
        format="json",
    )

    assert response.status_code == 400
    assert "circular" in str(response.json()).lower()


# ---- user_has_full_scope ----

@pytest.mark.django_db
def test_full_scope_requires_delete_perm():
    org = _org()
    manager = _assign(_user("m@x.com"), "tasks.delete", organization=org)
    staff = _assign(_user("s@x.com"), "tasks.view", "tasks.create", organization=org)
    assert user_has_full_scope(manager, "tasks", org) is True
    assert user_has_full_scope(staff, "tasks", org) is False


@pytest.mark.django_db
def test_full_scope_superuser_always():
    su = User.objects.create_superuser(email="su@x.com", password="p")
    assert user_has_full_scope(su, "anything", su.organizations.first()) is True


@pytest.mark.django_db
def test_full_scope_empty_domain_false():
    org = _org()
    u = _assign(_user("u@x.com"), "tasks.delete", organization=org)
    assert user_has_full_scope(u, "", org) is False


# ---- IsOwnerOrFullScope ----

class _Obj:
    def __init__(self, **kw):
        self.__dict__.update(kw)


class _View:
    rbac_domain = "tasks"


class _Req:
    def __init__(self, user, method="PATCH"):
        self.user = user
        self.method = method
        self.session = {}


@pytest.mark.django_db
def test_object_perm_owner_can_write():
    owner = _assign(_user("o@x.com"), "tasks.view")
    obj = _Obj(created_by_id=owner.id)
    assert IsOwnerOrFullScope().has_object_permission(_Req(owner), _View(), obj) is True


@pytest.mark.django_db
def test_object_perm_non_owner_blocked():
    org = _org()
    owner = _user("o2@x.com")
    OrganizationMembership.objects.get_or_create(organization=org, user=owner)
    other = _assign(_user("x2@x.com"), "tasks.view", organization=org)
    obj = _Obj(created_by_id=owner.id)
    assert IsOwnerOrFullScope().has_object_permission(_Req(other), _View(), obj) is False


@pytest.mark.django_db
def test_object_perm_full_scope_writes_any():
    org = _org()
    mgr = _assign(_user("m2@x.com"), "tasks.delete", organization=org)
    other = _user("z@x.com")
    OrganizationMembership.objects.get_or_create(organization=org, user=other)
    obj = _Obj(created_by_id=other.id)
    assert IsOwnerOrFullScope().has_object_permission(_Req(mgr), _View(), obj) is True


@pytest.mark.django_db
def test_object_perm_reads_open():
    other = _assign(_user("r@x.com"), "tasks.view")
    obj = _Obj(created_by_id=_user("zz@x.com").id)
    assert IsOwnerOrFullScope().has_object_permission(_Req(other, "GET"), _View(), obj) is True


# ---- end-to-end: pdf_tools no longer publicly executable ----

@pytest.mark.django_db
def test_pdf_tools_not_public():
    """Unauthenticated POST must NOT execute the PDF op. Accepts 401/403 (now
    auth-gated) or 404 (route not currently wired into config.urls) — either way
    it is not a public 200/400 like before the permission_classes change."""
    client = APIClient()
    resp = client.post("/api/pdf-tools/compress/")
    assert resp.status_code in (401, 403, 404)


# ---- end-to-end: scoped user only sees/edits own records ----

@pytest.mark.django_db
def test_staff_task_list_scoped_to_own():
    from apps.tasks.models import Task
    from apps.companies.models import Organization
    org = Organization.objects.create(name="RBAC Test Org")
    owner = _assign(_user("ta@x.com"), "tasks.view", "tasks.create", organization=org)
    other = _assign(_user("tb@x.com"), "tasks.view", "tasks.create", organization=org)
    Task.objects.create(organization=org, title="mine", created_by=owner)
    Task.objects.create(organization=org, title="theirs", created_by=other)
    client = APIClient()
    client.force_authenticate(user=owner)
    resp = client.get("/api/tasks/")
    assert resp.status_code == 200
    body = resp.json()
    rows = body["results"] if isinstance(body, dict) else body
    titles = {t["title"] for t in rows}
    assert "mine" in titles and "theirs" not in titles


@pytest.mark.django_db
def test_manager_task_list_sees_all():
    from apps.tasks.models import Task
    from apps.companies.models import Organization
    org = Organization.objects.create(name="RBAC Test Org 2")
    mgr = _assign(_user("tm@x.com"), "tasks.view", "tasks.delete", organization=org)
    other = _user("tc@x.com")
    org.members.add(other)
    Task.objects.create(organization=org, title="theirs", created_by=other)
    client = APIClient()
    client.force_authenticate(user=mgr)
    resp = client.get("/api/tasks/")
    body = resp.json()
    rows = body["results"] if isinstance(body, dict) else body
    titles = {t["title"] for t in rows}
    assert "theirs" in titles
