import asyncio

import pytest
from pydantic import BaseModel

from datapizza.workflows import RunPolicies, RunStatus, Workflow, task


def test_routes_and_basic_sync_execution():
    class IncrementFlow(Workflow[dict]):
        state_factory = dict

        @task(start=True)
        def start(self, ctx, payload):
            ctx.state["started_with"] = payload
            ctx.emit("increment", {"value": payload + 1})

        @task(on="increment")
        def increment(self, ctx, payload):
            ctx.state["value"] = payload["value"]

    flow = IncrementFlow()
    result = flow.run(1)

    assert result.status == RunStatus.COMPLETED
    assert result.state == {"started_with": 1, "value": 2}
    assert [(route.event, route.task) for route in flow.routes()] == [
        ("__start__", "start"),
        ("increment", "increment"),
    ]


@pytest.mark.asyncio
async def test_async_tasks_and_stop_result():
    class AsyncFlow(Workflow[dict]):
        state_factory = dict

        @task(start=True)
        async def begin(self, ctx, payload):
            await asyncio.sleep(0)
            ctx.emit("finish", payload)

        @task(on="finish")
        async def finish(self, ctx, payload):
            await asyncio.sleep(0)
            ctx.state["payload"] = payload
            ctx.stop({"done": True})

    result = await AsyncFlow().a_run({"id": "123"})

    assert result.status == RunStatus.STOPPED
    assert result.result == {"done": True}
    assert result.state == {"payload": {"id": "123"}}


def test_failure_event_rolls_back_state():
    class FailureFlow(Workflow[dict]):
        state_factory = lambda self: {"attempts": 0, "handled": False}

        @task(start=True, retry={"max_attempts": 2}, failure_event="task_failed")
        def explode(self, ctx, payload):
            ctx.state["attempts"] += 1
            raise RuntimeError("boom")

        @task(on="task_failed")
        def handle(self, ctx, payload):
            ctx.state["handled"] = True
            ctx.state["failure_attempt"] = payload["attempt"]

    result = FailureFlow().run()

    assert result.status == RunStatus.COMPLETED
    assert result.state["attempts"] == 0
    assert result.state["handled"] is True
    assert result.state["failure_attempt"] == 2


def test_stop_discards_outbox():
    class StopFlow(Workflow[dict]):
        state_factory = dict

        @task(start=True)
        def start(self, ctx, payload):
            ctx.emit("never", None)
            ctx.stop("done")

        @task(on="never")
        def never(self, ctx, payload):
            ctx.state["should_not_run"] = True

    result = StopFlow().run()

    assert result.status == RunStatus.STOPPED
    assert result.result == "done"
    assert result.state == {}
    assert "should_not_run" not in result.state


def test_multiple_subscribers_run_in_declaration_order():
    class FanOutFlow(Workflow[dict]):
        state_factory = lambda self: {"order": []}

        @task(start=True)
        def start(self, ctx, payload):
            ctx.emit("fan", None)

        @task(on="fan")
        def first(self, ctx, payload):
            ctx.state["order"].append("first")

        @task(on="fan")
        def second(self, ctx, payload):
            ctx.state["order"].append("second")

    result = FanOutFlow().run()

    assert result.status == RunStatus.COMPLETED
    assert result.state["order"] == ["first", "second"]


def test_payload_validation_uses_pydantic_type_adapter():
    class InputModel(BaseModel):
        value: int

    class ValidationFlow(Workflow[dict]):
        state_factory = dict

        @task(start=True, input_type=InputModel)
        def start(self, ctx, payload):
            ctx.state["value"] = payload.value

    result = ValidationFlow().run({"value": "7"})

    assert result.status == RunStatus.COMPLETED
    assert result.state["value"] == 7


def test_fail_fast_marks_run_failed():
    class FailFastFlow(Workflow[dict]):
        state_factory = dict

        @task(start=True)
        def start(self, ctx, payload):
            raise RuntimeError("broken")

    result = FailFastFlow().run()

    assert result.status == RunStatus.FAILED
    assert "broken" in (result.termination_reason or "")


def test_loop_guardrail_stops_run():
    class LoopFlow(Workflow[dict]):
        run_policies = RunPolicies(max_events_processed=3)
        state_factory = lambda self: {"count": 0}

        @task(start=True)
        def start(self, ctx, payload):
            ctx.state["count"] += 1
            ctx.emit("again", None)

        @task(on="again")
        def again(self, ctx, payload):
            ctx.state["count"] += 1
            ctx.emit("again", None)

    result = LoopFlow().run()

    assert result.status == RunStatus.LIMIT_REACHED
    assert "max_events_processed" in (result.termination_reason or "")


def test_inherited_tasks_are_compiled_on_subclasses():
    class BaseFlow(Workflow[dict]):
        state_factory = dict

        @task(start=True)
        def start(self, ctx, payload):
            ctx.emit("child", payload)

    class ChildFlow(BaseFlow):
        @task(on="child")
        def child(self, ctx, payload):
            ctx.state["payload"] = payload

    flow = ChildFlow()
    result = flow.run("ok")

    assert result.status == RunStatus.COMPLETED
    assert result.state == {"payload": "ok"}
    assert [(route.event, route.task) for route in flow.routes()] == [
        ("__start__", "start"),
        ("child", "child"),
    ]


def test_overriding_task_without_decorator_removes_inherited_task():
    class BaseFlow(Workflow[dict]):
        state_factory = dict

        @task(start=True)
        def start(self, ctx, payload):
            ctx.emit("base", None)

        @task(on="base")
        def handle(self, ctx, payload):
            ctx.state["handled"] = True

    class ChildFlow(BaseFlow):
        def handle(self, ctx, payload):
            ctx.state["plain_method"] = True

    flow = ChildFlow()
    result = flow.run()

    assert result.status == RunStatus.COMPLETED
    assert result.state == {}
    assert [(route.event, route.task) for route in flow.routes()] == [
        ("__start__", "start"),
    ]
