"""Tests for DMS Phase 17 — Extensibility + Error Handling i18n + Documentation.

Covers:
  * hooks.py — all 6 hook name constants + ALL_HOOKS frozenset
  * registry.py — register_hook decorator, fire_hook, clear_hooks, get_handlers
  * registry.py — error isolation (exception in one handler does not propagate)
  * documents/services.py — hooks fired on create / update / delete / publish
  * locale — fa and ar .po files present
"""

from __future__ import annotations

import pytest

from simorgh.apps.dms.hooks import (
    ALL_HOOKS,
    HOOK_POST_DOCUMENT_ARCHIVE,
    HOOK_POST_DOCUMENT_CREATE,
    HOOK_POST_DOCUMENT_DELETE,
    HOOK_POST_DOCUMENT_PUBLISH,
    HOOK_POST_DOCUMENT_UPDATE,
    HOOK_PRE_DOCUMENT_DELETE,
)
from simorgh.apps.dms.registry import clear_hooks, fire_hook, get_handlers, register_hook


# ---------------------------------------------------------------------------
# Autouse: isolate hook state between tests
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def _clean_hooks():
    """Clear all DMS hooks before and after every test."""
    for hook in ALL_HOOKS:
        clear_hooks(hook)
    yield
    for hook in ALL_HOOKS:
        clear_hooks(hook)


# ---------------------------------------------------------------------------
# Fixtures (same pattern as Phase 16)
# ---------------------------------------------------------------------------

