"""Tests for Phase 5 — workflow engine: registry, FSM, approvals, triggers, API."""

from __future__ import annotations

import pytest
from django.urls import reverse

from simorgh.apps.events.bus import clear_subscribers, dispatch, register_event
from simorgh.apps.workflow.engine import (
    fire_for_event,
    fire_transition,
    record_approval,
    start_instance,
)
from simorgh.apps.workflow.evaluator import evaluate_conditions
from simorgh.apps.workflow.models import (
    WorkflowApproval,
    WorkflowApprovalDecision,
    WorkflowInstance,
    WorkflowInstanceStatus,
    WorkflowTransitionLog,
)
from simorgh.apps.workflow.registry import (
    ActionContext,
    ConditionSpec,
    StateSpec,
    TransitionSpec,
    WorkflowDefinition,
    WorkflowError,
    deserialize,
    get_action_handler,
    get_workflow,
    list_workflows,
    register_action,
    reset_actions_for_tests,
    reset_registry_for_tests,
)
from simorgh.apps.workflow.triggers import (
    register_workflow as register_workflow_with_triggers,
)
from simorgh.apps.workflow.triggers import (
    reset_triggers_for_tests,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def _workflow_registry():
    reset_registry_for_tests()
    reset_actions_for_tests()
    reset_triggers_for_tests()
    yield
    reset_registry_for_tests()
    reset_actions_for_tests()
    reset_triggers_for_tests()


@pytest.fixture
def lead_workflow(_workflow_registry):
    return register_workflow_with_triggers(
        WorkflowDefinition(
            name="crm.lead.review",
            version=1,
            subject_type="crm.lead",
            label_key="crm.lead.review.label",
            states=(
                StateSpec(
                    name="new",
                    label_key="crm.lead.state.new",
                    is_initial=True,
                ),
                StateSpec(
                    name="qualified",
                    label_key="crm.lead.state.qualified",
                    approval_permission="workflow.approval.grant",
                ),
                StateSpec(
                    name="won",
                    label_key="crm.lead.state.won",
                    is_final=True,
                ),
                StateSpec(
                    name="lost",
                    label_key="crm.lead.state.lost",
                    is_final=True,
                ),
            ),
            transitions=(
                TransitionSpec(
                    name="qualify",
                    from_state="new",
                    to_state="qualified",
                    label_key="crm.lead.t.qualify",
                    permission="workflow.transition.fire",
                    conditions=(
                        ConditionSpec(
                            expression={"field": "instance.score", "op": "gte", "value": 10},
                        ),
                    ),
                ),
                TransitionSpec(
                    name="close_won",
                    from_state="qualified",
                    to_state="won",
                    label_key="crm.lead.t.won",
                    requires_approval=True,
                ),
                TransitionSpec(
                    name="close_lost",
                    from_state="qualified",
                    to_state="lost",
                    label_key="crm.lead.t.lost",
                ),
                TransitionSpec(
                    name="auto_disqualify",
                    from_state="new",
                    to_state="lost",
                    label_key="crm.lead.t.auto",
                    trigger="crm.lead.cooled_off",
                ),
            ),
        )
    )


@pytest.fixture
def _lead_event(_workflow_registry):
    register_event("crm.lead.cooled_off", description="lead inactivity")
    yield
    clear_subscribers("crm.lead.cooled_off")


# ---------------------------------------------------------------------------
# Registry / DSL
# ---------------------------------------------------------------------------
def test_register_and_lookup(lead_workflow):
    assert get_workflow("crm.lead.review") is lead_workflow
    assert lead_workflow in list_workflows()


def test_register_rejects_bad_name(_workflow_registry):
    with pytest.raises(WorkflowError):
        WorkflowDefinition(
            name="badname",
            version=1,
            subject_type="crm.lead",
            label_key="x",
            states=(StateSpec(name="s", label_key="x", is_initial=True),),
            transitions=(),
        )


def test_register_requires_exactly_one_initial(_workflow_registry):
    with pytest.raises(WorkflowError):
        WorkflowDefinition(
            name="crm.lead.bad",
            version=1,
            subject_type="crm.lead",
            label_key="x",
            states=(
                StateSpec(name="a", label_key="x"),
                StateSpec(name="b", label_key="x"),
            ),
            transitions=(),
        )


def test_transition_refs_unknown_state(_workflow_registry):
    with pytest.raises(WorkflowError):
        WorkflowDefinition(
            name="crm.lead.bad2",
            version=1,
            subject_type="crm.lead",
            label_key="x",
            states=(StateSpec(name="a", label_key="x", is_initial=True),),
            transitions=(
                TransitionSpec(
                    name="go", from_state="a", to_state="ghost", label_key="x",
                ),
            ),
        )


def test_idempotent_register(lead_workflow):
    again = register_workflow_with_triggers(lead_workflow)
    assert again is lead_workflow


def test_conflicting_redefine_rejected(lead_workflow):
    other = WorkflowDefinition(
        name="crm.lead.review",
        version=1,
        subject_type="crm.lead",
        label_key="other",
        states=(StateSpec(name="x", label_key="x", is_initial=True),),
        transitions=(),
    )
    with pytest.raises(WorkflowError):
        register_workflow_with_triggers(other)


def test_serialize_round_trip(lead_workflow):
    payload = lead_workflow.serialize()
    rebuilt = deserialize(payload)
    assert rebuilt == lead_workflow


# ---------------------------------------------------------------------------
# Action registry
# ---------------------------------------------------------------------------
def test_register_action_and_lookup(_workflow_registry):
    @register_action("noop")
    def handler(ctx: ActionContext) -> None:  # pragma: no cover - returns None
        return None

    assert get_action_handler("noop") is handler


def test_action_name_must_be_identifier(_workflow_registry):
    with pytest.raises(WorkflowError):

        @register_action("bad name")
        def _h(_ctx: ActionContext) -> None:
            return None


def test_missing_action_raises(_workflow_registry):
    with pytest.raises(WorkflowError):
        get_action_handler("not_registered")


# ---------------------------------------------------------------------------
# Condition evaluator
# ---------------------------------------------------------------------------
def _ctx(instance) -> ActionContext:
    return ActionContext(
        instance=instance,
        definition=None,  # type: ignore[arg-type]
        transition=None,
        actor=None,
        payload={},
    )


class _Instance:
    def __init__(self, data, subject=None):
        self.data = data
        self.subject = subject


def test_condition_field_lookup_and_ops():
    inst = _Instance({"score": 25, "tag": "vip"})
    conds = (
        ConditionSpec(expression={"field": "instance.score", "op": "gte", "value": 10}),
        ConditionSpec(expression={"field": "instance.tag", "op": "in", "value": ["vip", "trial"]}),
    )
    assert evaluate_conditions(conds, _ctx(inst)) is True


def test_condition_any_all_not():
    inst = _Instance({"score": 5})
    cond = ConditionSpec(
        expression={
            "any": [
                {"field": "instance.score", "op": "gte", "value": 10},
                {"not": {"field": "instance.score", "op": "lt", "value": 0}},
            ],
        },
    )
    assert evaluate_conditions((cond,), _ctx(inst)) is True


def test_condition_unknown_op_rejected():
    inst = _Instance({"score": 1})
    with pytest.raises(WorkflowError):
        evaluate_conditions(
            (ConditionSpec(expression={"field": "instance.score", "op": "weird", "value": 1}),),
            _ctx(inst),
        )


def test_condition_depth_limit():
    node: dict = {"field": "instance.score", "op": "eq", "value": 1}
    for _ in range(10):
        node = {"all": [node]}
    with pytest.raises(WorkflowError):
        evaluate_conditions((ConditionSpec(expression=node),), _ctx(_Instance({"score": 1})))


# ---------------------------------------------------------------------------
# Runtime — start, fire, guards, finals
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_start_instance_initial_state(lead_workflow, tenant_acme, acme_tree):
    inst = start_instance(
        lead_workflow,
        tenant=tenant_acme,
        organization_node=acme_tree["eu"],
        data={"score": 50},
    )
    assert inst.current_state == "new"
    assert inst.status == WorkflowInstanceStatus.ACTIVE


@pytest.mark.django_db
def test_fire_transition_happy_path(lead_workflow, tenant_acme, acme_tree, alice):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    fire_transition(inst, "qualify", actor=alice)
    inst.refresh_from_db()
    assert inst.current_state == "qualified"
    assert WorkflowTransitionLog.objects.filter(instance=inst).count() == 1


@pytest.mark.django_db
def test_fire_transition_blocked_by_guard(lead_workflow, tenant_acme, acme_tree, alice):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 1},
    )
    with pytest.raises(WorkflowError):
        fire_transition(inst, "qualify", actor=alice)


