"""BPM Phase 16 — Process KPI integration tests.

Covers:
- KPI target met calculation with various operators
- Alert auto-creation on threshold breach
- KPI dashboard aggregation
"""

from __future__ import annotations

import decimal

import pytest

from simorgh.apps.bpm.models import (
    KPIAlert,
    KPIAlertType,
    KPICategory,
    KPITargetOperator,
    PCFFramework,
    ProcessDefinition,
    ProcessKPI,
    ProcessKPIMeasurement,
)
from simorgh.apps.bpm.selectors import (
    get_kpi_dashboard,
    list_process_kpis,
)
from simorgh.apps.bpm.services import record_kpi_measurement


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def framework(db) -> PCFFramework:
    return PCFFramework.objects.create(
        code="PCF-KPI-TEST",
        name="KPI Test Framework",
        industry="cross_industry",
        version="1.0",
        language="en",
        is_active=True,
    )


@pytest.fixture
def process(tenant_acme, framework) -> ProcessDefinition:
    return ProcessDefinition.objects.create(
        tenant=tenant_acme,
        framework=framework,
        hierarchy_id="1.1",
        level=2,
        name="KPI Test Process",
        name_fa="فرآیند تست KPI",
        status="active",
        version="1.0",
    )


def _make_kpi(process, *, code, target_value, operator=">=", name="Test KPI") -> ProcessKPI:
    return ProcessKPI.objects.create(
        process=process,
        code=code,
        name=name,
        name_fa=f"{name} FA",
        category=KPICategory.EFFECTIVENESS,
        target_value=decimal.Decimal(str(target_value)),
        target_operator=operator,
        unit="%",
    )


# ---------------------------------------------------------------------------
# test_kpi_target_met_calculation
# ---------------------------------------------------------------------------


class TestKPITargetMetCalculation:
    """Tests for ProcessKPI.evaluate() with all supported operators."""

    def test_gte_target_met(self, process):
        """>= operator: value at or above target → True."""
        kpi = _make_kpi(process, code="KPI-01", target_value=80, operator=">=")
        assert kpi.evaluate(decimal.Decimal("80")) is True
        assert kpi.evaluate(decimal.Decimal("95")) is True

    def test_gte_target_not_met(self, process):
        """>= operator: value below target → False."""
        kpi = _make_kpi(process, code="KPI-02", target_value=80, operator=">=")
        assert kpi.evaluate(decimal.Decimal("79.9")) is False

    def test_lte_target_met(self, process):
        """<= operator: value at or below target → True."""
        kpi = _make_kpi(process, code="KPI-03", target_value=5, operator="<=")
        assert kpi.evaluate(decimal.Decimal("5")) is True
        assert kpi.evaluate(decimal.Decimal("3")) is True

    def test_lte_target_not_met(self, process):
        """<= operator: value above target → False."""
        kpi = _make_kpi(process, code="KPI-04", target_value=5, operator="<=")
        assert kpi.evaluate(decimal.Decimal("5.1")) is False

    def test_eq_target_met(self, process):
        """= operator: exact match → True."""
        kpi = _make_kpi(process, code="KPI-05", target_value=100, operator="=")
        assert kpi.evaluate(decimal.Decimal("100")) is True

    def test_eq_target_not_met(self, process):
        """= operator: any deviation → False."""
        kpi = _make_kpi(process, code="KPI-06", target_value=100, operator="=")
        assert kpi.evaluate(decimal.Decimal("99.9")) is False
        assert kpi.evaluate(decimal.Decimal("100.1")) is False

    def test_gt_target_met(self, process):
        """> operator: strictly greater → True."""
        kpi = _make_kpi(process, code="KPI-07", target_value=50, operator=">")
        assert kpi.evaluate(decimal.Decimal("50.01")) is True

    def test_gt_target_not_met_at_boundary(self, process):
        """> operator: equal to target → False."""
        kpi = _make_kpi(process, code="KPI-08", target_value=50, operator=">")
        assert kpi.evaluate(decimal.Decimal("50")) is False

    def test_lt_target_met(self, process):
        """< operator: strictly lower → True."""
        kpi = _make_kpi(process, code="KPI-09", target_value=10, operator="<")
        assert kpi.evaluate(decimal.Decimal("9.99")) is True

    def test_lt_target_not_met_at_boundary(self, process):
        """< operator: equal to target → False."""
        kpi = _make_kpi(process, code="KPI-10", target_value=10, operator="<")
        assert kpi.evaluate(decimal.Decimal("10")) is False

    def test_measurement_is_target_met_property(self, process, alice):
        """ProcessKPIMeasurement.is_target_met delegates to kpi.evaluate()."""
        kpi = _make_kpi(process, code="KPI-11", target_value=70, operator=">=")
        m_ok = ProcessKPIMeasurement.objects.create(
            kpi=kpi,
            value=decimal.Decimal("75"),
            measured_by=alice,
        )
        m_fail = ProcessKPIMeasurement.objects.create(
            kpi=kpi,
            value=decimal.Decimal("60"),
            measured_by=alice,
        )
        assert m_ok.is_target_met is True
        assert m_fail.is_target_met is False

    def test_kpi_str_representation(self, process):
        """ProcessKPI __str__ includes hierarchy_id and code."""
        kpi = _make_kpi(process, code="KPI-STR", target_value=80)
        s = str(kpi)
        assert "1.1" in s
        assert "KPI-STR" in s


