"""Integration tests for the Automation Executor (task 3.5.5).

Tests the full pipeline:
    trigger event → condition evaluation → action execution → AutomationExecution record

Uses pytest-django with ``@pytest.mark.django_db`` so real DB rows are
created.  Actions are replaced with lightweight test doubles registered via
the ActionRegistry so no external services are called.
"""

from __future__ import annotations

import uuid
from typing import Any

import pytest

from simorgh.apps.automation.evaluator import evaluate_conditions
from simorgh.apps.automation.executor import execute_rule
from simorgh.apps.automation.models import AutomationExecution, ExecutionStatus
from simorgh.apps.automation.registry import (
    ActionContext,
    ActionSpec,
    reset_registry_for_tests,
    register_action,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def clean_registry():
    """Wipe the ActionRegistry between tests."""
    reset_registry_for_tests()
    yield
    reset_registry_for_tests()


@pytest.fixture
def recorded_calls() -> list[ActionContext]:
    """Shared list that test actions append to."""
    return []


@pytest.fixture
def noop_action(recorded_calls):
    """Register a no-op action that records its context."""
    def _handler(ctx: ActionContext) -> None:
        recorded_calls.append(ctx)

    spec = ActionSpec(
        key="test.noop_action",
        label="No-op",
        module="test",
        handler=_handler,
    )
    register_action(spec)
    return spec


@pytest.fixture
def failing_action(recorded_calls):
    """Register an action that always raises."""
    def _handler(ctx: ActionContext) -> None:
        recorded_calls.append(ctx)
        raise RuntimeError("simulated failure")

    spec = ActionSpec(
        key="test.failing_action",
        label="Failing",
        module="test",
        handler=_handler,
    )
    register_action(spec)
    return spec


@pytest.fixture
def org_node(db):
    from simorgh.apps.organizations.services import create_node
    from simorgh.apps.tenants.models import Tenant

    tenant = Tenant.objects.create(slug=f"t-{uuid.uuid4().hex[:8]}", name="Test Tenant")
    node = create_node(tenant_id=tenant.pk, name="Root")
    return tenant, node


@pytest.fixture
def make_rule(org_node):
    """Factory that creates a real AutomationRule in the DB."""
    tenant, node = org_node

    def _factory(
        *,
        name: str = "Test Rule",
        conditions: list | None = None,
        actions: list | None = None,
        is_active: bool = True,
        trigger_event: str = "test.event",
    ):
        from simorgh.apps.automation.models import AutomationRule, TriggerType

        return AutomationRule.objects.create(
            tenant=tenant,
            organization_node=node,
            name=name,
            trigger_type=TriggerType.EVENT,
            trigger_event=trigger_event,
            conditions=conditions or [],
            actions=actions or [],
            is_active=is_active,
        )

    return _factory


# ---------------------------------------------------------------------------
# 3.5.5 tests
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestExecuteRule:
    """Full trigger → condition → action pipeline."""

    def test_unconditional_rule_runs_action(self, make_rule, noop_action, recorded_calls):
        """Rule with no conditions always executes its actions."""
        rule = make_rule(
            actions=[{"action": "test.noop_action", "params": {"key": "value"}}],
        )
        payload = {"lead": {"status": "new"}}

        execution = execute_rule(rule, payload, trigger_event="test.event")

        assert execution.status == ExecutionStatus.SUCCESS
        assert len(execution.actions_executed) == 1
        assert execution.actions_executed[0]["status"] == "ok"
        assert len(recorded_calls) == 1
        assert recorded_calls[0].params == {"key": "value"}
        assert recorded_calls[0].tenant_id == rule.tenant_id

    def test_condition_match_runs_action(self, make_rule, noop_action, recorded_calls):
        """Conditions that match → action executes."""
        rule = make_rule(
            conditions=[{"field": "lead.status", "op": "eq", "value": "new"}],
            actions=[{"action": "test.noop_action", "params": {}}],
        )
        payload = {"lead": {"status": "new"}}

        execution = execute_rule(rule, payload)

        assert execution.status == ExecutionStatus.SUCCESS
        assert len(recorded_calls) == 1

    def test_condition_no_match_skips(self, make_rule, noop_action, recorded_calls):
        """Conditions that don't match → SKIPPED, no actions run."""
        rule = make_rule(
            conditions=[{"field": "lead.status", "op": "eq", "value": "closed"}],
            actions=[{"action": "test.noop_action", "params": {}}],
        )
        payload = {"lead": {"status": "new"}}

        execution = execute_rule(rule, payload)

        assert execution.status == ExecutionStatus.SKIPPED
        assert len(recorded_calls) == 0
        assert execution.actions_executed == []

    def test_action_failure_marks_failed(self, make_rule, failing_action, recorded_calls):
        """A failing action → execution is FAILED, error captured in log."""
        rule = make_rule(
            actions=[{"action": "test.failing_action", "params": {}}],
        )
        execution = execute_rule(rule, {})

        assert execution.status == ExecutionStatus.FAILED
        assert execution.actions_executed[0]["status"] == "error"
        assert "simulated failure" in execution.actions_executed[0]["error"]

    def test_partial_failure_continues(self, make_rule, noop_action, failing_action, recorded_calls):
        """On action failure, remaining actions still run (resilient mode)."""
        # Register a second noop that runs after the failing one
        after_calls: list[ActionContext] = []

        def _after(ctx: ActionContext) -> None:
            after_calls.append(ctx)

        after_spec = ActionSpec(
            key="test.after_action",
            label="After",
            module="test",
            handler=_after,
        )
        register_action(after_spec)

        rule = make_rule(
            actions=[
                {"action": "test.failing_action", "params": {}},
                {"action": "test.after_action", "params": {}},
            ],
        )
        execution = execute_rule(rule, {})

        assert execution.status == ExecutionStatus.FAILED
        # Both actions are logged
        keys = {e["action"] for e in execution.actions_executed}
        assert "test.failing_action" in keys
        assert "test.after_action" in keys
        # After action still ran
        assert len(after_calls) == 1

    def test_unknown_action_aborts(self, make_rule, noop_action, recorded_calls):
        """Unknown action key → FAILED immediately, subsequent actions not run."""
        rule = make_rule(
            actions=[
                {"action": "unknown.action", "params": {}},
                {"action": "test.noop_action", "params": {}},
            ],
        )
        execution = execute_rule(rule, {})

        assert execution.status == ExecutionStatus.FAILED
        assert execution.actions_executed[0]["status"] == "error"
        assert "Unknown action key" in execution.actions_executed[0]["error"]
        # noop_action was not called
        assert len(recorded_calls) == 0

    def test_idempotency_key_deduplication(self, make_rule, noop_action, recorded_calls):
        """Same idempotency_key → second call returns existing execution."""
        rule = make_rule(
            actions=[{"action": "test.noop_action", "params": {}}],
        )
        idem_key = str(uuid.uuid4())

        exec1 = execute_rule(rule, {}, idempotency_key=idem_key)
        exec2 = execute_rule(rule, {}, idempotency_key=idem_key)

        assert exec1.pk == exec2.pk
        # Action was only called once
        assert len(recorded_calls) == 1

    def test_execution_row_persisted(self, make_rule, noop_action):
        """AutomationExecution row is written to the DB."""
        rule = make_rule(
            actions=[{"action": "test.noop_action", "params": {}}],
        )
        payload = {"x": 1}
        execution = execute_rule(rule, payload, trigger_event="test.event")

        row = AutomationExecution.objects.get(pk=execution.pk)
        assert row.status == ExecutionStatus.SUCCESS
        assert row.trigger_event == "test.event"
        assert row.trigger_payload == payload
        assert row.finished_at is not None
        assert row.started_at is not None

    def test_rule_stats_updated(self, make_rule, noop_action):
        """run_count and last_run_at are bumped after execution."""
        rule = make_rule(
            actions=[{"action": "test.noop_action", "params": {}}],
        )
        execute_rule(rule, {})

        rule.refresh_from_db()
        assert rule.run_count == 1
        assert rule.last_run_at is not None
        assert rule.last_status == ExecutionStatus.SUCCESS

    def test_skipped_rule_stats_updated(self, make_rule, noop_action):
        """Skipped execution also bumps run stats."""
        rule = make_rule(
            conditions=[{"field": "x", "op": "eq", "value": "never"}],
            actions=[{"action": "test.noop_action", "params": {}}],
        )
        execute_rule(rule, {"x": "always"})

        rule.refresh_from_db()
        assert rule.run_count == 1
        assert rule.last_status == ExecutionStatus.SKIPPED

    def test_context_carries_trigger_info(self, make_rule, recorded_calls):
        """ActionContext.trigger_event and trigger_payload match inputs."""
        def _capture(ctx: ActionContext) -> None:
            recorded_calls.append(ctx)

        spec = ActionSpec(
            key="test.capture",
            label="Capture",
            module="test",
            handler=_capture,
        )
        register_action(spec)

        rule = make_rule(actions=[{"action": "test.capture", "params": {"p": 99}}])
        payload = {"order": {"id": 42}}
        execute_rule(rule, payload, trigger_event="test.order.placed", actor_id=7)

        ctx = recorded_calls[0]
        assert ctx.trigger_event == "test.order.placed"
        assert ctx.trigger_payload == payload
        assert ctx.params == {"p": 99}
        assert ctx.actor_id == 7

    def test_multiple_actions_all_run(self, make_rule, recorded_calls):
        """Multiple actions in sequence all execute in order."""
        order: list[str] = []

        for key in ("test.first", "test.second", "test.third"):
            captured_key = key

            def _handler(ctx: ActionContext, k: str = captured_key) -> None:
                order.append(k)

            register_action(ActionSpec(key=key, label=key, module="test", handler=_handler))

        rule = make_rule(
            actions=[
                {"action": "test.first", "params": {}},
                {"action": "test.second", "params": {}},
                {"action": "test.third", "params": {}},
            ]
        )
        execution = execute_rule(rule, {})

        assert execution.status == ExecutionStatus.SUCCESS
        assert order == ["test.first", "test.second", "test.third"]
        assert len(execution.actions_executed) == 3
