"""Tests for report generation, rendering, distribution, and publish lifecycle."""
from __future__ import annotations

from datetime import date
from decimal import Decimal
from io import BytesIO

import pytest
from django.contrib.auth import get_user_model

from apps.mel.indicators.models import (
    Indicator,
    IndicatorFrequency,
    IndicatorTarget,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
)
from apps.mel.indicators.services import record_data_point
from apps.mel.reports.models import (
    ArtifactFormat,
    ComplianceAlert,
    Report,
    ReportStatus,
    ReportTemplate,
)
from apps.mel.reports.services import (
    distribute_report,
    generate_report,
    publish_report,
    render_html,
    render_xlsx,
    run_compliance_checks,
)

pytestmark = pytest.mark.django_db(transaction=True)

User = get_user_model()


def _seed(slug: str):
    officer = User.objects.create_user(email=f"officer-{slug}@example.com", password="x")
    lf = LogFrame.objects.create(name=f"Report LF {slug}", slug=f"report-lf-{slug}", owner=officer)
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    outcome = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.OUTCOME, title="O", parent=impact)
    output = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.OUTPUT, title="P", parent=outcome)
    ind = Indicator.objects.create(
        logframe_row=output,
        code=f"report-ind-{slug}",
        name="Indicator",
        data_source="m",
        unit="count",
        calculation_method="count",
        frequency=IndicatorFrequency.MONTHLY,
    )
    IndicatorTarget.objects.create(
        indicator=ind,
        period_label="2026-04",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        baseline_value=Decimal("0"),
        target_value=Decimal("100"),
    )
    tpl = ReportTemplate.objects.create(
        name=f"Quarterly {slug}",
        slug=f"quarterly-{slug}",
        logframe=lf,
        sections=["indicator_rollup", "variance_table", "compliance"],
    )
    return officer, lf, ind, tpl


def test_generate_report_snapshots_indicator_rollup():
    officer, lf, ind, tpl = _seed("snapshot")
    record_data_point(
        indicator=ind,
        value=90,
        period_label="2026-04",
        auto_verify=True,
        source_module="m",
        source_event="e",
        source_object_id="1",
    )

    report = generate_report(
        template=tpl,
        period_label="2026-04",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        user=officer,
    )

    assert report.status == ReportStatus.DRAFT
    assert report.consolidated_data["indicator_rollup"]["counts"]["green"] == 1
    assert report.generated_by_id == officer.pk


def test_generate_report_is_idempotent_per_period():
    officer, lf, ind, tpl = _seed("idempotent")
    kwargs = dict(
        template=tpl,
        period_label="2026-04",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        user=officer,
    )
    r1 = generate_report(**kwargs)
    r2 = generate_report(**kwargs)

    assert r1.pk == r2.pk
    assert Report.objects.filter(template=tpl, period_label="2026-04").count() == 1


def test_run_compliance_checks_creates_red_alert():
    officer, lf, ind, tpl = _seed("compliance")
    record_data_point(
        indicator=ind,
        value=20,  # 20% → RED
        period_label="2026-04",
        auto_verify=True,
        source_module="m",
        source_event="e",
        source_object_id="1",
    )
    report = generate_report(
        template=tpl,
        period_label="2026-04",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        user=officer,
    )
    # generate_report already ran the checks; a second call is a no-op thanks to get_or_create.
    created_again = run_compliance_checks(report)

    alerts = ComplianceAlert.objects.filter(report=report)
    assert alerts.count() == 1
    assert alerts.first().code == f"indicator.red.{ind.code}"
    assert alerts.first().severity == "high"
    assert created_again == []


def test_render_html_and_xlsx_produce_artifacts():
    officer, lf, ind, tpl = _seed("render")
    record_data_point(
        indicator=ind, value=80, period_label="2026-04", auto_verify=True,
        source_module="m", source_event="e", source_object_id="1",
    )
    report = generate_report(
        template=tpl, period_label="2026-04",
        period_start=date(2026, 4, 1), period_end=date(2026, 4, 30),
    )

    html = render_html(report)
    assert "Indicator rollup" in html
    assert tpl.name in html

    xlsx_art = render_xlsx(report)
    assert xlsx_art.format == ArtifactFormat.XLSX
    assert xlsx_art.size_bytes > 0

    from openpyxl import load_workbook
    wb = load_workbook(BytesIO(xlsx_art.file.read()))
    assert "Indicators" in wb.sheetnames
    assert "Variance" in wb.sheetnames
    # Header row present.
    assert wb["Indicators"]["A1"].value == "Code"


