"""View tests for Activity CRUD + transition (FRMFL009-013)."""
from __future__ import annotations

from datetime import date
from unittest.mock import patch

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

from apps.core.permissions.roles import UserRole
from apps.mel.indicators.models import LogFrame, LogFrameLevel, LogFrameRow
from apps.mel.tracking.models import Activity, ActivityStatus

pytestmark = pytest.mark.django_db

User = get_user_model()


_LF_COUNTER = {"n": 0}


def _next_slug(prefix: str) -> str:
    _LF_COUNTER["n"] += 1
    return f"{prefix}-{_LF_COUNTER['n']}"


def _activity_row():
    slug = _next_slug("vlf-act")
    lf = LogFrame.objects.create(name=f"VLF-A {slug}", slug=slug)
    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)
    return LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.ACTIVITY, title="A", parent=output)


def _output_row(prefix="vlf-out"):
    slug = _next_slug(prefix)
    lf = LogFrame.objects.create(name=f"VLF-O {slug}", slug=slug)
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    outcome = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.OUTCOME, title="O", parent=impact)
    return LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.OUTPUT, title="P", parent=outcome)


def _make_user(email: str, role: str | None) -> User:
    user = User.objects.create_user(email=email, password="x")
    if role is not None:
        user.role = role
        user.save(update_fields=["role"])
    return user


def _officer():
    return _make_user("officer@example.test", UserRole.MEL_OFFICER.value)


def _planned_activity(name: str = "Workshop") -> Activity:
    return Activity.objects.create(
        logframe_row=_activity_row(),
        name=name,
        scheduled_start=date(2026, 4, 1),
        scheduled_end=date(2026, 4, 30),
    )


# ---------------------------------------------------------------------------
# List view
# ---------------------------------------------------------------------------


def test_list_view_visible_to_mel_officer():
    activity = _planned_activity()
    client = Client()
    client.force_login(_officer())
    resp = client.get(reverse("mel_tracking:activity_list"))
    assert resp.status_code == 200
    assert activity.name.encode() in resp.content


def test_list_view_hidden_from_unrelated_role():
    _planned_activity()
    client = Client()
    client.force_login(_make_user("learner@example.test", UserRole.LEARNER.value))
    resp = client.get(reverse("mel_tracking:activity_list"))
    assert resp.status_code == 403


def test_list_view_redirects_anonymous():
    _planned_activity()
    client = Client()
    resp = client.get(reverse("mel_tracking:activity_list"))
    assert resp.status_code == 302  # redirected to login


def test_list_view_filters_by_status():
    a1 = _planned_activity("Planned one")
    a2 = _planned_activity("Delayed one")
    a2.status = ActivityStatus.DELAYED
    a2.deviation_flag = True
    a2.save(update_fields=["status", "deviation_flag"])

    client = Client()
    client.force_login(_officer())
    resp = client.get(reverse("mel_tracking:activity_list") + "?status=delayed")
    assert resp.status_code == 200
    assert a2.name.encode() in resp.content
    assert a1.name.encode() not in resp.content


def test_list_view_filters_by_delayed_flag():
    a1 = _planned_activity("Plain")
    a2 = _planned_activity("Flagged")
    a2.deviation_flag = True
    a2.save(update_fields=["deviation_flag"])

    client = Client()
    client.force_login(_officer())
    resp = client.get(reverse("mel_tracking:activity_list") + "?delayed=1")
    assert resp.status_code == 200
    assert a2.name.encode() in resp.content
    assert a1.name.encode() not in resp.content


# ---------------------------------------------------------------------------
# Create / update views
# ---------------------------------------------------------------------------


def test_create_view_form_rejects_non_activity_logframe_row():
    output_row = _output_row()
    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_tracking:activity_create"),
        data={
            "logframe_row": output_row.pk,
            "name": "Bad row",
            "scheduled_start": "2026-04-01",
            "scheduled_end": "2026-04-30",
        },
    )
    # Form re-renders (200) with an error on logframe_row — the queryset filter
    # ensures the OUTPUT row isn't a valid choice.
    assert resp.status_code == 200
    assert b"logframe_row" in resp.content or b"Select a valid choice" in resp.content
    assert Activity.objects.filter(name="Bad row").exists() is False


def test_create_view_persists_and_redirects_to_detail():
    row = _activity_row()
    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_tracking:activity_create"),
        data={
            "logframe_row": row.pk,
            "name": "Workshop kickoff",
            "scheduled_start": "2026-04-01",
            "scheduled_end": "2026-04-30",
        },
    )
    activity = Activity.objects.get(name="Workshop kickoff")
    assert resp.status_code == 302
    assert resp["Location"] == reverse("mel_tracking:activity_detail", kwargs={"pk": activity.pk})


def test_update_view_does_not_expose_status_field():
    from apps.mel.tracking.forms import ActivityForm

    assert "status" not in ActivityForm.base_fields


# ---------------------------------------------------------------------------
# Detail view
# ---------------------------------------------------------------------------


