"""BPM Phase 16 — Process Instance integration tests.

Covers:
- Instance lifecycle (PLANNED → IN_PROGRESS → COMPLETED)
- Control point gate execution and idempotency
- Rule compliance check recording
"""

from __future__ import annotations

import decimal

import pytest

from simorgh.apps.bpm.models import (
    ControlPointExecution,
    ControlPointExecutionOutcome,
    ControlPointStage,
    InstanceStatus,
    PCFFramework,
    ProcessControlPoint,
    ProcessDefinition,
    ProcessInstance,
    ProcessInstanceRuleCheck,
    ProcessInstanceStep,
    ProcessOperationalStep,
    ProcessRole,
    ProcessRule,
    StepStatus,
)
from simorgh.apps.bpm.selectors import (
    get_instance,
    list_instance_cp_executions,
    list_instance_rule_checks,
    list_instance_steps,
    list_instances,
)
from simorgh.apps.bpm.services import (
    create_instance,
    execute_control_point,
    update_instance_step,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def framework(db) -> PCFFramework:
    return PCFFramework.objects.create(
        code="PCF-INST-TEST",
        name="Instance 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.1",
        level=3,
        name="Instance Test Process",
        name_fa="فرآیند تست نمونه",
        status="active",
        version="1.0",
    )


@pytest.fixture
def step1(process) -> ProcessOperationalStep:
    return ProcessOperationalStep.objects.create(
        process=process,
        step_number="1",
        title="Initiation",
        order=1,
    )


@pytest.fixture
def step2(process) -> ProcessOperationalStep:
    return ProcessOperationalStep.objects.create(
        process=process,
        step_number="2",
        title="Execution",
        order=2,
    )


@pytest.fixture
def step3(process) -> ProcessOperationalStep:
    return ProcessOperationalStep.objects.create(
        process=process,
        step_number="3",
        title="Closure",
        order=3,
    )


@pytest.fixture
def role_pm(process) -> ProcessRole:
    return ProcessRole.objects.create(
        process=process,
        code="PM",
        name="Process Manager",
        order=1,
    )


@pytest.fixture
def control_point(process) -> ProcessControlPoint:
    return ProcessControlPoint.objects.create(
        process=process,
        code="CP1",
        stage=ControlPointStage.DURING,
        description="Mid-process gate review",
        order=1,
    )


@pytest.fixture
def business_rule(process) -> ProcessRule:
    return ProcessRule.objects.create(
        process=process,
        code="R1",
        description="All input documents must be reviewed before proceeding",
        source="Policy P-001",
        is_configurable=False,
        order=1,
    )


@pytest.fixture
def instance(tenant_acme, process, alice, step1, step2, step3) -> ProcessInstance:
    """Create a planned instance with 3 step trackers seeded."""
    return create_instance(
        tenant=tenant_acme,
        process=process,
        validated_data={
            "title": "Q1 Strategic Planning Run",
            "triggered_by": "Annual cycle",
        },
        user=alice,
    )


# ---------------------------------------------------------------------------
# test_process_instance_lifecycle
# ---------------------------------------------------------------------------


class TestProcessInstanceLifecycle:
    """Tests for ProcessInstance status transitions."""

    def test_instance_created_as_planned(self, instance):
        """Newly created instances start in PLANNED status."""
        assert instance.status == InstanceStatus.PLANNED

    def test_instance_number_auto_generated(self, instance):
        """instance_number is auto-generated in <code>-<year>-<seq> format."""
        assert instance.instance_number
        assert instance.instance_number.startswith("111-")

    def test_advance_status_planned_to_in_progress(self, instance):
        """advance_status() moves PLANNED → IN_PROGRESS."""
        instance.advance_status()
        instance.refresh_from_db()
        assert instance.status == InstanceStatus.IN_PROGRESS
        assert instance.start_date is not None

    def test_advance_status_in_progress_to_completed(self, instance):
        """Two advance_status() calls move through PLANNED → IN_PROGRESS → COMPLETED."""
        instance.advance_status()
        instance.advance_status()
        instance.refresh_from_db()
        assert instance.status == InstanceStatus.COMPLETED
        assert instance.actual_end_date is not None

    def test_instance_steps_seeded_on_creation(self, instance, step1, step2, step3):
        """create_instance() seeds one ProcessInstanceStep per operational step."""
        steps = ProcessInstanceStep.objects.filter(instance=instance)
        assert steps.count() == 3

    def test_all_seeded_steps_are_pending(self, instance):
        """All seeded steps start in PENDING status."""
        statuses = ProcessInstanceStep.objects.filter(
            instance=instance
        ).values_list("status", flat=True)
        assert all(s == StepStatus.PENDING for s in statuses)

    def test_update_instance_step_status(self, instance, step1):
        """update_instance_step() changes step status."""
        inst_step = ProcessInstanceStep.objects.get(
            instance=instance, step=step1
        )
        update_instance_step(
            instance, inst_step.step.pk, {"status": StepStatus.IN_PROGRESS}
        )
        inst_step.refresh_from_db()
        assert inst_step.status == StepStatus.IN_PROGRESS

    def test_update_instance_step_notes(self, instance, step1, alice):
        """update_instance_step() stores notes and assigned_to."""
        inst_step = ProcessInstanceStep.objects.get(
            instance=instance, step=step1
        )
        update_instance_step(
            instance, inst_step.step.pk,
            {"status": StepStatus.DONE, "notes": "Completed ahead of schedule", "assigned_to": alice}
        )
        inst_step.refresh_from_db()
        assert inst_step.notes == "Completed ahead of schedule"
        assert inst_step.assigned_to == alice

    def test_step_start_marks_in_progress(self, instance, step1):
        """ProcessInstanceStep.start() sets status=IN_PROGRESS and started_at."""
        inst_step = ProcessInstanceStep.objects.get(
            instance=instance, step=step1
        )
        inst_step.start()
        inst_step.refresh_from_db()
        assert inst_step.status == StepStatus.IN_PROGRESS
        assert inst_step.started_at is not None

    def test_step_complete_marks_done(self, instance, step1):
        """ProcessInstanceStep.complete() sets status=DONE and completed_at."""
        inst_step = ProcessInstanceStep.objects.get(
            instance=instance, step=step1
        )
        inst_step.start()
        inst_step.complete()
        inst_step.refresh_from_db()
        assert inst_step.status == StepStatus.DONE
        assert inst_step.completed_at is not None

    def test_instance_tenant_isolation(self, tenant_acme, tenant_globex, framework, alice):
        """list_instances() filters by tenant."""
        globex_process = ProcessDefinition.objects.create(
            tenant=tenant_globex,
            framework=framework,
            hierarchy_id="2",
            level=1,
            name="Globex Process",
            name_fa="فرآیند گلوبکس",
            status="active",
            version="1.0",
        )
        ProcessInstance.objects.create(
            tenant=tenant_globex,
            process=globex_process,
            title="Globex Run",
        )
        acme_instances = list_instances(tenant_acme)
        globex_instances = list_instances(tenant_globex)
        # Acme instances should not include Globex ones
        assert not acme_instances.filter(process=globex_process).exists()
        assert globex_instances.filter(process=globex_process).exists()

    def test_instance_cancelled_status(self, instance):
        """An instance can be manually set to CANCELLED."""
        instance.status = InstanceStatus.CANCELLED
        instance.save(update_fields=["status"])
        instance.refresh_from_db()
        assert instance.status == InstanceStatus.CANCELLED

    def test_list_instance_steps_selector(self, instance, step1, step2, step3):
        """list_instance_steps() returns all steps for the instance."""
        steps = list_instance_steps(instance)
        assert steps.count() == 3

    def test_sequential_instance_numbers(self, tenant_acme, process, alice, step1):
        """Sequential instances of the same process get incremental numbers."""
        i1 = create_instance(tenant_acme, process, {"title": "Run 1"}, alice)
        i2 = create_instance(tenant_acme, process, {"title": "Run 2"}, alice)
        # Both should have the same process code prefix but different seq
        assert i1.instance_number != i2.instance_number
        assert i1.instance_number.startswith("111-")
        assert i2.instance_number.startswith("111-")


# ---------------------------------------------------------------------------
# test_control_point_execution
# ---------------------------------------------------------------------------


class TestControlPointExecution:
    """Tests for control point gate review during execution."""

    def test_control_point_can_be_created(self, process):
        """ProcessControlPoint can be added to a process."""
        cp = ProcessControlPoint.objects.create(
            process=process,
            code="CP-NEW",
            stage=ControlPointStage.AFTER,
            description="Post-process sign-off",
            order=10,
        )
        assert cp.pk is not None
        assert cp.process == process

    def test_execute_control_point_passed(
        self, instance, control_point, alice
    ):
        """execute_control_point() creates a PASSED execution record."""
        execution = execute_control_point(
            instance, control_point,
            outcome=ControlPointExecutionOutcome.PASSED,
            user=alice,
            notes="All inputs verified",
        )
        assert execution.outcome == ControlPointExecutionOutcome.PASSED
        assert execution.executed_by == alice
        assert execution.notes == "All inputs verified"

    def test_execute_control_point_failed(
        self, instance, control_point, alice
    ):
        """execute_control_point() creates a FAILED execution record."""
        execution = execute_control_point(
            instance, control_point,
            outcome=ControlPointExecutionOutcome.FAILED,
            user=alice,
        )
        assert execution.outcome == ControlPointExecutionOutcome.FAILED

    def test_execute_control_point_deferred(
        self, instance, control_point, alice
    ):
        """execute_control_point() creates a DEFERRED execution record."""
        execution = execute_control_point(
            instance, control_point,
            outcome=ControlPointExecutionOutcome.DEFERRED,
            user=alice,
        )
        assert execution.outcome == ControlPointExecutionOutcome.DEFERRED

    def test_execute_control_point_is_idempotent(
        self, instance, control_point, alice
    ):
        """Calling execute_control_point() twice updates the same row."""
        execute_control_point(
            instance, control_point,
            outcome=ControlPointExecutionOutcome.PASSED, user=alice
        )
        execute_control_point(
            instance, control_point,
            outcome=ControlPointExecutionOutcome.FAILED, user=alice,
            notes="Re-evaluated"
        )
        executions = ControlPointExecution.objects.filter(
            instance=instance, control_point=control_point
        )
        assert executions.count() == 1
        assert executions.first().outcome == ControlPointExecutionOutcome.FAILED

    def test_list_instance_cp_executions_selector(
        self, instance, control_point, alice
    ):
        """list_instance_cp_executions() returns CP executions for an instance."""
        execute_control_point(
            instance, control_point,
            outcome=ControlPointExecutionOutcome.PASSED, user=alice
        )
        executions = list_instance_cp_executions(instance)
        assert executions.count() == 1

    def test_cp_unique_constraint(
        self, instance, control_point, alice
    ):
        """Direct duplicate (instance, control_point) pair raises IntegrityError."""
        from django.db import IntegrityError

        ControlPointExecution.objects.create(
            instance=instance,
            control_point=control_point,
            outcome=ControlPointExecutionOutcome.PASSED,
            executed_by=alice,
        )
        with pytest.raises(IntegrityError):
            ControlPointExecution.objects.create(
                instance=instance,
                control_point=control_point,
                outcome=ControlPointExecutionOutcome.PASSED,
                executed_by=alice,
            )

    def test_cp_code_unique_per_process(self, process):
        """Two CPs with the same code in same process raise IntegrityError."""
        from django.db import IntegrityError

        ProcessControlPoint.objects.create(
            process=process, code="CP-DUP",
            stage=ControlPointStage.DURING,
            description="First", order=1,
        )
        with pytest.raises(IntegrityError):
            ProcessControlPoint.objects.create(
                process=process, code="CP-DUP",
                stage=ControlPointStage.AFTER,
                description="Second", order=2,
            )

    def test_cp_execution_str_representation(
        self, instance, control_point, alice
    ):
        """ControlPointExecution __str__ is human-readable."""
        execution = execute_control_point(
            instance, control_point,
            outcome=ControlPointExecutionOutcome.PASSED, user=alice
        )
        s = str(execution)
        assert "CP1" in s


# ---------------------------------------------------------------------------
# test_rule_compliance_check
# ---------------------------------------------------------------------------


class TestRuleComplianceCheck:
    """Tests for recording rule compliance checks on instances."""

    def test_record_compliant_check(self, instance, business_rule, alice):
        """Compliance check with is_compliant=True is stored."""
        check = ProcessInstanceRuleCheck.objects.create(
            instance=instance,
            rule=business_rule,
            is_compliant=True,
            checked_by=alice,
            notes="All documents present",
        )
        assert check.is_compliant is True
        assert check.rule == business_rule

    def test_record_non_compliant_check(self, instance, business_rule, alice):
        """Compliance check with is_compliant=False is stored."""
        check = ProcessInstanceRuleCheck.objects.create(
            instance=instance,
            rule=business_rule,
            is_compliant=False,
            checked_by=alice,
            notes="Missing contract annexure",
        )
        assert check.is_compliant is False

    def test_rule_check_unique_per_instance_rule(
        self, instance, business_rule, alice
    ):
        """Duplicate (instance, rule) pair raises IntegrityError."""
        from django.db import IntegrityError

        ProcessInstanceRuleCheck.objects.create(
            instance=instance,
            rule=business_rule,
            is_compliant=True,
            checked_by=alice,
        )
        with pytest.raises(IntegrityError):
            ProcessInstanceRuleCheck.objects.create(
                instance=instance,
                rule=business_rule,
                is_compliant=False,
                checked_by=alice,
            )

    def test_list_instance_rule_checks_selector(
        self, instance, business_rule, process, alice
    ):
        """list_instance_rule_checks() returns checks for the instance."""
        # Create a second rule
        rule2 = ProcessRule.objects.create(
            process=process,
            code="R2",
            description="Second rule",
            order=2,
        )
        ProcessInstanceRuleCheck.objects.create(
            instance=instance, rule=business_rule, is_compliant=True
        )
        ProcessInstanceRuleCheck.objects.create(
            instance=instance, rule=rule2, is_compliant=False
        )
        checks = list_instance_rule_checks(instance)
        assert checks.count() == 2

    def test_rule_compliance_rate(self, instance, business_rule, process, alice):
        """Compliance rate (compliant / total) can be computed from the checks."""
        rules = [business_rule]
        for i in range(2, 5):
            rules.append(
                ProcessRule.objects.create(
                    process=process, code=f"R{i}",
                    description=f"Rule {i}", order=i,
                )
            )
        compliant_count = 3
        for i, rule in enumerate(rules):
            ProcessInstanceRuleCheck.objects.create(
                instance=instance,
                rule=rule,
                is_compliant=(i < compliant_count),
                checked_by=alice,
            )
        checks = list_instance_rule_checks(instance)
        total = checks.count()
        compliant = checks.filter(is_compliant=True).count()
        rate = compliant / total
        assert total == 4
        assert compliant == 3
        assert rate == pytest.approx(0.75)

    def test_rule_check_str_representation(
        self, instance, business_rule, alice
    ):
        """ProcessInstanceRuleCheck __str__ is human-readable."""
        check = ProcessInstanceRuleCheck.objects.create(
            instance=instance,
            rule=business_rule,
            is_compliant=True,
            checked_by=alice,
        )
        s = str(check)
        assert "R1" in s

    def test_rule_code_unique_per_process(self, process):
        """Two rules with the same code in same process raise IntegrityError."""
        from django.db import IntegrityError

        ProcessRule.objects.create(
            process=process, code="R-DUP",
            description="Original", order=1,
        )
        with pytest.raises(IntegrityError):
            ProcessRule.objects.create(
                process=process, code="R-DUP",
                description="Duplicate", order=2,
            )

    def test_get_instance_selector(self, tenant_acme, instance):
        """get_instance() retrieves the instance by public_id and tenant."""
        fetched = get_instance(instance.public_id, tenant_acme)
        assert fetched.pk == instance.pk

    def test_full_instance_flow(
        self, tenant_acme, process, alice,
        step1, step2, step3, control_point, business_rule
    ):
        """End-to-end: create → start → execute steps → CP → rule check → complete."""
        # 1. Create instance
        inst = create_instance(
            tenant=tenant_acme,
            process=process,
            validated_data={"title": "Full Flow Test"},
            user=alice,
        )
        assert inst.status == InstanceStatus.PLANNED

        # 2. Start
        inst.advance_status()
        assert inst.status == InstanceStatus.IN_PROGRESS

        # 3. Execute steps
        for op_step in [step1, step2, step3]:
            inst_step = ProcessInstanceStep.objects.get(
                instance=inst, step=op_step
            )
            inst_step.start()
            inst_step.complete()

        # 4. Execute control point
        execution = execute_control_point(
            inst, control_point,
            outcome=ControlPointExecutionOutcome.PASSED,
            user=alice,
        )
        assert execution.outcome == ControlPointExecutionOutcome.PASSED

        # 5. Record rule compliance
        check = ProcessInstanceRuleCheck.objects.create(
            instance=inst,
            rule=business_rule,
            is_compliant=True,
            checked_by=alice,
        )
        assert check.is_compliant is True

        # 6. Complete instance
        inst.advance_status()
        inst.refresh_from_db()
        assert inst.status == InstanceStatus.COMPLETED
        assert inst.actual_end_date is not None

        # 7. Verify all steps done
        all_steps = list_instance_steps(inst)
        assert all(s.status == StepStatus.DONE for s in all_steps)