def test_publish_report_fires_mel_report_published_signal():
    from apps.mel.reports.signals import mel_report_published

    captured: list[int] = []

    def handler(sender, report, **kwargs):
        captured.append(report.pk)

    mel_report_published.connect(handler, dispatch_uid="test.publish.handler")
    try:
        officer, lf, ind, tpl = _seed("publish")
        record_data_point(
            indicator=ind, value=90, period_label="2026-04", auto_verify=True,
            source_module="m", source_event="e", source_object_id="1",
        )
        report = generate_report(
            template=tpl, period_label="2026-04",
            period_start=date(2026, 4, 1), period_end=date(2026, 4, 30),
        )
        publish_report(report)

        assert captured == [report.pk]
        report.refresh_from_db()
        assert report.status == ReportStatus.PUBLISHED
        assert report.published_at is not None
    finally:
        mel_report_published.disconnect(dispatch_uid="test.publish.handler")


def test_distribute_report_writes_distribution_rows():
    officer, lf, ind, tpl = _seed("distribute")
    recipient = User.objects.create_user(email="recipient@example.com", password="x")
    tpl.default_recipients.add(recipient)

    report = generate_report(
        template=tpl, period_label="2026-04",
        period_start=date(2026, 4, 1), period_end=date(2026, 4, 30),
    )
    queued = distribute_report(report, extra_emails=["external@example.com"])

    assert queued == 2
    assert report.distributions.filter(recipient_user=recipient).exists()
    assert report.distributions.filter(recipient_email="external@example.com").exists()


# ---------------------------------------------------------------------------
# G14 — Read-only lock on PUBLISHED reports
# ---------------------------------------------------------------------------


def _publish(slug: str):
    officer, lf, ind, tpl = _seed(slug)
    record_data_point(
        indicator=ind, value=90, period_label="2026-04", auto_verify=True,
        source_module="m", source_event="e", source_object_id="1",
    )
    report = generate_report(
        template=tpl, period_label="2026-04",
        period_start=date(2026, 4, 1), period_end=date(2026, 4, 30),
    )
    publish_report(report)
    return officer, report


def test_published_report_narrative_edit_is_blocked():
    from django.core.exceptions import ValidationError

    _, report = _publish("g14-narrative")
    report.refresh_from_db()
    report.narrative = "after-publish edit"
    with pytest.raises(ValidationError) as excinfo:
        report.save()
    assert "published report" in str(excinfo.value).lower()


def test_published_report_share_token_rotation_allowed():
    _, report = _publish("g14-share")
    report.refresh_from_db()
    report.share_token = Report.generate_share_token()
    report.save()  # must not raise — share_token is the carve-out
    report.refresh_from_db()
    assert report.share_token


def test_unpublish_report_logs_audit_row():
    from apps.core.audit.models import AuditLog
    from apps.mel.reports.services import unpublish_report

    officer, report = _publish("g14-unpublish")
    unpublish_report(report, user=officer, reason="baseline corrected")
    report.refresh_from_db()
    assert report.status == ReportStatus.APPROVED
    assert AuditLog.objects.filter(
        target_model="Report", object_id=str(report.pk), action="UNPUBLISH",
    ).exists()
    # Once unpublished, narrative is editable again.
    report.narrative = "fresh draft"
    report.save()
    report.refresh_from_db()
    assert report.narrative == "fresh draft"


def test_unpublish_report_requires_reason():
    from django.core.exceptions import ValidationError
    from apps.mel.reports.services import unpublish_report

    officer, report = _publish("g14-reason-required")
    with pytest.raises(ValidationError):
        unpublish_report(report, user=officer, reason="")


def test_published_artifact_edit_is_blocked():
    from django.core.exceptions import ValidationError

    from apps.mel.reports.models import ArtifactFormat as _AF
    from apps.mel.reports.models import ReportArtifact

    officer, lf, ind, tpl = _seed("g14-artifact")
    record_data_point(
        indicator=ind, value=80, period_label="2026-04", auto_verify=True,
        source_module="m", source_event="e", source_object_id="1",
    )
    report = generate_report(
        template=tpl, period_label="2026-04",
        period_start=date(2026, 4, 1), period_end=date(2026, 4, 30),
    )
    # Create the artefact BEFORE publish so the lock doesn't fire on creation.
    art = ReportArtifact.objects.create(
        report=report, format=_AF.HTML, checksum="abc", size_bytes=1,
    )
    publish_report(report)
    art.refresh_from_db()
    art.size_bytes = 999
    with pytest.raises(ValidationError):
        art.save()


# ---------------------------------------------------------------------------
# G15 — Run-now + subscribers
# ---------------------------------------------------------------------------


def test_run_now_distributes_to_template_and_extra_emails():
    """G15 — distribute_report should honour both template recipients and extras."""
    officer, lf, ind, tpl = _seed("g15-distribute")
    sub = User.objects.create_user(email="sub@example.com", password="x")
    tpl.default_recipients.add(sub)
    record_data_point(
        indicator=ind, value=70, period_label="2026-04", auto_verify=True,
        source_module="m", source_event="e", source_object_id="1",
    )
    report = generate_report(
        template=tpl, period_label="2026-04",
        period_start=date(2026, 4, 1), period_end=date(2026, 4, 30),
    )
    queued = distribute_report(report, extra_emails=["donor@example.com", "board@example.com"])

    assert queued == 3
    assert report.distributions.filter(recipient_user=sub).exists()
    assert report.distributions.filter(recipient_email="donor@example.com").exists()
    assert report.distributions.filter(recipient_email="board@example.com").exists()


