"""WS3 (reports) browser-QA fix sweep — 2026-07-11.

Covers defects C-D2/D3/D4/D5/D6/D7/D8, E-3 (action_url), and the P3 tail
(C-D9/D10/D11, zero-recipient toast).
"""
from __future__ import annotations

from datetime import date
from decimal import Decimal

import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse

from apps.core.permissions.roles import UserRole
from apps.mel.indicators.models import (
    Indicator,
    IndicatorFrequency,
    IndicatorTarget,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
)
from apps.mel.reports.builders import (
    build_rims_funding_summary,
    build_smehub_learning_report,
)
from apps.mel.reports.models import (
    Report,
    ReportSection,
    ReportStatus,
    ReportTemplate,
)
from apps.mel.reports.services import (
    distribute_report,
    generate_report,
    mel_recipient_users,
    publish_report,
    render_all,
)

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

User = get_user_model()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _officer(slug: str):
    return User.objects.create_user(
        email=f"officer-{slug}@example.com", password="x", role=UserRole.MEL_OFFICER,
    )


def _lf_with_indicators(slug: str, n: int = 5):
    owner = _officer(f"own-{slug}")
    lf = LogFrame.objects.create(name=f"LF {slug}", slug=f"lf-{slug}", owner=owner)
    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,
    )
    inds = [
        Indicator.objects.create(
            logframe_row=output,
            code=f"{slug}-ind-{i}",
            name=f"Ind {i}",
            data_source="m",
            unit="count",
            calculation_method="count",
            frequency=IndicatorFrequency.MONTHLY,
        )
        for i in range(n)
    ]
    return owner, lf, output, inds


def _template(slug: str, lf, sections):
    return ReportTemplate.objects.create(
        name=f"T {slug}", slug=f"t-{slug}", logframe=lf, sections=sections,
    )


def _generate(tpl, owner, label="2026-04", narrative=""):
    return generate_report(
        template=tpl,
        period_label=label,
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        user=owner,
        narrative=narrative,
    )


# ---------------------------------------------------------------------------
# C-D3 — builder per-section indicator_codes honoured at generation
# ---------------------------------------------------------------------------


def test_builder_indicator_codes_limit_rollup():
    owner, lf, output, inds = _lf_with_indicators("d3", n=5)
    tpl = _template("d3", lf, ["indicator_rollup"])
    chosen = [inds[0].code, inds[2].code, inds[4].code]
    ReportSection.objects.create(
        template=tpl, ordinal=0, kind="indicator_rollup", title="Rollup",
        config={"indicator_codes": chosen},
    )

    report = _generate(tpl, owner)
    rows = report.consolidated_data["indicator_rollup"]["rows"]

    assert sorted(r["code"] for r in rows) == sorted(chosen)
    assert len(rows) == 3


def test_rollup_without_config_shows_all_indicators():
    owner, lf, output, inds = _lf_with_indicators("d3b", n=5)
    tpl = _template("d3b", lf, ["indicator_rollup"])

    report = _generate(tpl, owner)
    rows = report.consolidated_data["indicator_rollup"]["rows"]

    assert len(rows) == 5


# ---------------------------------------------------------------------------
# C-D2 — narrative edit on a PUBLISHED report is blocked (no 500)
# ---------------------------------------------------------------------------


def test_published_narrative_edit_redirects_without_500(client):
    owner, lf, output, inds = _lf_with_indicators("d2", n=1)
    tpl = _template("d2", lf, ["indicator_rollup"])
    report = _generate(tpl, owner, narrative="original narrative")
    publish_report(report)
    assert Report.objects.get(pk=report.pk).status == ReportStatus.PUBLISHED

    officer = _officer("d2")
    client.force_login(officer)
    url = reverse("mel_reports:report_narrative", kwargs={"pk": report.pk})

    resp = client.post(url, {"narrative": "hacked edit"})

    assert resp.status_code == 302
    assert Report.objects.get(pk=report.pk).narrative == "original narrative"


def test_published_report_detail_hides_edit_narrative_button(client):
    owner, lf, output, inds = _lf_with_indicators("d2c", n=1)
    tpl = _template("d2c", lf, ["indicator_rollup"])
    report = _generate(tpl, owner)
    publish_report(report)

    officer = _officer("d2c")
    client.force_login(officer)
    resp = client.get(reverse("mel_reports:report_detail", kwargs={"pk": report.pk}))
    body = resp.content.decode()

    assert resp.status_code == 200
    assert "Edit narrative" not in body


# ---------------------------------------------------------------------------
# C-D4 — generate form offers a period-label datalist
# ---------------------------------------------------------------------------


