"""BPM Phase 7 — REST API tests.

Covers:
- GET  /api/v1/bpm/frameworks/
- GET  /api/v1/bpm/frameworks/{code}/elements/
- GET  /api/v1/bpm/processes/
- POST /api/v1/bpm/processes/
- GET  /api/v1/bpm/processes/{id}/
- PATCH /api/v1/bpm/processes/{id}/
- GET  /api/v1/bpm/processes/{id}/raci/
- PUT  /api/v1/bpm/processes/{id}/raci/
- GET  /api/v1/bpm/processes/{id}/kpis/
- GET  /api/v1/bpm/processes/{id}/rules/
- GET  /api/v1/bpm/processes/{id}/control-points/
- GET  /api/v1/bpm/processes/{id}/io/
- POST /api/v1/bpm/instances/
- GET  /api/v1/bpm/instances/{id}/
- PATCH /api/v1/bpm/instances/{id}/steps/{step_id}/
- POST /api/v1/bpm/instances/{id}/control-points/{cp}/execute/
- POST /api/v1/bpm/instances/{id}/kpis/{kpi}/measure/
- GET  /api/v1/bpm/kpis/dashboard/
"""

from __future__ import annotations

import decimal

import pytest
from simorgh.apps.bpm.models import (
    ControlPointExecutionOutcome,
    InstanceStatus,
    PCFFramework,
    ProcessControlPoint,
    ProcessDefinition,
    ProcessInstance,
    ProcessKPI,
    ProcessOperationalStep,
    ProcessRole,
    ProcessRule,
    StepStatus,
)
from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.apps.memberships.models import Membership


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

BASE = "/api/v1/bpm/"


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def perms_synced(db):
    sync_registry_to_db()


@pytest.fixture
def alice_admin(alice, tenant_acme, acme_tree, role_admin, perms_synced):
    """Alice with all permissions."""
    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_admin,
    )
    m.users.add(alice)
    return alice


@pytest.fixture
def framework(db) -> PCFFramework:
    return PCFFramework.objects.create(
        code="TEST",
        name="Test Framework",
        industry="test",
        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",
        level=1,
        name="Test Process",
        name_fa="فرآیند آزمایشی",
        status="active",
        version="1.0",
    )


@pytest.fixture
def process_role(process) -> ProcessRole:
    return ProcessRole.objects.create(
        process=process,
        code="PM",
        name="Process Manager",
        order=1,
    )


@pytest.fixture
def op_step(process) -> ProcessOperationalStep:
    return ProcessOperationalStep.objects.create(
        process=process,
        step_number="1",
        title="First Step",
        order=1,
    )


@pytest.fixture
def kpi(process, process_role) -> ProcessKPI:
    return ProcessKPI.objects.create(
        process=process,
        code="KPI-T-01",
        name="Test KPI",
        category="effectiveness",
        target_value=decimal.Decimal("80.00"),
        target_operator=">=",
        unit="%",
        frequency="monthly",
        owner_role=process_role,
    )


@pytest.fixture
def rule(process) -> ProcessRule:
    return ProcessRule.objects.create(
        process=process,
        code="R1",
        description="Test rule",
        source="internal",
        order=1,
    )


@pytest.fixture
def control_point(process, process_role) -> ProcessControlPoint:
    return ProcessControlPoint.objects.create(
        process=process,
        code="CP1",
        stage="before",
        description="Check before start",
        responsible_role=process_role,
    )


@pytest.fixture
def instance(tenant_acme, process, alice) -> ProcessInstance:
    return ProcessInstance.objects.create(
        tenant=tenant_acme,
        process=process,
        title="Test Run",
        triggered_by="test",
        status=InstanceStatus.PLANNED,
        created_by=alice,
    )


# ---------------------------------------------------------------------------
# Framework views
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestFrameworkList:
    def test_unauthenticated_returns_401(self, api_client):
        resp = api_client.get(f"{BASE}frameworks/")
        assert resp.status_code == 401

    def test_list_active_frameworks(self, api_client, alice_admin, tenant_acme, framework):
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}frameworks/", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] >= 1
        codes = [f["code"] for f in data["results"]]
        assert "TEST" in codes

    def test_inactive_framework_excluded(self, api_client, alice_admin, tenant_acme, framework):
        framework.is_active = False
        framework.save()
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}frameworks/", HTTP_X_TENANT=tenant_acme.slug)
        codes = [f["code"] for f in resp.json()["results"]]
        assert "TEST" not in codes


@pytest.mark.django_db
class TestFrameworkElements:
    def test_unknown_code_returns_404(self, api_client, alice_admin, tenant_acme):
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}frameworks/NOEXIST/elements/", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 404

    def test_returns_framework_and_elements(self, api_client, alice_admin, tenant_acme, framework):
        from simorgh.apps.bpm.models import PCFElement

        PCFElement.objects.create(
            framework=framework,
            pcf_id=10000,
            hierarchy_id="1.0",
            level=1,
            name_en="Root Category",
        )
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}frameworks/TEST/elements/", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        body = resp.json()
        assert body["framework"]["code"] == "TEST"
        assert isinstance(body["elements"], list)
        assert len(body["elements"]) >= 1