# ---------------------------------------------------------------------------
# G12 — ComplianceAlert UI resolve
# ---------------------------------------------------------------------------


def test_compliance_alert_resolve_appends_reason_and_sets_resolver():
    from django.test import RequestFactory

    from apps.mel.reports.models import ComplianceAlert, ComplianceSeverity
    from apps.mel.reports.views import ComplianceAlertResolveView

    officer = User.objects.create_user(email="g12-resolver@example.com", password="x")
    officer.is_superuser = True
    officer.is_staff = True
    officer.role = "system_admin"
    officer.save()
    alert = ComplianceAlert.objects.create(
        code="indicator.red.x",
        severity=ComplianceSeverity.HIGH,
        message="Original RED message.",
    )

    rf = RequestFactory()
    req = rf.post(
        f"/mel/reports/compliance-alerts/{alert.pk}/resolve/",
        {"reason": "data corrected upstream"},
    )
    req.user = officer
    setattr(req, "session", "session")
    # Bypass message framework by attaching a minimal storage.
    from django.contrib.messages.storage.fallback import FallbackStorage
    req._messages = FallbackStorage(req)

    response = ComplianceAlertResolveView.as_view()(req, pk=alert.pk)
    alert.refresh_from_db()

    assert response.status_code in (302, 303)
    assert alert.resolved is True
    assert alert.resolved_by_id == officer.pk
    # M&E SRS Table 66 — audit records shall not be modified: the original
    # message stays untouched; the reason lives in resolution_note.
    assert alert.message == "Original RED message."
    assert alert.resolution_note == "data corrected upstream"


def test_report_finalize_requires_variance_for_below_target():
    from apps.mel.indicators.models import DataPoint, IndicatorVariance
    from apps.mel.reports.services import generate_report, report_publish_blockers

    officer, lf, ind, tpl = _seed("var-gate")
    # 10 / 100 target → RED (below target).
    DataPoint.objects.create(
        indicator=ind, period_label="2026-04", value=Decimal("10"),
        status=DataPoint.Status.VERIFIED,
    )
    report = generate_report(
        template=tpl, period_label="2026-04",
        period_start=date(2026, 4, 1), period_end=date(2026, 4, 30), user=officer,
    )
    report.narrative = "Quarterly performance summary."
    report.save(update_fields=["narrative"])

    blockers = report_publish_blockers(report, require_narrative=True)
    assert any("below-target" in b for b in blockers)

    IndicatorVariance.objects.create(
        indicator=ind, period_label="2026-04",
        cause_analysis="Field work delayed by weather.",
        adjustment="Reschedule the missed training sessions.",
    )
    cleared = report_publish_blockers(report, require_narrative=True)
    assert not any("below-target" in b for b in cleared)


def _resolve_alert_as(role, *, code, severity, is_super=False):
    """Drive ComplianceAlertResolveView as a user with `role`; return (alert, msgs, resp)."""
    from django.contrib.messages.storage.fallback import FallbackStorage
    from django.test import RequestFactory

    from apps.mel.reports.models import ComplianceAlert
    from apps.mel.reports.views import ComplianceAlertResolveView

    u = User.objects.create_user(email=f"pe1-{role}-{code}@example.com", password="x")
    u.role = role
    u.is_superuser = is_super
    u.save()
    alert = ComplianceAlert.objects.create(code=code, severity=severity, message="Budget overrun.")
    req = RequestFactory().post(
        f"/mel/reports/compliance-alerts/{alert.pk}/resolve/", {"reason": "reviewed"}
    )
    req.user = u
    setattr(req, "session", "session")
    req._messages = FallbackStorage(req)
    resp = ComplianceAlertResolveView.as_view()(req, pk=alert.pk)
    alert.refresh_from_db()
    msgs = " ".join(str(m) for m in req._messages)
    return alert, msgs, resp


def test_budget_alert_resolution_blocked_for_officer():
    from apps.mel.reports.models import ComplianceSeverity

    alert, msgs, _ = _resolve_alert_as(
        "mel_officer", code="budget.overrun.x", severity=ComplianceSeverity.CRITICAL
    )
    assert alert.resolved is False
    assert "management approval" in msgs.lower()


def test_budget_alert_resolution_allowed_for_manager():
    from apps.mel.reports.models import ComplianceSeverity

    alert, msgs, _ = _resolve_alert_as(
        "program_manager", code="budget.overrun.y", severity=ComplianceSeverity.CRITICAL
    )
    assert alert.resolved is True
    assert alert.resolved_by is not None
