"""BPM Phase 16 — RACI Matrix integration tests.

Covers:
- Single-Accountable rule (at most one 'A' per activity)
- Bulk upsert RACI entries via service
- RACI data export structure
"""

from __future__ import annotations

import pytest

from simorgh.apps.bpm.models import (
    PCFFramework,
    ProcessDefinition,
    ProcessOperationalStep,
    ProcessRole,
    RACIEntry,
    RACIMatrix,
    RACIResponsibility,
)
from simorgh.apps.bpm.selectors import list_process_raci
from simorgh.apps.bpm.services import bulk_upsert_raci


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def framework(db) -> PCFFramework:
    return PCFFramework.objects.create(
        code="PCF-RACI-TEST",
        name="RACI 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="Strategy Process",
        name_fa="فرآیند استراتژی",
        status="active",
        version="1.0",
    )


@pytest.fixture
def step1(process) -> ProcessOperationalStep:
    return ProcessOperationalStep.objects.create(
        process=process,
        step_number="1",
        title="Define objectives",
        order=1,
    )


@pytest.fixture
def step2(process) -> ProcessOperationalStep:
    return ProcessOperationalStep.objects.create(
        process=process,
        step_number="2",
        title="Analyse environment",
        order=2,
    )


@pytest.fixture
def role_ceo(process) -> ProcessRole:
    return ProcessRole.objects.create(
        process=process,
        code="CEO",
        name="Chief Executive Officer",
        order=1,
    )


@pytest.fixture
def role_sm(process) -> ProcessRole:
    return ProcessRole.objects.create(
        process=process,
        code="SM",
        name="Strategy Manager",
        order=2,
    )


@pytest.fixture
def role_hr(process) -> ProcessRole:
    return ProcessRole.objects.create(
        process=process,
        code="HR",
        name="HR Director",
        order=3,
    )


@pytest.fixture
def raci_matrix(process) -> RACIMatrix:
    return RACIMatrix.objects.create(
        process=process,
        version="1.0",
        is_current=True,
    )


# ---------------------------------------------------------------------------
# test_raci_single_accountable
# ---------------------------------------------------------------------------


class TestRACISingleAccountable:
    """Tests that at-most-one-Accountable-per-activity is enforced."""

    def test_single_accountable_allowed(self, raci_matrix, step1, role_ceo):
        """Creating one Accountable entry succeeds."""
        entry = RACIEntry.objects.create(
            matrix=raci_matrix,
            step=step1,
            activity_label=step1.title,
            role=role_ceo,
            responsibility=RACIResponsibility.ACCOUNTABLE,
        )
        assert entry.pk is not None
        assert entry.responsibility == "A"

    def test_multiple_roles_different_responsibilities(
        self, raci_matrix, step1, role_ceo, role_sm, role_hr
    ):
        """R, A, C, I can all be assigned to different roles for one activity."""
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility=RACIResponsibility.ACCOUNTABLE,
        )
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_sm,
            responsibility=RACIResponsibility.RESPONSIBLE,
        )
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_hr,
            responsibility=RACIResponsibility.CONSULTED,
        )
        entries = RACIEntry.objects.filter(matrix=raci_matrix, step=step1)
        assert entries.count() == 3
        accountable = entries.filter(responsibility=RACIResponsibility.ACCOUNTABLE)
        assert accountable.count() == 1

    def test_duplicate_role_activity_pair_raises(self, raci_matrix, step1, role_ceo):
        """Duplicate (matrix, activity_label, role) pair raises IntegrityError."""
        from django.db import IntegrityError

        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility=RACIResponsibility.RESPONSIBLE,
        )
        with pytest.raises(IntegrityError):
            RACIEntry.objects.create(
                matrix=raci_matrix, step=step1,
                activity_label=step1.title, role=role_ceo,
                responsibility=RACIResponsibility.ACCOUNTABLE,
            )

    def test_same_role_different_activities(self, raci_matrix, step1, step2, role_ceo):
        """The same role can appear in multiple activities with any responsibility."""
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility=RACIResponsibility.ACCOUNTABLE,
        )
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step2,
            activity_label=step2.title, role=role_ceo,
            responsibility=RACIResponsibility.ACCOUNTABLE,
        )
        assert RACIEntry.objects.filter(
            matrix=raci_matrix, role=role_ceo,
            responsibility=RACIResponsibility.ACCOUNTABLE,
        ).count() == 2

    def test_only_one_current_matrix_per_process(self, process):
        """Saving a new is_current=True matrix flips the previous one."""
        m1 = RACIMatrix.objects.create(
            process=process, version="1.0", is_current=True
        )
        m2 = RACIMatrix.objects.create(
            process=process, version="2.0", is_current=True
        )
        m1.refresh_from_db()
        assert m1.is_current is False
        assert m2.is_current is True

    def test_activity_label_auto_filled_from_step(self, raci_matrix, step1, role_ceo):
        """activity_label is auto-filled from step.title when empty."""
        entry = RACIEntry(
            matrix=raci_matrix,
            step=step1,
            activity_label="",  # empty — should be auto-filled
            role=role_ceo,
            responsibility=RACIResponsibility.RESPONSIBLE,
        )
        entry.save()
        assert entry.activity_label == step1.title

    def test_free_text_activity_without_step(self, raci_matrix, role_ceo):
        """An entry without a step FK but with activity_label is valid."""
        entry = RACIEntry.objects.create(
            matrix=raci_matrix,
            step=None,
            activity_label="Ad-hoc Review",
            role=role_ceo,
            responsibility=RACIResponsibility.INFORMED,
        )
        assert entry.step is None
        assert entry.activity_label == "Ad-hoc Review"


