"""WS3 / FRREP-AC005 — CollectionGroupAccess ACL pivot.

Covers:

* The model invariant (`unique_together` on collection+group).
* `Document.objects.for_user()` honouring active group grants.
* Expired grants being filtered out.
* The `grant_collection_access` upsert behaviour.
* `revoke_collection_access` deleting the grant + writing audit.
"""

from __future__ import annotations

from datetime import timedelta

import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.utils import timezone

from apps.core.audit import tasks as audit_tasks
from apps.core.audit.models import AuditLog
from apps.repository.documents import services
from apps.repository.documents.models import (
    CollectionGroupAccess,
    Document,
)

User = get_user_model()


# ── Sync-audit fixture ────────────────────────────────────────────────────


@pytest.fixture(autouse=True)
def sync_audit(monkeypatch):
    """Make `write_audit_log.delay(...)` write synchronously inside tests.

    Production routes audit writes through Celery (apps/core/audit/mixins.py
    `log_audit` → `transaction.on_commit` → `write_audit_log.delay`). Without
    eager mode the rows never appear in the test DB. Patching `delay` to call
    `persist_audit_log` directly is the lightest-weight equivalent.
    """

    def _eager_delay(**kwargs):
        return audit_tasks.persist_audit_log(**kwargs)

    monkeypatch.setattr(audit_tasks.write_audit_log, "delay", _eager_delay)


# ── Helpers ────────────────────────────────────────────────────────────────


def _publish(doc, *, manager):
    """Push a document through QA → published."""
    services.submit_for_qa(doc, actor=doc.submitted_by)
    services.decide_qa(doc.qa_records.first(), decision="approve", actor=manager, checklist=services.full_qa_checklist())


@pytest.fixture
def health_group(db):
    return Group.objects.create(name="Health Researchers")


@pytest.fixture
def member_user(db, health_group):
    u = User.objects.create_user(
        email="member@test.example", password="pw-test-1234"
    )
    u.groups.add(health_group)
    return u


@pytest.fixture
def outsider_user(db):
    return User.objects.create_user(
        email="outsider@test.example", password="pw-test-1234"
    )


# ── Model invariants ──────────────────────────────────────────────────────


@pytest.mark.django_db
def test_unique_collection_group_pair(restricted_collection, health_group, manager_user):
    """A given (collection, group) pair can only have one grant row."""
    services.grant_collection_access(
        restricted_collection, health_group, actor=manager_user
    )
    services.grant_collection_access(
        restricted_collection, health_group, actor=manager_user
    )
    # Upsert behaviour — second call updates, doesn't create.
    assert (
        CollectionGroupAccess.objects.filter(
            collection=restricted_collection, group=health_group
        ).count()
        == 1
    )


@pytest.mark.django_db
def test_grant_is_active_when_no_expiry(restricted_collection, health_group, manager_user):
    grant = services.grant_collection_access(
        restricted_collection, health_group, actor=manager_user
    )
    assert grant.is_active is True


@pytest.mark.django_db
def test_grant_inactive_after_expiry(restricted_collection, health_group, manager_user):
    past = timezone.now() - timedelta(days=1)
    grant = services.grant_collection_access(
        restricted_collection,
        health_group,
        actor=manager_user,
        expires_at=past,
    )
    assert grant.is_active is False


# ── Manager / queryset behaviour ─────────────────────────────────────────


@pytest.mark.django_db
def test_member_sees_restricted_docs_when_group_granted(
    restricted_collection,
    health_group,
    member_user,
    manager_user,
    submitter_user,
    sample_pdf,
):
    doc = services.ingest_document(
        file=sample_pdf,
        title="Restricted Maternal Health Report",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)

    # Without a grant — member cannot see the doc.
    assert doc not in Document.objects.for_user(member_user)

    # Grant the group → member now sees it.
    services.grant_collection_access(
        restricted_collection, health_group, actor=manager_user
    )
    visible = Document.objects.for_user(member_user)
    assert doc in visible


@pytest.mark.django_db
def test_outsider_does_not_see_restricted_docs(
    restricted_collection,
    health_group,
    outsider_user,
    manager_user,
    submitter_user,
    sample_pdf,
):
    """Granting access to one group does not leak to non-members."""
    doc = services.ingest_document(
        file=sample_pdf,
        title="Restricted Doc",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)
    services.grant_collection_access(
        restricted_collection, health_group, actor=manager_user
    )
    assert doc not in Document.objects.for_user(outsider_user)


@pytest.mark.django_db
def test_expired_grant_filtered_out(
    restricted_collection,
    health_group,
    member_user,
    manager_user,
    submitter_user,
    sample_pdf,
):
    doc = services.ingest_document(
        file=sample_pdf,
        title="Restricted Doc",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)

    # Expired in the past → no visibility.
    services.grant_collection_access(
        restricted_collection,
        health_group,
        actor=manager_user,
        expires_at=timezone.now() - timedelta(hours=1),
    )
    assert doc not in Document.objects.for_user(member_user)


# ── Service-level behaviour + audit ──────────────────────────────────────
#
# Audit logging routes through ``transaction.on_commit`` (apps/core/audit/mixins.py),
# so these audit-asserting tests use ``transaction=True`` to make the wrapping
# Django transaction actually commit and trigger the on_commit callback.


@pytest.mark.django_db(transaction=True)
def test_revoke_writes_audit_then_deletes_row(
    restricted_collection, health_group, manager_user
):
    grant = services.grant_collection_access(
        restricted_collection, health_group, actor=manager_user
    )
    grant_pk = grant.pk

    services.revoke_collection_access(grant, actor=manager_user)

    assert not CollectionGroupAccess.objects.filter(pk=grant_pk).exists()
    deletions = AuditLog.objects.filter(
        target_app="repository_documents",
        target_model="CollectionGroupAccess",
        action=AuditLog.Action.DELETE,
        object_id=str(grant_pk),
    )
    assert deletions.exists()


@pytest.mark.django_db(transaction=True)
def test_grant_records_audit_on_create_and_update(
    restricted_collection, health_group, manager_user
):
    services.grant_collection_access(
        restricted_collection, health_group, actor=manager_user
    )
    services.grant_collection_access(
        restricted_collection,
        health_group,
        actor=manager_user,
        permission_level=CollectionGroupAccess.PermissionLevel.COMMENT,
    )
    base = AuditLog.objects.filter(
        target_app="repository_documents",
        target_model="CollectionGroupAccess",
    )
    assert base.filter(action=AuditLog.Action.CREATE).exists()
    assert base.filter(action=AuditLog.Action.UPDATE).exists()
