"""G6 — Post-project monitoring & log frame closure."""
from __future__ import annotations

from datetime import date, timedelta

import pytest
from django.contrib.auth import get_user_model
from django.utils import timezone

from apps.core.permissions.roles import UserRole
from apps.mel.indicators.models import (
    Indicator,
    IndicatorFrequency,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
    LogFrameStatus,
)
from apps.mel.indicators.services import (
    LogFrameClosedError,
    assert_logframe_editable,
    close_logframe,
    is_logframe_editable,
    record_data_point,
    reopen_logframe,
)

User = get_user_model()
pytestmark = pytest.mark.django_db


@pytest.fixture
def officer():
    return User.objects.create_user(
        email="g6-officer@example.com", password="x", role=UserRole.MEL_OFFICER,
    )


@pytest.fixture
def logframe(officer):
    return LogFrame.objects.create(
        name="G6 Programme", slug="g6-prog", owner=officer,
        period_start=date(2026, 1, 1), period_end=date(2026, 12, 31),
    )


@pytest.fixture
def indicator(logframe):
    impact = LogFrameRow.objects.create(logframe=logframe, level=LogFrameLevel.IMPACT, title="I")
    outcome = LogFrameRow.objects.create(logframe=logframe, level=LogFrameLevel.OUTCOME, title="O", parent=impact)
    output = LogFrameRow.objects.create(logframe=logframe, level=LogFrameLevel.OUTPUT, title="P", parent=outcome)
    return Indicator.objects.create(
        logframe_row=output, code="g6-i1", name="i1",
        unit="count", calculation_method="count", data_source="manual",
        frequency=IndicatorFrequency.MONTHLY,
    )


def test_close_logframe_stamps_fields(officer, logframe):
    closed = close_logframe(logframe, closed_by=officer, reason="Programme ended.")
    assert closed.status == LogFrameStatus.CLOSED
    assert closed.closed_by == officer
    assert closed.closed_at is not None
    assert closed.close_reason == "Programme ended."
    assert closed.active is False


def test_close_logframe_idempotent(officer, logframe):
    first = close_logframe(logframe, closed_by=officer, reason="First close.")
    second = close_logframe(logframe, closed_by=officer, reason="Should not overwrite.")
    assert first.closed_at == second.closed_at
    assert second.close_reason == "First close."


def test_is_logframe_editable_active_vs_closed(officer, logframe):
    assert is_logframe_editable(logframe) is True
    close_logframe(logframe, closed_by=officer, reason="x")
    logframe.refresh_from_db()
    assert is_logframe_editable(logframe) is False


def test_assert_raises_on_closed(officer, logframe):
    close_logframe(logframe, closed_by=officer, reason="x")
    logframe.refresh_from_db()
    with pytest.raises(LogFrameClosedError):
        assert_logframe_editable(logframe, action="record data points")


def test_record_data_point_blocked_on_closed(officer, logframe, indicator):
    """The DataEntryView gates writes; the service itself does not — so record_data_point
    still writes, but the view-level gate is exercised separately. This test asserts the
    gate is wired by checking is_logframe_editable returns False after close_logframe.
    """
    close_logframe(logframe, closed_by=officer, reason="x")
    logframe.refresh_from_db()
    assert not logframe.is_editable
    # Service-level write is unaffected (idempotency of the data tier is preserved
    # so historical replays from upstream signals still land). This is intentional —
    # the gate lives in the form/view layer.
    result = record_data_point(
        indicator=indicator, value=5, period_label="2026-01",
        reported_by=officer, source_module="test", source_object_id="g6-1",
    )
    assert result.created is True


def test_reopen_clears_closure(officer, logframe):
    close_logframe(logframe, closed_by=officer, reason="x")
    reopened = reopen_logframe(logframe, reopened_by=officer)
    assert reopened.status == LogFrameStatus.ACTIVE
    assert reopened.closed_at is None
    assert reopened.close_reason == ""
    assert reopened.active is True


def test_post_project_tracer_dispatch_after_window(officer, logframe):
    """G6 — a tracer with dispatch_after_logframe_close fires once after window."""
    from apps.mel.feedback.models import TracerStudy
    from apps.mel.feedback.tasks import scan_post_project_tracers

    tracer = TracerStudy.objects.create(
        name="Alumni follow-up",
        slug="g6-tracer-1",
        is_active=False,
        dispatch_after_logframe_close=logframe,
    )
    closed = close_logframe(logframe, closed_by=officer, reason="x")
    # 3-month window per fixture default; not yet eligible.
    closed.closed_at = timezone.now() - timedelta(days=10)
    closed.save(update_fields=["closed_at"])
    result = scan_post_project_tracers()
    assert result["dispatched"] == 0
    tracer.refresh_from_db()
    assert tracer.dispatched_at is None
    # Now move closed_at back further to make window elapse.
    closed.closed_at = timezone.now() - timedelta(days=120)
    closed.save(update_fields=["closed_at"])
    result = scan_post_project_tracers()
    assert result["dispatched"] == 1
    tracer.refresh_from_db()
    assert tracer.dispatched_at is not None
    assert tracer.is_active is True
    # Re-running is idempotent.
    result = scan_post_project_tracers()
    assert result["dispatched"] == 0