def test_detail_view_lists_only_legal_transitions_for_planned():
    activity = _planned_activity()
    client = Client()
    client.force_login(_officer())
    resp = client.get(reverse("mel_tracking:activity_detail", kwargs={"pk": activity.pk}))
    assert resp.status_code == 200
    legal = {t["value"] for t in resp.context["legal_transitions"]}
    assert legal == {
        ActivityStatus.IN_PROGRESS,
        ActivityStatus.DELAYED,
        ActivityStatus.INCOMPLETE,
        ActivityStatus.CANCELLED,
    }


def test_mark_incomplete_records_reason_and_offers_followup():
    """M&E SRS Table 65 — an incomplete field collection records a reason and
    exposes the schedule-follow-up form on the detail page."""
    activity = _planned_activity()
    client = Client()
    client.force_login(_officer())
    # PLANNED → INCOMPLETE via the transition view.
    resp = client.post(
        reverse("mel_tracking:activity_transition", kwargs={"pk": activity.pk}),
        {"new_status": ActivityStatus.INCOMPLETE, "deviation_reason": "Inaccessible location"},
    )
    assert resp.status_code == 302
    activity.refresh_from_db()
    assert activity.status == ActivityStatus.INCOMPLETE
    assert activity.deviation_flag is True
    assert "Inaccessible" in activity.deviation_reason
    # Detail page now offers follow-up scheduling.
    detail = client.get(reverse("mel_tracking:activity_detail", kwargs={"pk": activity.pk}))
    assert detail.context["is_incomplete"] is True
    assert b"followup" in detail.content


def test_schedule_followup_creates_linked_planned_activity():
    activity = _planned_activity()
    activity.status = ActivityStatus.INCOMPLETE
    activity.save(update_fields=["status"])
    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_tracking:activity_schedule_followup", kwargs={"pk": activity.pk}),
        {"name": "", "scheduled_start": "2026-08-01", "scheduled_end": "2026-08-10"},
    )
    assert resp.status_code == 302
    followup = activity.children.get()
    assert followup.status == ActivityStatus.PLANNED
    assert followup.logframe_row_id == activity.logframe_row_id
    assert followup.parent_id == activity.pk


def test_schedule_followup_rejected_when_not_incomplete():
    activity = _planned_activity()  # still PLANNED
    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_tracking:activity_schedule_followup", kwargs={"pk": activity.pk}),
        {"scheduled_start": "2026-08-01", "scheduled_end": "2026-08-10"},
    )
    assert resp.status_code == 302
    assert activity.children.count() == 0


def test_detail_view_completed_has_no_transitions():
    activity = _planned_activity()
    activity.status = ActivityStatus.COMPLETED
    activity.save(update_fields=["status"])
    client = Client()
    client.force_login(_officer())
    resp = client.get(reverse("mel_tracking:activity_detail", kwargs={"pk": activity.pk}))
    assert resp.status_code == 200
    assert resp.context["legal_transitions"] == []


# ---------------------------------------------------------------------------
# Transition view
# ---------------------------------------------------------------------------


def test_transition_view_calls_service_and_redirects():
    activity = _planned_activity()
    client = Client()
    officer = _officer()
    client.force_login(officer)
    with patch("apps.mel.tracking.views.record_activity_progress") as svc:
        resp = client.post(
            reverse("mel_tracking:activity_transition", kwargs={"pk": activity.pk}),
            data={"new_status": ActivityStatus.IN_PROGRESS},
        )
    svc.assert_called_once()
    args, kwargs = svc.call_args
    assert args == (activity.pk,)
    assert kwargs["new_status"] == ActivityStatus.IN_PROGRESS
    assert kwargs["user"] == officer
    assert resp.status_code == 302
    assert resp["Location"] == reverse(
        "mel_tracking:activity_detail", kwargs={"pk": activity.pk}
    )


def test_transition_view_rejects_illegal_transition():
    activity = _planned_activity()  # status=PLANNED
    client = Client()
    client.force_login(_officer())
    resp = client.post(
        reverse("mel_tracking:activity_transition", kwargs={"pk": activity.pk}),
        data={"new_status": ActivityStatus.COMPLETED},
        follow=True,
    )
    assert resp.status_code == 200
    activity.refresh_from_db()
    assert activity.status == ActivityStatus.PLANNED
    # Error message surfaced via the messages framework on the redirected page.
    body = resp.content.decode()
    assert "Cannot transition" in body or "error" in body.lower()


@pytest.mark.django_db(transaction=True)
def test_transition_delayed_emits_signal():
    """``record_activity_progress`` defers the signal via ``transaction.on_commit``,
    so this test needs a real transaction (not the implicit test rollback)."""
    from apps.mel.tracking.signals import mel_activity_delayed

    activity = _planned_activity()
    received = []

    def receiver(sender, **kw):
        received.append(kw.get("activity"))

    mel_activity_delayed.connect(receiver)
    try:
        client = Client()
        client.force_login(_officer())
        resp = client.post(
            reverse("mel_tracking:activity_transition", kwargs={"pk": activity.pk}),
            data={"new_status": ActivityStatus.DELAYED, "deviation_reason": "Vendor late"},
        )
        assert resp.status_code == 302
    finally:
        mel_activity_delayed.disconnect(receiver)

    assert len(received) == 1
    assert received[0].pk == activity.pk