# ---------------------------------------------------------------------------
# Process views
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestProcessListCreate:
    def test_list_processes_for_tenant(self, api_client, alice_admin, tenant_acme, process):
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}processes/", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] >= 1
        ids = [p["id"] for p in data["results"]]
        assert str(process.public_id) in ids

    def test_tenant_isolation(self, api_client, alice_admin, tenant_acme, tenant_globex, process):
        """Processes from tenant_globex must not appear in tenant_acme's listing."""
        from simorgh.apps.bpm.models import PCFFramework, ProcessDefinition

        fw = PCFFramework.objects.create(
            code="OTHER", name="Other", industry="x", version="1.0", language="en", is_active=True
        )
        ProcessDefinition.objects.create(
            tenant=tenant_globex,
            framework=fw,
            hierarchy_id="99",
            level=1,
            name="Globex Process",
            status="active",
            version="1.0",
        )
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}processes/", HTTP_X_TENANT=tenant_acme.slug)
        names = [p["name"] for p in resp.json()["results"]]
        assert "Globex Process" not in names

    def test_create_process(self, api_client, alice_admin, tenant_acme, framework):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}processes/",
            data={
                "name": "New Process",
                "name_fa": "فرآیند جدید",
                "hierarchy_id": "2",
                "level": 1,
                "status": "draft",
                "version": "1.0",
                "framework_id": framework.pk,
            },
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 201
        body = resp.json()
        assert body["name"] == "New Process"
        assert "id" in body

    def test_create_process_missing_name(self, api_client, alice_admin, tenant_acme):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}processes/",
            data={"hierarchy_id": "3", "level": 1},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 400

    def test_no_tenant_returns_400(self, api_client, alice, db):
        api_client.force_login(alice)
        resp = api_client.get(f"{BASE}processes/")
        assert resp.status_code == 400


@pytest.mark.django_db
class TestProcessDetail:
    def test_get_process(self, api_client, alice_admin, tenant_acme, process):
        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}processes/{process.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["id"] == str(process.public_id)
        assert resp.json()["name"] == process.name

    def test_patch_process(self, api_client, alice_admin, tenant_acme, process):
        api_client.force_login(alice_admin)
        resp = api_client.patch(
            f"{BASE}processes/{process.public_id}/",
            data={"name": "Updated Name"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["name"] == "Updated Name"

    def test_not_found(self, api_client, alice_admin, tenant_acme):
        import uuid

        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}processes/{uuid.uuid4()}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 404


@pytest.mark.django_db
class TestProcessRaci:
    def test_get_raci_empty(self, api_client, alice_admin, tenant_acme, process):
        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}processes/{process.public_id}/raci/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["count"] == 0

    def test_put_raci_bulk_upsert(
        self, api_client, alice_admin, tenant_acme, process, process_role, op_step
    ):
        api_client.force_login(alice_admin)
        resp = api_client.put(
            f"{BASE}processes/{process.public_id}/raci/",
            data={
                "entries": [
                    {
                        "step_id": op_step.pk,
                        "role_id": process_role.pk,
                        "responsibility": "R",
                        "activity_label": "Execute Step",
                    }
                ]
            },
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["count"] == 1

    def test_put_raci_replaces_existing(
        self, api_client, alice_admin, tenant_acme, process, process_role, op_step
    ):
        """Second PUT should replace, not append."""
        api_client.force_login(alice_admin)
        url = f"{BASE}processes/{process.public_id}/raci/"
        # First upsert
        api_client.put(url, data={"entries": [{"step_id": op_step.pk, "role_id": process_role.pk, "responsibility": "R"}]}, format="json", HTTP_X_TENANT=tenant_acme.slug)
        # Second upsert with different entry
        resp = api_client.put(url, data={"entries": [{"step_id": op_step.pk, "role_id": process_role.pk, "responsibility": "A"}]}, format="json", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        assert resp.json()["count"] == 1
        assert resp.json()["results"][0]["responsibility"] == "A"


@pytest.mark.django_db
class TestProcessSubResources:
    def test_get_kpis(self, api_client, alice_admin, tenant_acme, process, kpi):
        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}processes/{process.public_id}/kpis/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["count"] == 1
        assert resp.json()["results"][0]["code"] == "KPI-T-01"

    def test_get_rules(self, api_client, alice_admin, tenant_acme, process, rule):
        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}processes/{process.public_id}/rules/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["count"] == 1
        assert resp.json()["results"][0]["code"] == "R1"

    def test_get_control_points(self, api_client, alice_admin, tenant_acme, process, control_point):
        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}processes/{process.public_id}/control-points/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["count"] == 1
        assert resp.json()["results"][0]["code"] == "CP1"

    def test_get_io_empty(self, api_client, alice_admin, tenant_acme, process):
        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}processes/{process.public_id}/io/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["count"] == 0


