"""Tests for Phase 3 — audit, events, notifications, storage, settings."""

from __future__ import annotations

import io

import pytest

from simorgh.apps.app_settings.registry import (
    SettingDefinition,
    register_setting,
    reset_registry_for_tests,
)
from simorgh.apps.app_settings.services import invalidate, resolve, set_value
from simorgh.apps.audit.diff import diff_dicts, serialize_instance
from simorgh.apps.audit.models import AuditLog
from simorgh.apps.events.bus import (
    EventError,
    clear_subscribers,
    dispatch,
    register_event,
    subscribe,
)
from simorgh.apps.notifications.channels import get_backend, register_backend
from simorgh.apps.notifications.models import Notification, NotificationChannel
from simorgh.apps.notifications.services import dispatch as notify_dispatch
from simorgh.apps.notifications.templates import (
    NotificationTemplate,
    register_template,
)
from simorgh.apps.storage.providers import (
    LocalFileSystemProvider,
    StorageError,
    safe_tenant_path,
)
from simorgh.apps.storage.services import store_file
from simorgh.core.audit import record_event
from simorgh.core.context import RequestContext, use_request_context

# ---------------------------------------------------------------------------
# Audit
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_record_event_persists_to_db(alice, tenant_acme):
    record_event(
        "test.action",
        resource_type="test.thing",
        resource_id="42",
        after={"name": "x"},
        tenant_id=tenant_acme.pk,
        actor_id=alice.pk,
    )
    row = AuditLog.objects.get(action="test.action")
    assert row.resource_id == "42"
    assert row.tenant_id == tenant_acme.pk
    assert row.actor_id == alice.pk
    assert row.after == {"name": "x"}


def test_diff_dicts_returns_only_changed_keys():
    out = diff_dicts({"a": 1, "b": 2, "c": 3}, {"a": 1, "b": 20, "d": 4})
    assert out == {
        "b": {"before": 2, "after": 20},
        "c": {"before": 3, "after": None},
        "d": {"before": None, "after": 4},
    }


@pytest.mark.django_db
def test_serialize_instance_redacts_sensitive_fields(alice):
    snap = serialize_instance(alice)
    assert snap.get("password") == "***"


# ---------------------------------------------------------------------------
# Events
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _clear_events():
    clear_subscribers()
    yield
    clear_subscribers()


@pytest.mark.django_db
def test_event_dispatches_to_subscribers():
    register_event("test.fired", payload_keys=("id",))
    seen: list[dict] = []

    @subscribe("test.fired")
    def _handler(payload):
        seen.append(payload)

    dispatch("test.fired", {"id": 7})
    assert seen == [{"id": 7}]


@pytest.mark.django_db
def test_event_dispatch_isolates_handler_failures():
    register_event("test.flaky")
    calls: list[str] = []

    @subscribe("test.flaky")
    def _bad(_payload):
        raise RuntimeError("boom")

    @subscribe("test.flaky")
    def _good(_payload):
        calls.append("ok")

    dispatch("test.flaky", {})
    assert calls == ["ok"]


def test_event_dispatch_rejects_unknown_name():
    with pytest.raises(EventError):
        dispatch("never.registered")


def test_event_dispatch_validates_payload_keys():
    register_event("test.with_keys", payload_keys=("required",))
    with pytest.raises(EventError):
        dispatch("test.with_keys", {})


# ---------------------------------------------------------------------------
# Notifications
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_notification_dispatch_renders_and_persists(alice, tenant_acme, acme_tree):
    register_template(
        NotificationTemplate(
            kind="test.hello",
            title_key="Hello {name}",
            body_key="Welcome {name}!",
            default_channels=("inbox",),
        )
    )
    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(),
        permissions=frozenset(),
        org_node_ids=frozenset({acme_tree["eu"].pk}),
    )
    with use_request_context(ctx):
        notify_dispatch("test.hello", recipients=[alice], context={"name": "Alice"})

    row = Notification.objects.get(recipient=alice, kind="test.hello")
    assert row.title == "Hello Alice"
    assert row.channel == NotificationChannel.INBOX
    assert row.delivered_at is not None  # InboxChannel sets it


@pytest.mark.django_db
def test_custom_channel_can_be_registered():
    class _Counter:
        name = "counter"
        count = 0

        def send(self, _notification):
            type(self).count += 1

    backend = _Counter()
    register_backend(backend)
    assert get_backend("counter") is backend


# ---------------------------------------------------------------------------
# Storage
# ---------------------------------------------------------------------------


def test_local_provider_round_trip(tmp_path):
    provider = LocalFileSystemProvider(root=tmp_path)
    provider.save("a/b.txt", io.BytesIO(b"hello"))
    assert provider.exists("a/b.txt")
    with provider.open("a/b.txt") as fh:
        assert fh.read() == b"hello"
    assert provider.sha256("a/b.txt") == (
        "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
    )
    provider.delete("a/b.txt")
    assert not provider.exists("a/b.txt")


def test_local_provider_blocks_traversal(tmp_path):
    provider = LocalFileSystemProvider(root=tmp_path)
    with pytest.raises(StorageError):
        provider.save("../outside.txt", io.BytesIO(b"x"))


def test_safe_tenant_path_rejects_dotfiles():
    with pytest.raises(StorageError):
        safe_tenant_path(1, ".env")


@pytest.mark.django_db
def test_store_file_persists_metadata_and_payload(
    alice, tenant_acme, acme_tree, tmp_path, settings
):
    settings.STORAGE_ROOT = tmp_path
    from simorgh.apps.storage import providers as _p

    _p.reset_default_provider()
    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(),
        permissions=frozenset(),
        org_node_ids=frozenset({acme_tree["eu"].pk}),
    )
    with use_request_context(ctx):
        meta = store_file(
            filename="report.txt",
            content=io.BytesIO(b"payload"),
            content_type="text/plain",
        )

    assert meta.size_bytes == 7
    assert meta.tenant_id == tenant_acme.pk
    assert meta.uploaded_by_id == alice.pk
    assert (tmp_path / meta.path).exists()


# ---------------------------------------------------------------------------
# Settings hierarchy
# ---------------------------------------------------------------------------


@pytest.fixture
def _settings_registry():
    reset_registry_for_tests()
    invalidate()
    yield
    reset_registry_for_tests()
    invalidate()


@pytest.mark.django_db
def test_setting_resolution_walks_specificity(_settings_registry, tenant_acme, alice):
    register_setting(
        SettingDefinition(
            key="ui.theme",
            type="string",
            default="light",
            label_key="settings.ui.theme",
        )
    )

    assert resolve("ui.theme") == "light"

    set_value("ui.theme", "dark", scope="tenant", tenant_id=tenant_acme.pk)
    invalidate("ui.theme")
    assert resolve("ui.theme", tenant_id=tenant_acme.pk) == "dark"

    set_value("ui.theme", "neon", scope="user", scope_id=str(alice.pk), tenant_id=tenant_acme.pk)
    invalidate("ui.theme")
    assert resolve("ui.theme", tenant_id=tenant_acme.pk, user_id=alice.pk) == "neon"


@pytest.mark.django_db
def test_setting_rejects_disallowed_scope(_settings_registry):
    register_setting(
        SettingDefinition(
            key="ops.rate_limit",
            type="int",
            default=100,
            label_key="settings.ops.rate_limit",
            scopes=("system", "tenant"),
        )
    )
    from simorgh.apps.app_settings.registry import SettingsError

    with pytest.raises(SettingsError):
        set_value("ops.rate_limit", 50, scope="user", scope_id="1")