@pytest.mark.django_db
def test_fire_transition_wrong_source_state(lead_workflow, tenant_acme, acme_tree, alice):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    with pytest.raises(WorkflowError):
        fire_transition(inst, "close_lost", actor=alice)


@pytest.mark.django_db
def test_fire_transition_to_final_completes(lead_workflow, tenant_acme, acme_tree, alice):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    fire_transition(inst, "qualify", actor=alice)
    fire_transition(inst, "close_lost", actor=alice)
    inst.refresh_from_db()
    assert inst.current_state == "lost"
    assert inst.status == WorkflowInstanceStatus.COMPLETED
    assert inst.closed_at is not None


# ---------------------------------------------------------------------------
# Approvals
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_approval_required_blocks_fire(lead_workflow, tenant_acme, acme_tree, alice):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    fire_transition(inst, "qualify", actor=alice)
    with pytest.raises(WorkflowError):
        fire_transition(inst, "close_won", actor=alice)


@pytest.mark.django_db
def test_approval_consumed_on_fire(lead_workflow, tenant_acme, acme_tree, alice):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    fire_transition(inst, "qualify", actor=alice)
    approval = record_approval(
        inst,
        "close_won",
        actor=alice,
        decision=WorkflowApprovalDecision.APPROVED,
    )
    assert approval.consumed_at is None
    fire_transition(inst, "close_won", actor=alice)
    approval.refresh_from_db()
    assert approval.consumed_at is not None
    inst.refresh_from_db()
    assert inst.status == WorkflowInstanceStatus.COMPLETED