# ---------------------------------------------------------------------------
# Instance views
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestInstanceListCreate:
    def test_create_instance(self, api_client, alice_admin, tenant_acme, process, op_step):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/",
            data={
                "process_id": str(process.public_id),
                "title": "Q1 Run",
                "triggered_by": "scheduler",
            },
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 201
        body = resp.json()
        assert body["title"] == "Q1 Run"
        assert body["process_id"] == str(process.public_id)
        # Auto-created steps
        assert len(body["steps"]) == 1  # one op_step fixture

    def test_create_instance_invalid_process(self, api_client, alice_admin, tenant_acme):
        import uuid

        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/",
            data={"process_id": str(uuid.uuid4()), "title": "X"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 404

    def test_list_instances(self, api_client, alice_admin, tenant_acme, instance):
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}instances/", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        assert resp.json()["count"] >= 1

    def test_get_instance_detail(self, api_client, alice_admin, tenant_acme, instance):
        api_client.force_login(alice_admin)
        resp = api_client.get(
            f"{BASE}instances/{instance.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["id"] == str(instance.public_id)


@pytest.mark.django_db
class TestInstanceStepUpdate:
    def test_update_step_status(
        self, api_client, alice_admin, tenant_acme, instance, op_step
    ):
        from simorgh.apps.bpm.models import ProcessInstanceStep

        inst_step = ProcessInstanceStep.objects.create(
            instance=instance,
            step=op_step,
            status=StepStatus.PENDING,
        )
        api_client.force_login(alice_admin)
        resp = api_client.patch(
            f"{BASE}instances/{instance.public_id}/steps/{op_step.pk}/",
            data={"status": "in_progress"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["status"] == "in_progress"

    def test_update_step_not_found(self, api_client, alice_admin, tenant_acme, instance):
        api_client.force_login(alice_admin)
        resp = api_client.patch(
            f"{BASE}instances/{instance.public_id}/steps/9999/",
            data={"status": "done"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 404


@pytest.mark.django_db
class TestControlPointExecution:
    def test_execute_control_point(
        self, api_client, alice_admin, tenant_acme, instance, control_point
    ):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/{instance.public_id}/control-points/{control_point.code}/execute/",
            data={"outcome": "passed", "notes": "All clear"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 201
        assert resp.json()["outcome"] == "passed"

    def test_execute_invalid_outcome(
        self, api_client, alice_admin, tenant_acme, instance, control_point
    ):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/{instance.public_id}/control-points/{control_point.code}/execute/",
            data={"outcome": "invalid_value"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 400

    def test_execute_cp_idempotent(
        self, api_client, alice_admin, tenant_acme, instance, control_point
    ):
        """Re-executing a CP should update (upsert), not create a duplicate."""
        api_client.force_login(alice_admin)
        url = f"{BASE}instances/{instance.public_id}/control-points/{control_point.code}/execute/"
        api_client.post(url, data={"outcome": "passed"}, format="json", HTTP_X_TENANT=tenant_acme.slug)
        resp = api_client.post(url, data={"outcome": "failed"}, format="json", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 201
        from simorgh.apps.bpm.models import ControlPointExecution
        count = ControlPointExecution.objects.filter(instance=instance, control_point=control_point).count()
        assert count == 1
        assert ControlPointExecution.objects.get(instance=instance, control_point=control_point).outcome == "failed"


@pytest.mark.django_db
class TestKPIMeasurement:
    def test_record_measurement_target_met(
        self, api_client, alice_admin, tenant_acme, instance, kpi
    ):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/{instance.public_id}/kpis/{kpi.code}/measure/",
            data={"value": "90.0", "notes": "Good performance"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 201
        body = resp.json()
        assert body["kpi_code"] == "KPI-T-01"
        assert body["is_target_met"] is True

    def test_record_measurement_below_target_creates_alert(
        self, api_client, alice_admin, tenant_acme, instance, kpi
    ):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/{instance.public_id}/kpis/{kpi.code}/measure/",
            data={"value": "50.0"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 201
        assert resp.json()["is_target_met"] is False

        from simorgh.apps.bpm.models import KPIAlert

        assert KPIAlert.objects.filter(kpi=kpi).exists()

    def test_record_measurement_missing_value(
        self, api_client, alice_admin, tenant_acme, instance, kpi
    ):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/{instance.public_id}/kpis/{kpi.code}/measure/",
            data={"notes": "forgot value"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 400

    def test_record_measurement_invalid_kpi(
        self, api_client, alice_admin, tenant_acme, instance
    ):
        api_client.force_login(alice_admin)
        resp = api_client.post(
            f"{BASE}instances/{instance.public_id}/kpis/NOEXIST/measure/",
            data={"value": "80.0"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# KPI Dashboard
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestKPIDashboard:
    def test_dashboard_structure(self, api_client, alice_admin, tenant_acme, process, kpi):
        api_client.force_login(alice_admin)
        resp = api_client.get(f"{BASE}kpis/dashboard/", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        body = resp.json()
        assert "total_kpis" in body
        assert "by_process" in body
        assert "by_category" in body
        assert "recent_alerts" in body
        assert body["total_kpis"] >= 1

    def test_dashboard_no_tenant_returns_400(self, api_client, alice, db):
        api_client.force_login(alice)
        resp = api_client.get(f"{BASE}kpis/dashboard/")
        assert resp.status_code == 400
