"""WS6 / FRREP-DCI012 — inbound mail-to-Repository ingestion + audit (FRREP-DCI017).

Mocks the ``imap_tools.MailMessage`` shape directly so the test never needs a
real IMAP server. The module under test is `apps.repository.integration.mail_ingest`.
"""

from __future__ import annotations

from types import SimpleNamespace

import pytest

from apps.repository.documents.models import (
    Collection,
    Document,
    EmailIngestEvent,
    EmailIngestMailbox,
)
from apps.repository.integration import mail_ingest


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


def _fake_msg(*, to, sender, subject, attachments, cc=None, body="", uid="UID-123"):
    """Build a duck-typed object that behaves like imap_tools.MailMessage."""
    return SimpleNamespace(
        to=tuple(to),
        cc=tuple(cc or ()),
        from_=sender,
        subject=subject,
        text=body,
        html="",
        attachments=[
            SimpleNamespace(filename=name, payload=payload)
            for name, payload in attachments
        ],
        uid=uid,
    )


@pytest.fixture
def inbox_collection(db, manager_user):
    col = Collection.objects.create(
        name="Inbox-enabled collection",
        slug="inbox-enabled",
        visibility=Collection.Visibility.PUBLIC_OPEN,
        email_ingest_enabled=True,
        email_ingest_address="repo+inbox@iilmp.org",
        email_ingest_allowed_senders="@ruforum.org\nvip@external.org",
    )
    col.managed_by.add(manager_user)
    return col


@pytest.fixture
def mailbox(db, inbox_collection):
    """Default EmailIngestMailbox row for tests."""
    return EmailIngestMailbox.objects.create(
        name="Test mailbox",
        imap_host="imap.test.local",
        imap_port=993,
        username="repo@test.local",
        default_collection=inbox_collection,
    )


# ── Sender allowlist (Collection method) ──────────────────────────────────


@pytest.mark.django_db
def test_allowed_sender_matches_explicit_address(inbox_collection):
    assert inbox_collection.email_ingest_sender_allowed("vip@external.org")


@pytest.mark.django_db
def test_allowed_sender_matches_domain_wildcard(inbox_collection):
    assert inbox_collection.email_ingest_sender_allowed("anyone@ruforum.org")


@pytest.mark.django_db
def test_disallowed_sender_returns_false(inbox_collection):
    assert not inbox_collection.email_ingest_sender_allowed("attacker@evil.com")


# ── per-message processing ────────────────────────────────────────────────


@pytest.mark.django_db
def test_process_message_ingests_attachments_for_matching_collection(
    mailbox, inbox_collection
):
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nfake-mail-test\n%%EOF\n"
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="A new submission",
        attachments=[("manuscript.pdf", pdf_payload)],
    )

    ingested, counters, status, doc, err = mail_ingest.process_message(msg, mailbox)

    assert ingested == 1
    assert counters["ingested"] == 1
    assert status == EmailIngestEvent.Status.INGESTED
    assert doc is not None
    assert doc.collection_id == inbox_collection.pk
    assert err == ""


@pytest.mark.django_db
def test_process_message_falls_back_to_mailbox_default_collection(mailbox):
    """When the recipient address doesn't match any Collection's
    email_ingest_address, the mailbox's default_collection is used."""
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nfake-mail-test\n%%EOF\n"
    msg = _fake_msg(
        to=["unknown@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="Misaddressed",
        attachments=[("manuscript.pdf", pdf_payload)],
    )

    ingested, counters, status, doc, err = mail_ingest.process_message(msg, mailbox)

    assert ingested == 1
    assert status == EmailIngestEvent.Status.INGESTED
    assert doc.collection_id == mailbox.default_collection_id


@pytest.mark.django_db
def test_process_message_rejects_disallowed_sender(mailbox):
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nfake-mail-test\n%%EOF\n"
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="attacker@evil.com",
        subject="Hostile submission",
        attachments=[("manuscript.pdf", pdf_payload)],
    )

    ingested, counters, status, doc, err = mail_ingest.process_message(msg, mailbox)

    assert ingested == 0
    assert counters["rejected_sender"] == 1
    assert status == EmailIngestEvent.Status.REJECTED_SENDER
    assert doc is None


@pytest.mark.django_db
def test_process_message_dedupes_via_existing_sha256(
    mailbox, inbox_collection, submitter_user
):
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\ndedupe-test\n%%EOF\n"
    msg1 = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="First send",
        attachments=[("paper.pdf", pdf_payload)],
        uid="UID-1",
    )
    msg2 = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="Resend",
        attachments=[("paper.pdf", pdf_payload)],
        uid="UID-2",
    )

    mail_ingest.process_message(msg1, mailbox)
    ingested, counters, status, _, _ = mail_ingest.process_message(msg2, mailbox)

    assert ingested == 0
    assert counters["skipped_dup"] == 1
    assert status == EmailIngestEvent.Status.SKIPPED_DUPLICATE