# ---------------------------------------------------------------------------
# test_kpi_alert_on_breach
# ---------------------------------------------------------------------------


class TestKPIAlertOnBreach:
    """Tests for automatic KPIAlert creation via record_kpi_measurement service."""

    def test_no_alert_when_target_met(self, process, alice):
        """No KPIAlert created when measurement satisfies the target."""
        kpi = _make_kpi(process, code="KPI-A01", target_value=80, operator=">=")
        record_kpi_measurement(kpi, decimal.Decimal("85"), alice)
        assert KPIAlert.objects.filter(kpi=kpi).count() == 0

    def test_alert_created_when_below_target(self, process, alice):
        """KPIAlert with type BELOW_TARGET created when value < target (>= operator)."""
        kpi = _make_kpi(process, code="KPI-A02", target_value=80, operator=">=")
        measurement = record_kpi_measurement(kpi, decimal.Decimal("60"), alice)
        alerts = KPIAlert.objects.filter(kpi=kpi)
        assert alerts.count() == 1
        alert = alerts.first()
        assert alert.measurement == measurement
        assert alert.alert_type == KPIAlertType.BELOW_TARGET

    def test_alert_created_when_above_target(self, process, alice):
        """KPIAlert with type ABOVE_TARGET created when value > target (<= operator)."""
        kpi = _make_kpi(process, code="KPI-A03", target_value=5, operator="<=")
        measurement = record_kpi_measurement(kpi, decimal.Decimal("8"), alice)
        alerts = KPIAlert.objects.filter(kpi=kpi)
        assert alerts.count() == 1
        alert = alerts.first()
        assert alert.alert_type == KPIAlertType.ABOVE_TARGET

    def test_multiple_breaches_create_multiple_alerts(self, process, alice):
        """Each breaching measurement creates its own alert."""
        kpi = _make_kpi(process, code="KPI-A04", target_value=80, operator=">=")
        record_kpi_measurement(kpi, decimal.Decimal("50"), alice)
        record_kpi_measurement(kpi, decimal.Decimal("55"), alice)
        assert KPIAlert.objects.filter(kpi=kpi).count() == 2

    def test_measurement_stored_with_notes(self, process, alice):
        """Notes are persisted on the measurement."""
        kpi = _make_kpi(process, code="KPI-A05", target_value=80, operator=">=")
        m = record_kpi_measurement(
            kpi, decimal.Decimal("85"), alice,
            notes="Q1 result"
        )
        assert m.notes == "Q1 result"

    def test_measurement_stored_with_process_instance_id(self, process, alice):
        """process_instance_id is stored on the measurement when provided."""
        import uuid
        kpi = _make_kpi(process, code="KPI-A06", target_value=80, operator=">=")
        instance_id = uuid.uuid4()
        m = record_kpi_measurement(
            kpi, decimal.Decimal("90"), alice,
            process_instance_id=instance_id,
        )
        assert m.process_instance_id == instance_id

    def test_alert_kpi_cascade_delete(self, process, alice):
        """KPIAlert is deleted when the KPI is deleted (CASCADE)."""
        kpi = _make_kpi(process, code="KPI-A07", target_value=80, operator=">=")
        record_kpi_measurement(kpi, decimal.Decimal("50"), alice)
        assert KPIAlert.objects.filter(kpi=kpi).count() == 1
        kpi.delete()
        assert KPIAlert.objects.count() == 0

    def test_record_kpi_measurement_measured_by_optional(self, process):
        """measured_by can be None (anonymous/system measurement)."""
        kpi = _make_kpi(process, code="KPI-A08", target_value=80, operator=">=")
        m = record_kpi_measurement(kpi, decimal.Decimal("90"), user=None)
        assert m.measured_by is None

    def test_list_process_kpis_selector(self, process):
        """list_process_kpis() returns all KPIs for the process."""
        _make_kpi(process, code="KPI-S01", target_value=80)
        _make_kpi(process, code="KPI-S02", target_value=5, operator="<=")
        qs = list_process_kpis(process)
        assert qs.count() == 2


