"""Tests for WorkspaceAccessGrant — role → workspace access rules.

Covers:
  - add_access_grant / remove_access_grant / list_access_grants services
  - _sync_members_for_access_grant backfill
  - access materialization signal (Membership.users m2m_changed → post_add)
  - GET / POST / DELETE /workspaces/<slug>/access-grants/ API
"""

from __future__ import annotations

import pytest

from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.apps.memberships.models import Membership
from simorgh.apps.workspaces.models import WorkspaceMembership, WorkspaceAccessGrant
from simorgh.apps.workspaces.services import (
    add_access_grant,
    add_member,
    create_workspace,
    list_access_grants,
    remove_access_grant,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def perms(db):
    sync_registry_to_db()
    from simorgh.apps.iam.models import Permission
    return {p.codename: p for p in Permission.objects.all()}


@pytest.fixture
def ws(tenant_acme, acme_tree):
    return create_workspace(tenant_acme, slug="support-center", name="Support Center")


@pytest.fixture
def role_support(tenant_acme, perms):
    from simorgh.apps.iam.models import Role
    role = Role.objects.create(tenant=tenant_acme, code="support_agent", name="Support Agent")
    role.permissions.set([perms["workspaces.workspace.view"]])
    return role


@pytest.fixture
def bob(db):
    from simorgh.apps.accounts.models import User
    return User.objects.create_user("+989000000099", password="x", email="bob@example.com")


@pytest.fixture
def admin_client(alice, tenant_acme, api_client):
    """DRF client authenticated as alice (superuser) with tenant header."""
    alice.is_superuser = True
    alice.save(update_fields=["is_superuser"])
    api_client.force_login(alice)
    return api_client, alice, tenant_acme


# ---------------------------------------------------------------------------
# Service: add_access_grant
# ---------------------------------------------------------------------------

class TestAddAccessGrant:
    def test_creates_grant(self, db, ws, role_support, acme_tree):
        grant = add_access_grant(ws, role_support)
        assert WorkspaceAccessGrant.objects.filter(workspace=ws, role=role_support).exists()
        assert grant.workspace_id == ws.pk
        assert grant.role_id == role_support.pk

    def test_idempotent(self, db, ws, role_support, acme_tree):
        g1 = add_access_grant(ws, role_support)
        g2 = add_access_grant(ws, role_support)
        assert g1.pk == g2.pk
        assert WorkspaceAccessGrant.objects.filter(workspace=ws, role=role_support).count() == 1

    def test_backfills_existing_membership_users(self, db, ws, role_support, acme_tree, bob, tenant_acme):
        """Users already in Membership with this role get workspace access on grant creation."""
        m = Membership.objects.create(
            tenant=tenant_acme,
            organization_node=acme_tree["root"],
            role=role_support,
        )
        m.users.add(bob)

        add_access_grant(ws, role_support)

        assert WorkspaceMembership.objects.filter(workspace=ws, user=bob).exists()


# ---------------------------------------------------------------------------
# Service: remove_access_grant
# ---------------------------------------------------------------------------

class TestRemoveAccessGrant:
    def test_removes_grant(self, db, ws, role_support, acme_tree):
        add_access_grant(ws, role_support)
        remove_access_grant(ws, role_support)
        assert not WorkspaceAccessGrant.objects.filter(workspace=ws, role=role_support).exists()

    def test_noop_if_not_exists(self, db, ws, role_support, acme_tree):
        """Removing a non-existent grant does not raise."""
        remove_access_grant(ws, role_support)  # should not raise


# ---------------------------------------------------------------------------
# Service: list_access_grants
# ---------------------------------------------------------------------------

class TestListAccessGrants:
    def test_returns_grants(self, db, ws, role_support, acme_tree):
        add_access_grant(ws, role_support)
        grants = list(list_access_grants(ws))
        assert len(grants) == 1
        assert grants[0].role_id == role_support.pk

    def test_empty_when_no_grants(self, db, ws):
        assert list(list_access_grants(ws)) == []


# ---------------------------------------------------------------------------
# Signal: access materialization
# ---------------------------------------------------------------------------

class TestAccessMaterializationSignal:
    def test_materialize_on_membership_user_add(
        self, db, ws, role_support, acme_tree, bob, tenant_acme
    ):
        """Adding a user to a Membership materializes WorkspaceMembership if a grant exists."""
        add_access_grant(ws, role_support)

        m = Membership.objects.create(
            tenant=tenant_acme,
            organization_node=acme_tree["root"],
            role=role_support,
        )
        m.users.add(bob)  # ← triggers signal

        assert WorkspaceMembership.objects.filter(workspace=ws, user=bob).exists()

    def test_no_materialization_without_grant(
        self, db, ws, role_support, acme_tree, bob, tenant_acme
    ):
        """If no grant exists, adding user to Membership does NOT materialize."""
        m = Membership.objects.create(
            tenant=tenant_acme,
            organization_node=acme_tree["root"],
            role=role_support,
        )
        m.users.add(bob)

        assert not WorkspaceMembership.objects.filter(workspace=ws, user=bob).exists()

    def test_materialize_multiple_workspaces(
        self, db, ws, role_support, acme_tree, bob, tenant_acme
    ):
        """User gets access to every workspace that grants the role."""
        ws2 = create_workspace(tenant_acme, slug="finance-hub", name="Finance Hub")
        add_access_grant(ws, role_support)
        add_access_grant(ws2, role_support)

        m = Membership.objects.create(
            tenant=tenant_acme,
            organization_node=acme_tree["root"],
            role=role_support,
        )
        m.users.add(bob)

        assert WorkspaceMembership.objects.filter(workspace=ws, user=bob).exists()
        assert WorkspaceMembership.objects.filter(workspace=ws2, user=bob).exists()

    def test_suspended_membership_no_materialize(
        self, db, ws, role_support, acme_tree, bob, tenant_acme
    ):
        """Suspended memberships do not trigger access materialization."""
        from simorgh.apps.memberships.models import MembershipStatus
        add_access_grant(ws, role_support)

        m = Membership.objects.create(
            tenant=tenant_acme,
            organization_node=acme_tree["root"],
            role=role_support,
            status=MembershipStatus.SUSPENDED,
        )
        m.users.add(bob)

        assert not WorkspaceMembership.objects.filter(workspace=ws, user=bob).exists()


# ---------------------------------------------------------------------------
# API
# ---------------------------------------------------------------------------

class TestAccessGrantsAPI:
    BASE = "/api/v1/workspaces/{slug}/access-grants/"

    def url(self, slug: str) -> str:
        return self.BASE.format(slug=slug)

    def test_list_empty(self, db, ws, admin_client):
        client, alice, tenant_acme = admin_client
        add_member(ws, alice)
        resp = client.get(self.url(ws.slug), HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        assert resp.data["results"] == []

    def test_add_access_grant_via_api(self, db, ws, role_support, admin_client):
        client, alice, tenant_acme = admin_client
        add_member(ws, alice)
        import json as _json
        resp = client.post(
            self.url(ws.slug),
            data=_json.dumps({"role_id": str(role_support.pk)}),
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 201
        assert resp.data["role_id"] == str(role_support.pk)
        assert WorkspaceAccessGrant.objects.filter(workspace=ws, role=role_support).exists()

    def test_list_shows_grant(self, db, ws, role_support, admin_client, acme_tree):
        client, alice, tenant_acme = admin_client
        add_member(ws, alice)
        add_access_grant(ws, role_support)
        resp = client.get(self.url(ws.slug), HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        assert len(resp.data["results"]) == 1
        assert resp.data["results"][0]["role_code"] == role_support.code

    def test_delete_access_grant_via_api(self, db, ws, role_support, admin_client, acme_tree):
        import json as _json
        client, alice, tenant_acme = admin_client
        add_member(ws, alice)
        add_access_grant(ws, role_support)
        resp = client.delete(
            self.url(ws.slug),
            data=_json.dumps({"role_id": str(role_support.pk)}),
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 204
        assert not WorkspaceAccessGrant.objects.filter(workspace=ws, role=role_support).exists()

    def test_add_unknown_role_returns_400(self, db, ws, admin_client):
        import json as _json
        client, alice, tenant_acme = admin_client
        add_member(ws, alice)
        resp = client.post(
            self.url(ws.slug),
            data=_json.dumps({"role_id": 99999}),
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 400

    def test_unauthenticated_returns_401(self, db, ws, acme_tree, api_client):
        resp = api_client.get(self.url(ws.slug), HTTP_X_TENANT="acme")
        assert resp.status_code == 401