@pytest.fixture
def repo(tenant_acme, acme_tree):
    from simorgh.apps.dms.repositories.models import Repository

    return Repository.objects.create(
        name="Phase 17 Repo",
        slug="ph17-repo",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def doc(repo, tenant_acme, acme_tree):
    from simorgh.apps.dms.documents.models import Document, DocumentStatus

    # Create directly (bypass service) so no hook fires during fixture setup.
    return Document.objects.create(
        title="Phase 17 Document",
        repository=repo,
        status=DocumentStatus.DRAFT,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def draft_version(doc, tenant_acme, acme_tree):
    from simorgh.apps.dms.documents.services import add_version

    return add_version(
        document=doc,
        bump="minor",
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )


# ---------------------------------------------------------------------------
# 1. hooks.py — constants
# ---------------------------------------------------------------------------

def test_hook_constants_are_strings():
    for hook in (
        HOOK_POST_DOCUMENT_CREATE,
        HOOK_POST_DOCUMENT_UPDATE,
        HOOK_PRE_DOCUMENT_DELETE,
        HOOK_POST_DOCUMENT_DELETE,
        HOOK_POST_DOCUMENT_ARCHIVE,
        HOOK_POST_DOCUMENT_PUBLISH,
    ):
        assert isinstance(hook, str), f"{hook!r} should be a str"


def test_all_hooks_is_frozenset_of_six():
    assert isinstance(ALL_HOOKS, frozenset)
    assert len(ALL_HOOKS) == 6


def test_all_hooks_contains_every_constant():
    expected = {
        HOOK_POST_DOCUMENT_CREATE,
        HOOK_POST_DOCUMENT_UPDATE,
        HOOK_PRE_DOCUMENT_DELETE,
        HOOK_POST_DOCUMENT_DELETE,
        HOOK_POST_DOCUMENT_ARCHIVE,
        HOOK_POST_DOCUMENT_PUBLISH,
    }
    assert ALL_HOOKS == expected


# ---------------------------------------------------------------------------
# 2. registry.py — register_hook decorator
# ---------------------------------------------------------------------------

def test_register_hook_decorator_returns_original_function():
    def my_handler(**kwargs):
        pass

    returned = register_hook(HOOK_POST_DOCUMENT_CREATE)(my_handler)
    assert returned is my_handler


def test_register_hook_appends_to_registry():
    def handler_a(**kwargs):
        pass

    def handler_b(**kwargs):
        pass

    register_hook(HOOK_POST_DOCUMENT_CREATE)(handler_a)
    register_hook(HOOK_POST_DOCUMENT_CREATE)(handler_b)
    handlers = get_handlers(HOOK_POST_DOCUMENT_CREATE)
    assert handler_a in handlers
    assert handler_b in handlers


# ---------------------------------------------------------------------------
# 3. registry.py — fire_hook
# ---------------------------------------------------------------------------

def test_fire_hook_with_no_handlers_does_not_raise():
    # No handlers registered — should be a no-op.
    fire_hook(HOOK_POST_DOCUMENT_CREATE, document=object())


def test_fire_hook_calls_handler_with_correct_kwargs():
    received = {}

    def handler(**kwargs):
        received.update(kwargs)

    register_hook(HOOK_POST_DOCUMENT_CREATE)(handler)
    sentinel = object()
    fire_hook(HOOK_POST_DOCUMENT_CREATE, document=sentinel)
    assert received.get("document") is sentinel


def test_fire_hook_calls_multiple_handlers_in_order():
    order = []

    def first(**kwargs):
        order.append("first")

    def second(**kwargs):
        order.append("second")

    register_hook(HOOK_POST_DOCUMENT_CREATE)(first)
    register_hook(HOOK_POST_DOCUMENT_CREATE)(second)
    fire_hook(HOOK_POST_DOCUMENT_CREATE, document=None)
    assert order == ["first", "second"]


def test_fire_hook_error_does_not_propagate():
    def bad_handler(**kwargs):
        raise RuntimeError("boom")

    register_hook(HOOK_POST_DOCUMENT_CREATE)(bad_handler)
    # Must not raise.
    fire_hook(HOOK_POST_DOCUMENT_CREATE, document=None)


def test_fire_hook_error_still_calls_remaining_handlers():
    called = []

    def bad_handler(**kwargs):
        raise RuntimeError("first handler explodes")

    def good_handler(**kwargs):
        called.append("good")

    register_hook(HOOK_POST_DOCUMENT_CREATE)(bad_handler)
    register_hook(HOOK_POST_DOCUMENT_CREATE)(good_handler)
    fire_hook(HOOK_POST_DOCUMENT_CREATE, document=None)
    assert called == ["good"]


# ---------------------------------------------------------------------------
# 4. registry.py — clear_hooks / get_handlers
# ---------------------------------------------------------------------------

def test_clear_hooks_removes_all_handlers():
    def h(**kwargs):
        pass

    register_hook(HOOK_POST_DOCUMENT_CREATE)(h)
    assert len(get_handlers(HOOK_POST_DOCUMENT_CREATE)) == 1
    clear_hooks(HOOK_POST_DOCUMENT_CREATE)
    assert get_handlers(HOOK_POST_DOCUMENT_CREATE) == []


def test_get_handlers_returns_copy():
    def h(**kwargs):
        pass

    register_hook(HOOK_POST_DOCUMENT_CREATE)(h)
    copy1 = get_handlers(HOOK_POST_DOCUMENT_CREATE)
    copy2 = get_handlers(HOOK_POST_DOCUMENT_CREATE)
    assert copy1 == copy2
    copy1.clear()
    # Mutating the returned list must not affect the registry.
    assert len(get_handlers(HOOK_POST_DOCUMENT_CREATE)) == 1


# ---------------------------------------------------------------------------
# 5. Service integration — hooks fired from service layer
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_create_document_fires_post_hook(repo, tenant_acme, acme_tree):
    from simorgh.apps.dms.documents.services import create_document

    fired = []

    def handler(document, **kwargs):
        fired.append(document)

    register_hook(HOOK_POST_DOCUMENT_CREATE)(handler)
    created = create_document(
        title="Hook Test Doc",
        repository=repo,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert len(fired) == 1
    assert fired[0].pk == created.pk


@pytest.mark.django_db
def test_update_document_fires_post_hook(doc, tenant_acme):
    from simorgh.apps.dms.documents.services import update_document

    fired = []

    def handler(document, **kwargs):
        fired.append(document)

    register_hook(HOOK_POST_DOCUMENT_UPDATE)(handler)
    update_document(doc, title="Updated Title")
    assert len(fired) == 1
    assert fired[0].pk == doc.pk


@pytest.mark.django_db
def test_delete_document_fires_pre_and_post_hooks(doc):
    from simorgh.apps.dms.documents.models import Document
    from simorgh.apps.dms.documents.services import delete_document

    pre_fired = []
    post_fired = []

    def pre_handler(document, **kwargs):
        # Doc should still exist in DB at PRE hook time.
        assert Document.objects.filter(pk=document.pk).exists()
        pre_fired.append(document.pk)

    def post_handler(document, **kwargs):
        post_fired.append(document.pk)

    register_hook(HOOK_PRE_DOCUMENT_DELETE)(pre_handler)
    register_hook(HOOK_POST_DOCUMENT_DELETE)(post_handler)

    doc_pk = doc.pk
    delete_document(doc)

    assert pre_fired == [doc_pk]
    assert post_fired == [doc_pk]


@pytest.mark.django_db
def test_publish_version_fires_post_hook(draft_version):
    from simorgh.apps.dms.documents.services import publish_version

    fired = []

    def handler(version, document, **kwargs):
        fired.append({"version": version, "document": document})

    register_hook(HOOK_POST_DOCUMENT_PUBLISH)(handler)
    published = publish_version(draft_version)

    assert len(fired) == 1
    assert fired[0]["version"].pk == published.pk
    assert fired[0]["document"].pk == published.document_id


# ---------------------------------------------------------------------------
# 6. Locale files exist
# ---------------------------------------------------------------------------

def test_locale_fa_po_file_exists():
    from pathlib import Path

    from django.conf import settings

    po = Path(settings.BASE_DIR) / "locale" / "fa" / "LC_MESSAGES" / "django.po"
    assert po.exists(), f"Missing: {po}"


def test_locale_ar_po_file_exists():
    from pathlib import Path

    from django.conf import settings

    po = Path(settings.BASE_DIR) / "locale" / "ar" / "LC_MESSAGES" / "django.po"
    assert po.exists(), f"Missing: {po}"


def test_locale_fa_mo_file_exists():
    from pathlib import Path

    from django.conf import settings

    mo = Path(settings.BASE_DIR) / "locale" / "fa" / "LC_MESSAGES" / "django.mo"
    assert mo.exists(), f"Missing: {mo}"


def test_locale_ar_mo_file_exists():
    from pathlib import Path

    from django.conf import settings

    mo = Path(settings.BASE_DIR) / "locale" / "ar" / "LC_MESSAGES" / "django.mo"
    assert mo.exists(), f"Missing: {mo}"
