"""Tests for Activity tracking (FRMFL009–013)."""
from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.core.exceptions import ValidationError

from apps.mel.indicators.models import (
    Indicator,
    IndicatorFrequency,
    LogFrame,
    LogFrameLevel,
    LogFrameRow,
)
from apps.mel.tracking.models import (
    Activity,
    ActivityDependency,
    ActivityStatus,
    OutputDeliverable,
    OutputDeliverableStatus,
)
from apps.mel.tracking.services import (
    record_activity_progress,
    record_output_progress,
    scan_delayed_activities,
)

pytestmark = pytest.mark.django_db


_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("lf-act")
    lf = LogFrame.objects.create(name=f"LF-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():
    slug = _next_slug("lf-out")
    lf = LogFrame.objects.create(name=f"LF-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 test_activity_rejects_non_activity_logframe_row():
    lf = LogFrame.objects.create(name="LF-bad", slug="lf-act-bad")
    impact = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.IMPACT, title="I")
    outcome = LogFrameRow.objects.create(logframe=lf, level=LogFrameLevel.OUTCOME, title="O", parent=impact)
    act = Activity(
        logframe_row=outcome,
        name="Bad",
        scheduled_start=date(2026, 4, 1),
        scheduled_end=date(2026, 4, 30),
    )
    with pytest.raises(ValidationError):
        act.full_clean()


def test_activity_status_transitions_enforced():
    row = _activity_row()
    activity = Activity.objects.create(
        logframe_row=row,
        name="Grant call",
        scheduled_start=date(2026, 4, 1),
        scheduled_end=date(2026, 4, 30),
    )
    # Legal: PLANNED → IN_PROGRESS.
    r = record_activity_progress(activity.pk, new_status=ActivityStatus.IN_PROGRESS)
    assert r.transitioned is True
    assert r.previous_status == ActivityStatus.PLANNED
    assert r.activity.actual_start is not None

    # Illegal: PLANNED → COMPLETED (but we're now IN_PROGRESS, so legal from here).
    record_activity_progress(activity.pk, new_status=ActivityStatus.COMPLETED)
    activity.refresh_from_db()
    assert activity.status == ActivityStatus.COMPLETED
    assert activity.actual_end is not None
    assert activity.deviation_flag is False


def test_cannot_transition_from_terminal_status():
    row = _activity_row()
    activity = Activity.objects.create(
        logframe_row=row,
        name="Done",
        scheduled_start=date(2026, 4, 1),
        scheduled_end=date(2026, 4, 30),
    )
    record_activity_progress(activity.pk, new_status=ActivityStatus.IN_PROGRESS)
    record_activity_progress(activity.pk, new_status=ActivityStatus.COMPLETED)

    with pytest.raises(ValidationError):
        record_activity_progress(activity.pk, new_status=ActivityStatus.IN_PROGRESS)


def test_activity_dependency_rejects_self_edge():
    from django.db import IntegrityError, transaction as dbt

    row = _activity_row()
    a = Activity.objects.create(
        logframe_row=row,
        name="A",
        scheduled_start=date(2026, 4, 1),
        scheduled_end=date(2026, 4, 30),
    )
    with dbt.atomic():
        with pytest.raises(IntegrityError):
            ActivityDependency.objects.create(from_activity=a, to_activity=a)


def test_scan_delayed_activities_flags_past_due_items():
    row = _activity_row()
    old = Activity.objects.create(
        logframe_row=row,
        name="Old",
        scheduled_start=date(2026, 1, 1),
        scheduled_end=date(2026, 1, 15),
        status=ActivityStatus.PLANNED,
    )
    row2 = _activity_row()  # second logframe, fresh activity
    fresh = Activity.objects.create(
        logframe_row=row2,
        name="Fresh",
        scheduled_start=date(2099, 1, 1),
        scheduled_end=date(2099, 1, 15),
        status=ActivityStatus.PLANNED,
    )

    flagged = scan_delayed_activities(today=date(2026, 4, 24))

    old.refresh_from_db()
    fresh.refresh_from_db()

    assert flagged == 1
    assert old.status == ActivityStatus.DELAYED
    assert old.deviation_flag is True
    assert fresh.status == ActivityStatus.PLANNED


def test_record_output_progress_marks_completed_and_overdue():
    row = _output_row()
    deliverable = OutputDeliverable.objects.create(
        logframe_row=row,
        title="Reports",
        target_quantity=Decimal("10"),
        unit="reports",
    )
    # Half-way progress → IN_PROGRESS.
    record_output_progress(deliverable.pk, achieved=Decimal("5"))
    deliverable.refresh_from_db()
    assert deliverable.status == OutputDeliverableStatus.IN_PROGRESS
    assert deliverable.percent_achieved == pytest.approx(50.0)

    # Hit target → COMPLETED.
    record_output_progress(deliverable.pk, achieved=Decimal("10"))
    deliverable.refresh_from_db()
    assert deliverable.status == OutputDeliverableStatus.COMPLETED


def test_output_overdue_when_past_due_date_and_incomplete():
    row = _output_row()
    deliverable = OutputDeliverable.objects.create(
        logframe_row=row,
        title="Overdue",
        target_quantity=Decimal("10"),
        unit="reports",
        due_date=date(2020, 1, 1),
    )
    record_output_progress(deliverable.pk, achieved=Decimal("2"))
    deliverable.refresh_from_db()
    assert deliverable.status == OutputDeliverableStatus.OVERDUE