# ---------------------------------------------------------------------------
# test_raci_bulk_upsert
# ---------------------------------------------------------------------------


class TestRACIBulkUpsert:
    """Tests for bulk_upsert_raci service."""

    def test_bulk_upsert_creates_matrix_and_entries(
        self, process, step1, step2, role_ceo, role_sm
    ):
        """bulk_upsert_raci creates a new RACIMatrix and populates entries."""
        entries_data = [
            {
                "step_id": step1.pk,
                "role_id": role_ceo.pk,
                "responsibility": "A",
                "activity_label": step1.title,
            },
            {
                "step_id": step1.pk,
                "role_id": role_sm.pk,
                "responsibility": "R",
                "activity_label": step1.title,
            },
            {
                "step_id": step2.pk,
                "role_id": role_ceo.pk,
                "responsibility": "I",
                "activity_label": step2.title,
            },
        ]
        created = bulk_upsert_raci(process, entries_data)
        assert len(created) == 3

    def test_bulk_upsert_clears_existing_entries(
        self, process, step1, role_ceo, role_sm, raci_matrix
    ):
        """Calling bulk_upsert_raci a second time replaces all entries."""
        # Seed initial entries
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility="A",
        )
        assert RACIEntry.objects.filter(matrix=raci_matrix).count() == 1

        # Upsert with a different set
        new_entries = [
            {
                "step_id": step1.pk,
                "role_id": role_sm.pk,
                "responsibility": "R",
                "activity_label": step1.title,
            },
        ]
        bulk_upsert_raci(process, new_entries)
        matrix = RACIMatrix.objects.get(process=process)
        assert RACIEntry.objects.filter(matrix=matrix).count() == 1
        assert RACIEntry.objects.filter(
            matrix=matrix, role=role_sm, responsibility="R"
        ).exists()

    def test_bulk_upsert_returns_entry_instances(
        self, process, step1, role_ceo
    ):
        """bulk_upsert_raci returns a list of RACIEntry model instances."""
        entries_data = [
            {
                "step_id": step1.pk,
                "role_id": role_ceo.pk,
                "responsibility": "A",
                "activity_label": step1.title,
            }
        ]
        result = bulk_upsert_raci(process, entries_data)
        assert all(isinstance(e, RACIEntry) for e in result)

    def test_bulk_upsert_empty_list_clears_matrix(
        self, process, step1, role_ceo, raci_matrix
    ):
        """Calling bulk_upsert_raci with an empty list removes all entries."""
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility="R",
        )
        bulk_upsert_raci(process, [])
        matrix = RACIMatrix.objects.get(process=process)
        assert RACIEntry.objects.filter(matrix=matrix).count() == 0

    def test_bulk_upsert_with_notes(self, process, step1, role_ceo):
        """Notes field is stored correctly in bulk upsert."""
        entries_data = [
            {
                "step_id": step1.pk,
                "role_id": role_ceo.pk,
                "responsibility": "A",
                "activity_label": step1.title,
                "notes": "CEO must approve all strategic decisions",
            }
        ]
        result = bulk_upsert_raci(process, entries_data)
        assert result[0].notes == "CEO must approve all strategic decisions"


