"""Tests for DMS Phase 15 — Notification Integration.

Covers:
  * notifications/templates.py — all 8 DMS templates registered
  * notifications/services.py  — dispatch_dms_notification best-effort wrapper
  * notifications/handlers.py  — on_mention_created signal: Notification created, is_notified=True
  * notifications/handlers.py  — on_comment_resolved signal: Notification created for comment author
  * notifications/handlers.py  — on_document_published subscriber: Notification for document creator
  * notifications/handlers.py  — on_document_archived subscriber: Notification for document creator
  * notifications/handlers.py  — on_document_submitted_for_review subscriber
  * notifications/handlers.py  — on_checkout_expired subscriber: Notification for lock holder
  * notifications/handlers.py  — on_hold_placed subscriber: Notification for document creator
  * Best-effort: missing document / user does not raise
"""

from __future__ import annotations

import uuid

import pytest
from django.db.models.signals import post_save

from simorgh.apps.events.bus import dispatch as bus_dispatch, clear_subscribers
from simorgh.apps.notifications.models import Notification
from simorgh.apps.notifications.templates import get_template


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def alice_membership(alice, tenant_acme, acme_tree, role_admin):
    """alice has admin role (all permissions)."""
    from simorgh.apps.memberships.models import Membership

    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_admin,
    )
    m.users.add(alice)
    return m


@pytest.fixture
def bob(db):
    from django.contrib.auth import get_user_model

    User = get_user_model()
    return User.objects.create_user(
        "+989000000098",
        password="x",
        email="bob_ph15@example.com",
    )