# ── parsing_rules (FRREP-DCI012) ───────────────────────────────────────────


@pytest.mark.django_db
def test_default_parsing_rules_use_subject_as_title_and_body_as_abstract(mailbox):
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nrules-default\n%%EOF\n"
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="My Subject Line",
        body="The body text.",
        attachments=[("manuscript.pdf", pdf_payload)],
    )

    _, _, _, doc, _ = mail_ingest.process_message(msg, mailbox)

    assert doc.title == "My Subject Line"
    assert doc.abstract == "The body text."


@pytest.mark.django_db
def test_parsing_rules_title_from_filename(mailbox):
    """mailbox.parsing_rules={"title_from": "filename"} must actually change
    the ingested title — previously this admin-configurable JSONField was
    stored but never read (FRREP-DCI012)."""
    mailbox.parsing_rules = {"title_from": "filename"}
    mailbox.save(update_fields=["parsing_rules"])
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nrules-filename\n%%EOF\n"
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="Ignored subject",
        attachments=[("Annual_Report-2026.pdf", pdf_payload)],
    )

    _, _, _, doc, _ = mail_ingest.process_message(msg, mailbox)

    assert doc.title == "Annual Report 2026"


@pytest.mark.django_db
def test_parsing_rules_abstract_from_none_leaves_abstract_blank(mailbox):
    mailbox.parsing_rules = {"abstract_from": "none"}
    mailbox.save(update_fields=["parsing_rules"])
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nrules-noabs\n%%EOF\n"
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="Subject only",
        body="This body should be ignored.",
        attachments=[("paper.pdf", pdf_payload)],
    )

    _, _, _, doc, _ = mail_ingest.process_message(msg, mailbox)

    assert doc.abstract == ""


@pytest.mark.django_db
def test_parsing_rules_abstract_from_subject(mailbox):
    mailbox.parsing_rules = {"abstract_from": "subject"}
    mailbox.save(update_fields=["parsing_rules"])
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nrules-abs-subj\n%%EOF\n"
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="Use me as abstract too",
        body="This body should be ignored.",
        attachments=[("paper.pdf", pdf_payload)],
    )

    _, _, _, doc, _ = mail_ingest.process_message(msg, mailbox)

    assert doc.abstract == "Use me as abstract too"


# ── multi-mailbox + audit log (FRREP-DCI017) ──────────────────────────────


@pytest.mark.django_db
def test_poll_inbox_iterates_multiple_mailboxes(monkeypatch, inbox_collection):
    box_a = EmailIngestMailbox.objects.create(
        name="A", imap_host="a.test", username="a@test",
        default_collection=inbox_collection,
    )
    box_b = EmailIngestMailbox.objects.create(
        name="B", imap_host="b.test", username="b@test",
        default_collection=inbox_collection,
    )
    polled = []

    class _FakeMailBox:
        def __init__(self, mailbox):
            self.mailbox = mailbox

        def fetch(self, *args, **kwargs):
            polled.append(self.mailbox.name)
            return []

        def logout(self):
            pass

    from contextlib import contextmanager as _ctx

    @_ctx
    def _fake_connect(mailbox):
        yield _FakeMailBox(mailbox)

    monkeypatch.setattr(mail_ingest, "_connect_mailbox", _fake_connect)

    summary = mail_ingest.poll_inbox()
    assert summary.mailboxes == 2
    assert sorted(polled) == ["A", "B"]


@pytest.mark.django_db
def test_poll_inbox_records_event_per_message_and_is_idempotent(
    monkeypatch, mailbox, inbox_collection
):
    """FRREP-DCI017 audit-trail mandate: each processed message gets a
    persisted EmailIngestEvent. Re-polling the same UID is a no-op."""
    pdf_payload = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\nidemp-test\n%%EOF\n"
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="Subject A",
        attachments=[("paper.pdf", pdf_payload)],
        uid="UID-IDEMP",
    )

    class _MB:
        def __init__(self):
            self.moved = []

        def fetch(self, *args, **kwargs):
            return [msg]

        def move(self, uid, target):
            self.moved.append((uid, target))

        def flag(self, *a, **k):
            pass

        def logout(self):
            pass

    fake = _MB()
    from contextlib import contextmanager as _ctx

    @_ctx
    def _fake_connect(_mailbox):
        yield fake

    monkeypatch.setattr(mail_ingest, "_connect_mailbox", _fake_connect)

    summary = mail_ingest.poll_inbox()
    assert summary.ingested == 1
    assert EmailIngestEvent.objects.filter(
        mailbox=mailbox, message_uid="UID-IDEMP",
        status=EmailIngestEvent.Status.INGESTED,
    ).count() == 1
    assert fake.moved == [("UID-IDEMP", mailbox.processed_folder)]

    # Second poll — same UID, already INGESTED → skipped, no duplicate event.
    summary2 = mail_ingest.poll_inbox()
    assert summary2.polled == 1  # still counted as polled
    assert EmailIngestEvent.objects.filter(
        mailbox=mailbox, message_uid="UID-IDEMP",
    ).count() == 1