def test_generate_form_populates_period_datalist(client):
    owner, lf, output, inds = _lf_with_indicators("d4", n=1)
    _template("d4", lf, ["indicator_rollup"])
    IndicatorTarget.objects.create(
        indicator=inds[0],
        period_label="2026-04",
        period_start=date(2026, 4, 1),
        period_end=date(2026, 4, 30),
        baseline_value=Decimal("0"),
        target_value=Decimal("100"),
    )

    officer = _officer("d4")
    client.force_login(officer)
    resp = client.get(reverse("mel_reports:report_generate"))
    body = resp.content.decode()

    assert resp.status_code == 200
    assert 'id="report-period-options"' in body
    assert "2026-04" in body


# ---------------------------------------------------------------------------
# C-D5 — recipient picker scoped to MEL-relevant/staff users; 0-save warns
# ---------------------------------------------------------------------------


def test_mel_recipient_users_scopes_to_relevant_roles():
    officer = _officer("recip")
    staff = User.objects.create_user(email="staff-recip@example.com", password="x", is_staff=True)
    outsider = User.objects.create_user(
        email="learner-recip@example.com", password="x", role=UserRole.LEARNER,
    )

    ids = set(mel_recipient_users().values_list("pk", flat=True))

    assert officer.pk in ids
    assert staff.pk in ids
    assert outsider.pk not in ids


def test_schedule_recipients_select_is_searchable(client):
    owner, lf, output, inds = _lf_with_indicators("d5", n=1)
    _template("d5", lf, ["indicator_rollup"])
    officer = _officer("d5")
    client.force_login(officer)

    resp = client.get(reverse("mel_reports:scheduled_reports"))
    body = resp.content.decode()

    assert resp.status_code == 200
    # data-s2 marks the recipients select searchable; init partial is included.
    assert "data-s2" in body
    assert "select2" in body.lower()


def test_schedule_create_with_no_recipients_warns(client):
    owner, lf, output, inds = _lf_with_indicators("d5b", n=1)
    tpl = _template("d5b", lf, ["indicator_rollup"])
    officer = _officer("d5b")
    client.force_login(officer)

    resp = client.post(
        reverse("mel_reports:scheduled_report_create"),
        {"template": tpl.pk, "frequency": "quarterly"},
        follow=True,
    )
    msgs = " ".join(str(m) for m in resp.context["messages"])

    assert "no recipients" in msgs.lower()


# ---------------------------------------------------------------------------
# C-D6 — learning report honours a geographic-scope parameter
# ---------------------------------------------------------------------------


def _tracking_record(slug: str, country: str, sector: str = "Agribusiness"):
    from apps.mel.tracking.models import SMEHubTrackingRecord
    from apps.smehub.onboarding.models import (
        Business,
        BusinessStage,
        EntrepreneurProfile,
    )

    user = User.objects.create_user(email=f"ent-{slug}@example.com", password="x")
    ep = EntrepreneurProfile.objects.create(user=user)
    biz = Business.objects.create(
        entrepreneur=ep,
        business_id=f"BIZ-{slug}",
        name=f"Biz {slug}",
        sector=sector,
        business_stage=BusinessStage.GROWTH,
        country=country,
    )
    return SMEHubTrackingRecord.objects.create(
        entrepreneur=ep, business=biz, sp2_milestones=[{"m": 1}],
    )


def test_learning_report_geographic_scope_narrows_output():
    _tracking_record("ug", "Uganda")
    _tracking_record("ke", "Kenya")

    full = build_smehub_learning_report()
    total_full = sum(r["records"] for r in full["sector_performance"])

    scoped = build_smehub_learning_report(geographic_scope="Uganda")
    total_scoped = sum(r["records"] for r in scoped["sector_performance"])

    assert total_full == 2
    assert total_scoped == 1
    assert scoped["params"]["geographic_scope"] == "Uganda"


def test_learning_report_view_accepts_params(client):
    officer = _officer("d6view")
    client.force_login(officer)

    resp = client.get(
        reverse("mel_reports:smehub_learning"),
        {"programme": "Agri", "geographic_scope": "Uganda", "period_start": "2026-01-01"},
    )

    assert resp.status_code == 200
    body = resp.content.decode()
    assert 'name="programme"' in body
    assert 'name="geographic_scope"' in body


# ---------------------------------------------------------------------------
# C-D7 — published report surfaces the review feedback link
# ---------------------------------------------------------------------------


def test_published_report_shows_feedback_link(client):
    owner, lf, output, inds = _lf_with_indicators("d7", n=1)
    tpl = _template("d7", lf, ["indicator_rollup"])
    report = _generate(tpl, owner)
    publish_report(report)

    from apps.mel.feedback.services import ensure_report_review_channel

    channel = ensure_report_review_channel(report)

    officer = _officer("d7")
    client.force_login(officer)
    resp = client.get(reverse("mel_reports:report_detail", kwargs={"pk": report.pk}))
    body = resp.content.decode()

    assert resp.status_code == 200
    assert channel.token in body
    assert reverse("mel_feedback:public_submit", kwargs={"token": channel.token}) in body