# ---------------------------------------------------------------------------
# test_raci_export
# ---------------------------------------------------------------------------


class TestRACIExport:
    """Tests for RACI data export / serialization."""

    def test_list_process_raci_selector(
        self, process, raci_matrix, step1, step2, role_ceo, role_sm
    ):
        """list_process_raci() returns all entries for the process."""
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility="A",
        )
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_sm,
            responsibility="R",
        )
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step2,
            activity_label=step2.title, role=role_ceo,
            responsibility="I",
        )
        qs = list_process_raci(process)
        assert qs.count() == 3

    def test_raci_entries_grouped_by_step(
        self, process, raci_matrix, step1, step2, role_ceo, role_sm
    ):
        """RACI entries can be filtered by step."""
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility="A",
        )
        RACIEntry.objects.create(
            matrix=raci_matrix, step=step2,
            activity_label=step2.title, role=role_sm,
            responsibility="R",
        )
        step1_entries = list_process_raci(process).filter(step=step1)
        assert step1_entries.count() == 1
        assert step1_entries.first().responsibility == "A"

    def test_raci_responsibility_choices(self, raci_matrix, step1, role_ceo, role_sm, role_hr):
        """All four RACI responsibilities can be created."""
        responsibilities = ["R", "A", "C", "I"]
        roles = [role_sm, role_ceo, role_hr]

        # Create R, A, C on step1 with 3 roles
        for i, (resp, role) in enumerate(zip(responsibilities[:3], roles)):
            RACIEntry.objects.create(
                matrix=raci_matrix, step=step1,
                activity_label=step1.title, role=role,
                responsibility=resp,
            )

        # Create I for role_sm on a free-text activity
        RACIEntry.objects.create(
            matrix=raci_matrix, step=None,
            activity_label="Ad-hoc task", role=role_sm,
            responsibility="I",
        )

        assert RACIEntry.objects.filter(matrix=raci_matrix).count() == 4

    def test_export_as_pivot_structure(
        self, process, raci_matrix, step1, step2, role_ceo, role_sm
    ):
        """RACI data can be pivoted into a {activity: {role: resp}} dict."""
        entries = [
            RACIEntry.objects.create(
                matrix=raci_matrix, step=step1,
                activity_label=step1.title, role=role_ceo,
                responsibility="A",
            ),
            RACIEntry.objects.create(
                matrix=raci_matrix, step=step1,
                activity_label=step1.title, role=role_sm,
                responsibility="R",
            ),
            RACIEntry.objects.create(
                matrix=raci_matrix, step=step2,
                activity_label=step2.title, role=role_ceo,
                responsibility="I",
            ),
        ]
        # Build pivot
        pivot: dict[str, dict[str, str]] = {}
        for e in entries:
            pivot.setdefault(e.activity_label, {})[e.role.code] = e.responsibility

        assert pivot[step1.title]["CEO"] == "A"
        assert pivot[step1.title]["SM"] == "R"
        assert pivot[step2.title]["CEO"] == "I"
        # Only one Accountable per activity
        assert sum(1 for r in pivot[step1.title].values() if r == "A") == 1

    def test_matrix_str_representation(self, raci_matrix):
        """RACIMatrix __str__ is human-readable."""
        s = str(raci_matrix)
        assert "1.1" in s
        assert "1.0" in s

    def test_entry_str_includes_responsibility(
        self, raci_matrix, step1, role_ceo
    ):
        """RACIEntry __str__ includes process hierarchy_id, activity, role and responsibility."""
        entry = RACIEntry.objects.create(
            matrix=raci_matrix, step=step1,
            activity_label=step1.title, role=role_ceo,
            responsibility="A",
        )
        s = str(entry)
        assert "A" in s or "→" in s