@pytest.mark.django_db
def test_poll_inbox_logs_validation_failure_as_event(monkeypatch, mailbox):
    """An attachment that fails magic-byte validation is recorded as a
    REJECTED_VALIDATION event with an error message."""
    msg = _fake_msg(
        to=["repo+inbox@iilmp.org"],
        sender="submitter@ruforum.org",
        subject="Bogus",
        attachments=[("trojan.pdf", b"\x7fELF" + b"\x00" * 4096)],
        uid="UID-INVALID",
    )

    class _MB:
        def fetch(self, *a, **k):
            return [msg]

        def move(self, *a, **k):
            pass

        def flag(self, *a, **k):
            pass

        def logout(self):
            pass

    from contextlib import contextmanager as _ctx

    @_ctx
    def _fake_connect(_mailbox):
        yield _MB()

    monkeypatch.setattr(mail_ingest, "_connect_mailbox", _fake_connect)

    mail_ingest.poll_inbox()
    event = EmailIngestEvent.objects.get(mailbox=mailbox, message_uid="UID-INVALID")
    assert event.status == EmailIngestEvent.Status.REJECTED_VALIDATION
    assert event.error_message != ""
    assert event.document is None


@pytest.mark.django_db
def test_disabled_mailbox_not_polled(monkeypatch, inbox_collection):
    EmailIngestMailbox.objects.create(
        name="Disabled", imap_host="x.test", username="x@test",
        default_collection=inbox_collection, is_enabled=False,
    )
    polled = []

    from contextlib import contextmanager as _ctx

    @_ctx
    def _fake_connect(mailbox):
        polled.append(mailbox.name)
        yield None

    monkeypatch.setattr(mail_ingest, "_connect_mailbox", _fake_connect)

    summary = mail_ingest.poll_inbox()
    assert summary.mailboxes == 0
    assert polled == []


@pytest.mark.django_db
def test_env_default_mailbox_auto_created_when_settings_present(settings, inbox_collection):
    settings.REPO_INBOUND_IMAP_HOST = "imap.env.local"
    settings.REPO_INBOUND_IMAP_PORT = 993
    settings.REPO_INBOUND_IMAP_USER = "env@test"
    settings.REPO_INBOUND_IMAP_PASSWORD = "secret"

    assert EmailIngestMailbox.objects.count() == 0
    mail_ingest._ensure_env_mailbox()
    assert EmailIngestMailbox.objects.filter(name="Default (env)").exists()
    bootstrap = EmailIngestMailbox.objects.get(name="Default (env)")
    assert bootstrap.imap_host == "imap.env.local"
    # Password is stored signed but property round-trips it.
    assert bootstrap.password == "secret"


@pytest.mark.django_db
def test_env_default_mailbox_skipped_when_mailbox_already_exists(
    settings, mailbox
):
    settings.REPO_INBOUND_IMAP_HOST = "imap.env.local"
    before = EmailIngestMailbox.objects.count()
    mail_ingest._ensure_env_mailbox()
    assert EmailIngestMailbox.objects.count() == before  # no bootstrap row added


@pytest.mark.django_db
def test_password_property_round_trip(inbox_collection):
    box = EmailIngestMailbox.objects.create(
        name="X", imap_host="x", username="u",
        default_collection=inbox_collection,
    )
    box.password = "p@ssw0rd!"
    box.save(update_fields=["password_signed"])
    box.refresh_from_db()
    assert box.password == "p@ssw0rd!"
    assert box.password_signed is not None
    assert b"p@ssw0rd!" not in bytes(box.password_signed)  # actually signed, not raw


# ── poll_inbox guard ──────────────────────────────────────────────────────


def test_poll_inbox_no_op_when_no_mailboxes_and_no_env(settings, db):
    settings.REPO_INBOUND_IMAP_HOST = ""
    summary = mail_ingest.poll_inbox()
    assert summary.polled == 0
    assert summary.mailboxes == 0