# ---------------------------------------------------------------------------
# C-D8 — step-wise transition buttons drive the FSM
# ---------------------------------------------------------------------------


def test_report_detail_renders_transition_buttons(client):
    owner, lf, output, inds = _lf_with_indicators("d8", n=1)
    tpl = _template("d8", lf, ["indicator_rollup"])
    report = _generate(tpl, owner)  # DRAFT

    officer = _officer("d8")
    client.force_login(officer)
    resp = client.get(reverse("mel_reports:report_detail", kwargs={"pk": report.pk}))
    body = resp.content.decode()

    assert resp.status_code == 200
    assert 'name="status"' in body
    assert 'value="in_review"' in body


def test_transition_endpoint_drives_fsm(client):
    owner, lf, output, inds = _lf_with_indicators("d8b", n=1)
    tpl = _template("d8b", lf, ["indicator_rollup"])
    report = _generate(tpl, owner)  # DRAFT

    officer = _officer("d8b")
    client.force_login(officer)
    resp = client.post(
        reverse("mel_reports:report_transition", kwargs={"pk": report.pk}),
        {"status": "in_review"},
    )

    assert resp.status_code == 302
    assert Report.objects.get(pk=report.pk).status == ReportStatus.IN_REVIEW


# ---------------------------------------------------------------------------
# E-3 / #33 — published notification carries an action_url
# ---------------------------------------------------------------------------


def test_publish_notification_sets_action_url():
    from apps.core.notifications.models import Notification

    owner, lf, output, inds = _lf_with_indicators("e3", n=1)
    tpl = _template("e3", lf, ["indicator_rollup"])
    recipient = User.objects.create_user(email="donor-e3@example.com", password="x")
    tpl.default_recipients.add(recipient)
    report = _generate(tpl, owner)
    publish_report(report)

    distribute_report(report)

    assert Notification.objects.filter(
        recipient=recipient, action_url=f"/mel/reports/{report.pk}/",
    ).exists()


# ---------------------------------------------------------------------------
# C-D9 / #42 — GET schedules/new/ redirects instead of 405
# ---------------------------------------------------------------------------


def test_schedule_new_get_redirects_not_405(client):
    officer = _officer("d9")
    client.force_login(officer)

    resp = client.get(reverse("mel_reports:scheduled_report_create"))

    assert resp.status_code == 302
    assert resp.url == reverse("mel_reports:scheduled_reports")


# ---------------------------------------------------------------------------
# C-D10 / #43 — render_all honours the requested formats
# ---------------------------------------------------------------------------


def test_render_all_honours_formats_selection():
    owner, lf, output, inds = _lf_with_indicators("d10", n=1)
    tpl = _template("d10", lf, ["indicator_rollup"])
    report = _generate(tpl, owner)

    render_all(report, formats=["html"])

    formats = set(report.artifacts.values_list("format", flat=True))
    assert formats == {"html"}


# ---------------------------------------------------------------------------
# C-D11 / #44 — RIMS funding builder accepts the period window without error
# ---------------------------------------------------------------------------


def test_rims_funding_summary_accepts_period_and_scope():
    owner, lf, output, inds = _lf_with_indicators("d11", n=1)
    lf.programme = "NoSuchProgramme-XYZ"
    lf.save(update_fields=["programme"])

    out = build_rims_funding_summary(
        logframe=lf, period_start=date(2026, 1, 1), period_end=date(2026, 12, 31),
    )

    # RIMS is installed in this project; keys present and no crash.
    assert "installed" in out
    assert "note" in out
    assert "programme_matched" in out


# ---------------------------------------------------------------------------
# #45 — publish toast reflects a zero-recipient distribution
# ---------------------------------------------------------------------------


def test_publish_toast_reflects_zero_recipients(client):
    owner, lf, output, inds = _lf_with_indicators("p45", n=1)
    tpl = _template("p45", lf, ["indicator_rollup"])  # no default_recipients
    # M&E SRS (E1) — a report needs a narrative before it can be finalized.
    report = _generate(tpl, owner, narrative="Quarterly performance summary.")

    officer = _officer("p45")
    client.force_login(officer)
    resp = client.post(
        reverse("mel_reports:report_publish", kwargs={"pk": report.pk}),
        follow=True,
    )
    msgs = " ".join(str(m) for m in resp.context["messages"])

    assert "no recipients" in msgs.lower()