@pytest.fixture
def repo(tenant_acme, acme_tree):
    from simorgh.apps.dms.repositories.models import Repository

    return Repository.objects.create(
        name="Phase 15 Repo",
        slug="ph15-repo",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def doc(repo, tenant_acme, acme_tree, alice):
    """Document with alice as creator (created_by)."""
    from simorgh.apps.dms.documents.models import Document

    d = Document.objects.create(
        title="Phase 15 Test Document",
        repository=repo,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )
    # Set created_by directly
    d.created_by = alice
    d.save(update_fields=["created_by"])
    return d


@pytest.fixture
def comment(doc, tenant_acme, acme_tree, alice):
    """Root comment by alice."""
    from simorgh.apps.dms.collaboration.models import Comment
    from simorgh.apps.dms.notifications.handlers import on_comment_resolved

    # Disconnect using dispatch_uid to match how apps.py connected it
    post_save.disconnect(
        on_comment_resolved,
        sender=Comment,
        dispatch_uid="dms_notify_comment_resolved",
    )
    c = Comment.objects.create(
        document=doc,
        author=alice,
        body="Test comment body",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )
    post_save.connect(
        on_comment_resolved,
        sender=Comment,
        dispatch_uid="dms_notify_comment_resolved",
    )
    return c


@pytest.fixture
def mention(comment, bob, tenant_acme, acme_tree):
    """Mention of bob in alice's comment (is_notified=False)."""
    from simorgh.apps.dms.collaboration.models import Mention
    from simorgh.apps.dms.notifications.handlers import on_mention_created

    # Disconnect using dispatch_uid to match how apps.py connected it
    post_save.disconnect(
        on_mention_created,
        sender=Mention,
        dispatch_uid="dms_notify_mention_created",
    )
    m = Mention.objects.create(
        comment=comment,
        mentioned_user=bob,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        is_notified=False,
    )
    post_save.connect(
        on_mention_created,
        sender=Mention,
        dispatch_uid="dms_notify_mention_created",
    )
    return m


@pytest.fixture
def doc_lock(doc, bob, tenant_acme, acme_tree):
    """An active DocumentLock held by bob."""
    from simorgh.apps.dms.versioning.models import DocumentLock

    return DocumentLock.objects.create(
        document=doc,
        locked_by=bob,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


# ---------------------------------------------------------------------------
# Template registration tests
# ---------------------------------------------------------------------------


def test_dms_mention_template_registered():
    t = get_template("dms.mention")
    assert t.kind == "dms.mention"
    assert t.default_channels == ("inbox",)


def test_dms_comment_resolved_template_registered():
    t = get_template("dms.comment_resolved")
    assert t.kind == "dms.comment_resolved"


def test_dms_document_published_template_registered():
    t = get_template("dms.document_published")
    assert t.kind == "dms.document_published"


def test_dms_document_submitted_template_registered():
    t = get_template("dms.document_submitted_for_review")
    assert t.kind == "dms.document_submitted_for_review"


def test_dms_document_archived_template_registered():
    t = get_template("dms.document_archived")
    assert t.kind == "dms.document_archived"


def test_dms_hold_placed_template_registered():
    t = get_template("dms.hold_placed")
    assert t.kind == "dms.hold_placed"


def test_dms_checkout_expired_template_registered():
    t = get_template("dms.checkout_expired")
    assert t.kind == "dms.checkout_expired"


def test_dms_share_link_revoked_template_registered():
    t = get_template("dms.share_link_revoked")
    assert t.kind == "dms.share_link_revoked"


# ---------------------------------------------------------------------------
# dispatch_dms_notification — wrapper behaviour
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_dispatch_dms_notification_creates_record(tenant_acme, acme_tree, alice):
    """The wrapper creates a Notification row for the given kind."""
    from simorgh.apps.dms.notifications.services import dispatch_dms_notification

    dispatch_dms_notification(
        "dms.document_published",
        recipients=[alice.pk],
        context={"document_title": "My Doc"},
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert Notification.objects.filter(
        recipient=alice, kind="dms.document_published"
    ).exists()


@pytest.mark.django_db
def test_dispatch_dms_notification_is_best_effort(tenant_acme, acme_tree, alice):
    """dispatch_dms_notification never raises even for unknown template kinds."""
    from simorgh.apps.dms.notifications.services import dispatch_dms_notification

    # Unknown kind — LookupError inside, should be swallowed
    dispatch_dms_notification(
        "dms.nonexistent_kind_xyz",
        recipients=[alice.pk],
        context={},
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    # No exception, and no notification created
    assert not Notification.objects.filter(kind="dms.nonexistent_kind_xyz").exists()


# ---------------------------------------------------------------------------
# Mention signal handler
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_mention_signal_creates_notification(
    tenant_acme, acme_tree, alice, bob, doc, comment, perms
):
    """Creating a Mention dispatches a dms.mention notification to the mentioned user."""
    from simorgh.apps.dms.collaboration.models import Mention

    assert not Notification.objects.filter(recipient=bob, kind="dms.mention").exists()

    Mention.objects.create(
        comment=comment,
        mentioned_user=bob,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        is_notified=False,
    )

    assert Notification.objects.filter(recipient=bob, kind="dms.mention").exists()


@pytest.mark.django_db
def test_mention_signal_marks_is_notified(
    tenant_acme, acme_tree, alice, bob, doc, comment, perms
):
    """After the signal fires, Mention.is_notified becomes True."""
    from simorgh.apps.dms.collaboration.models import Mention

    m = Mention.objects.create(
        comment=comment,
        mentioned_user=bob,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        is_notified=False,
    )
    m.refresh_from_db()
    assert m.is_notified is True


@pytest.mark.django_db
def test_mention_signal_skips_already_notified(
    tenant_acme, acme_tree, alice, bob, mention, perms
):
    """Saving (not creating) an already-notified Mention does not double-notify."""
    mention.is_notified = True
    mention.save(update_fields=["is_notified"])

    assert not Notification.objects.filter(recipient=bob, kind="dms.mention").exists()


# ---------------------------------------------------------------------------
# Comment resolved signal handler
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_comment_resolved_signal_creates_notification(
    tenant_acme, acme_tree, alice, doc, comment, perms
):
    """Resolving a comment dispatches a dms.comment_resolved notification to the author."""
    from simorgh.apps.dms.collaboration.services import resolve_comment

    resolve_comment(comment, resolving_user=alice)

    assert Notification.objects.filter(
        recipient=alice, kind="dms.comment_resolved"
    ).exists()


@pytest.mark.django_db
def test_comment_created_does_not_trigger_resolved_notification(
    tenant_acme, acme_tree, alice, doc, perms
):
    """Creating a comment (created=True) should NOT fire the resolved notification."""
    from simorgh.apps.dms.collaboration.models import Comment

    Comment.objects.create(
        document=doc,
        author=alice,
        body="Another comment",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )
    assert not Notification.objects.filter(kind="dms.comment_resolved").exists()


@pytest.mark.django_db
def test_unresolved_comment_save_does_not_notify(
    tenant_acme, acme_tree, alice, doc, comment, perms
):
    """Saving a comment without resolving it does not fire a notification."""
    comment.body = "Updated body"
    comment.save(update_fields=["body", "updated_at"])
    assert not Notification.objects.filter(kind="dms.comment_resolved").exists()


# ---------------------------------------------------------------------------
# Event-bus handler: document published
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_published_handler_notifies_document_creator(
    tenant_acme, acme_tree, alice, doc, perms
):
    """dms.document.published event creates a notification for the document creator."""
    bus_dispatch(
        "dms.document.published",
        {
            "document_id": str(doc.public_id),
            "tenant_id": tenant_acme.pk,
            "actor_id": alice.pk,
            "workflow_status": "published",
            "comment": "",
        },
    )
    assert Notification.objects.filter(
        recipient=alice, kind="dms.document_published"
    ).exists()


@pytest.mark.django_db
def test_published_handler_missing_document_does_not_raise(tenant_acme, acme_tree, perms):
    """on_document_published with an unknown document_id is silently ignored."""
    bus_dispatch(
        "dms.document.published",
        {
            "document_id": str(uuid.uuid4()),
            "tenant_id": tenant_acme.pk,
            "actor_id": 999,
            "workflow_status": "published",
            "comment": "",
        },
    )
    assert not Notification.objects.filter(kind="dms.document_published").exists()


# ---------------------------------------------------------------------------
# Event-bus handler: document archived
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_archived_handler_notifies_document_creator(
    tenant_acme, acme_tree, alice, doc, perms
):
    """dms.document.archived event creates a notification for the document creator."""
    bus_dispatch(
        "dms.document.archived",
        {
            "document_id": str(doc.public_id),
            "tenant_id": tenant_acme.pk,
            "actor_id": alice.pk,
            "workflow_status": "archived",
            "comment": "",
        },
    )
    assert Notification.objects.filter(
        recipient=alice, kind="dms.document_archived"
    ).exists()


# ---------------------------------------------------------------------------
# Event-bus handler: document submitted for review
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_submitted_handler_notifies_document_creator(
    tenant_acme, acme_tree, alice, doc, perms
):
    """dms.document.submitted_for_review event creates a notification for the creator."""
    bus_dispatch(
        "dms.document.submitted_for_review",
        {
            "document_id": str(doc.public_id),
            "tenant_id": tenant_acme.pk,
            "actor_id": alice.pk,
            "workflow_status": "under_review",
            "comment": "",
        },
    )
    assert Notification.objects.filter(
        recipient=alice, kind="dms.document_submitted_for_review"
    ).exists()


# ---------------------------------------------------------------------------
# Event-bus handler: checkout expired
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_checkout_expired_handler_notifies_lock_holder(
    tenant_acme, acme_tree, alice, bob, doc, doc_lock, perms
):
    """dms.checkout.expired event creates a notification for the lock holder."""
    bus_dispatch(
        "dms.checkout.expired",
        {
            "document_id": str(doc.public_id),
            "lock_id": str(doc_lock.public_id),
            "tenant_id": tenant_acme.pk,
        },
    )
    assert Notification.objects.filter(
        recipient=bob, kind="dms.checkout_expired"
    ).exists()


@pytest.mark.django_db
def test_checkout_expired_handler_missing_lock_does_not_raise(
    tenant_acme, acme_tree, doc, perms
):
    """checkout.expired with unknown lock_id is silently ignored."""
    bus_dispatch(
        "dms.checkout.expired",
        {
            "document_id": str(doc.public_id),
            "lock_id": str(uuid.uuid4()),
            "tenant_id": tenant_acme.pk,
        },
    )
    assert not Notification.objects.filter(kind="dms.checkout_expired").exists()


# ---------------------------------------------------------------------------
# Event-bus handler: hold placed
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_hold_placed_handler_notifies_document_creator(
    tenant_acme, acme_tree, alice, doc, perms
):
    """dms.hold.placed event creates a notification for the document creator."""
    bus_dispatch(
        "dms.hold.placed",
        {
            "document_id": str(doc.public_id),
            "hold_id": str(uuid.uuid4()),
            "tenant_id": tenant_acme.pk,
            "actor_id": alice.pk,
        },
    )
    assert Notification.objects.filter(
        recipient=alice, kind="dms.hold_placed"
    ).exists()


@pytest.mark.django_db
def test_hold_placed_handler_no_creator_does_not_raise(
    tenant_acme, acme_tree, alice, repo, perms
):
    """hold.placed with a document that has no created_by is silently ignored."""
    from simorgh.apps.dms.documents.models import Document

    doc_no_creator = Document.objects.create(
        title="No Creator Doc",
        repository=repo,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )
    # created_by is NULL by default
    bus_dispatch(
        "dms.hold.placed",
        {
            "document_id": str(doc_no_creator.public_id),
            "hold_id": str(uuid.uuid4()),
            "tenant_id": tenant_acme.pk,
            "actor_id": alice.pk,
        },
    )
    assert not Notification.objects.filter(kind="dms.hold_placed").exists()