@pytest.mark.django_db
def test_record_approval_for_non_approval_transition_rejected(
    lead_workflow, tenant_acme, acme_tree, alice,
):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    fire_transition(inst, "qualify", actor=alice)
    with pytest.raises(WorkflowError):
        record_approval(
            inst,
            "close_lost",
            actor=alice,
            decision=WorkflowApprovalDecision.APPROVED,
        )


# ---------------------------------------------------------------------------
# Triggers / event bus
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_event_trigger_fires_transition(lead_workflow, _lead_event, tenant_acme, acme_tree):
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 1},
    )
    dispatch("crm.lead.cooled_off", {"workflow_instance_id": str(inst.public_id)})
    inst.refresh_from_db()
    assert inst.current_state == "lost"


@pytest.mark.django_db
def test_fire_for_event_skips_when_not_legal(
    lead_workflow, _lead_event, tenant_acme, acme_tree, alice,
):
    alice.is_superuser = True
    alice.save()
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    # Move to "qualified" — auto_disqualify only fires from "new".
    fire_transition(inst, "qualify", actor=alice)
    inst.refresh_from_db()
    fired = fire_for_event(inst, "crm.lead.cooled_off")
    assert fired == []
    inst.refresh_from_db()
    assert inst.current_state == "qualified"


# ---------------------------------------------------------------------------
# HTTP API
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_definition_list_requires_auth(api_client, lead_workflow):
    resp = api_client.get(reverse("platform_workflow:definition-list"))
    assert resp.status_code in (401, 403)


@pytest.mark.django_db
def test_definition_detail_returns_payload(
    api_client, lead_workflow, alice, tenant_acme,
):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    resp = api_client.get(
        reverse("platform_workflow:definition-detail", kwargs={"name": "crm.lead.review"}),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    assert resp.json()["name"] == "crm.lead.review"


@pytest.mark.django_db
def test_instance_detail_and_fire(
    api_client, lead_workflow, alice, tenant_acme, acme_tree,
):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    detail = api_client.get(
        reverse("platform_workflow:instance-detail", kwargs={"public_id": str(inst.public_id)}),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert detail.status_code == 200
    body = detail.json()
    assert body["current_state"] == "new"
    assert any(t["name"] == "qualify" for t in body["available_transitions"])

    fire = api_client.post(
        reverse("platform_workflow:instance-fire", kwargs={"public_id": str(inst.public_id)}),
        {"transition": "qualify"},
        format="json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert fire.status_code == 200
    assert fire.json()["current_state"] == "qualified"


@pytest.mark.django_db
def test_instance_approve_endpoint(
    api_client, lead_workflow, alice, tenant_acme, acme_tree,
):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    inst = start_instance(
        lead_workflow, tenant=tenant_acme, organization_node=acme_tree["eu"], data={"score": 50},
    )
    fire_transition(inst, "qualify", actor=alice)
    resp = api_client.post(
        reverse("platform_workflow:instance-approve", kwargs={"public_id": str(inst.public_id)}),
        {"transition": "close_won", "decision": "approved"},
        format="json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 201
    assert WorkflowApproval.objects.filter(instance=inst).count() == 1


@pytest.mark.django_db
def test_instance_404_for_unknown(api_client, _workflow_registry, alice, tenant_acme):
    alice.is_superuser = True
    alice.save()
    api_client.force_login(alice)
    import uuid
    resp = api_client.get(
        reverse("platform_workflow:instance-detail", kwargs={"public_id": uuid.uuid4()}),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 404
    # silence unused-fixture noise
    assert WorkflowInstance.objects.count() == 0