# ---------------------------------------------------------------------------
# test_kpi_dashboard_aggregation
# ---------------------------------------------------------------------------


class TestKPIDashboardAggregation:
    """Tests for get_kpi_dashboard() aggregation query."""

    def test_dashboard_returns_dict(self, tenant_acme, process, alice):
        """get_kpi_dashboard() returns a dict."""
        result = get_kpi_dashboard(tenant_acme)
        assert isinstance(result, dict)

    def test_dashboard_totals_structure(self, tenant_acme, process, alice):
        """Dashboard dict contains expected top-level keys."""
        kpi = _make_kpi(process, code="KPI-D01", target_value=80, operator=">=")
        record_kpi_measurement(kpi, decimal.Decimal("90"), alice)
        result = get_kpi_dashboard(tenant_acme)
        assert "total_kpis" in result
        assert "total_measurements" in result

    def test_dashboard_counts_kpis(self, tenant_acme, process, alice):
        """Dashboard correctly counts the number of KPIs."""
        _make_kpi(process, code="KPI-D02", target_value=80)
        _make_kpi(process, code="KPI-D03", target_value=5, operator="<=")
        result = get_kpi_dashboard(tenant_acme)
        assert result["total_kpis"] >= 2

    def test_dashboard_counts_measurements(self, tenant_acme, process, alice):
        """Dashboard correctly counts total measurements."""
        kpi = _make_kpi(process, code="KPI-D04", target_value=80, operator=">=")
        record_kpi_measurement(kpi, decimal.Decimal("85"), alice)
        record_kpi_measurement(kpi, decimal.Decimal("70"), alice)
        result = get_kpi_dashboard(tenant_acme)
        assert result["total_measurements"] >= 2

    def test_dashboard_tenant_isolation(self, tenant_acme, tenant_globex, framework, alice):
        """Dashboard data is scoped to the tenant."""
        # Create process for Globex
        globex_process = ProcessDefinition.objects.create(
            tenant=tenant_globex,
            framework=framework,
            hierarchy_id="3",
            level=1,
            name="Globex KPI Process",
            name_fa="فرآیند گلوبکس",
            status="active",
            version="1.0",
        )
        globex_kpi = _make_kpi(
            globex_process, code="KPI-G01", target_value=90, name="Globex KPI"
        )
        record_kpi_measurement(globex_kpi, decimal.Decimal("95"), alice)

        acme_result = get_kpi_dashboard(tenant_acme)
        globex_result = get_kpi_dashboard(tenant_globex)

        # Globex KPI should not appear in Acme dashboard
        assert acme_result.get("total_kpis", 0) == 0 or all(
            kpi_data.get("kpi__process__name") != "Globex KPI Process"
            for kpi_data in acme_result.get("kpi_details", [])
        )
        assert globex_result["total_kpis"] >= 1

    def test_dashboard_with_no_measurements_is_safe(self, tenant_acme, process):
        """Dashboard returns valid structure even when no measurements exist."""
        _make_kpi(process, code="KPI-D05", target_value=80)
        result = get_kpi_dashboard(tenant_acme)
        assert result["total_measurements"] == 0

    def test_kpi_unique_per_process(self, process):
        """Two KPIs with the same code in the same process raise IntegrityError."""
        from django.db import IntegrityError

        _make_kpi(process, code="KPI-UNIQ", target_value=80)
        with pytest.raises(IntegrityError):
            _make_kpi(process, code="KPI-UNIQ", target_value=90)
