from datetime import timedelta
from typing import Any, List, Dict, Tuple, Callable

from deepdiff import DeepDiff
from frozendict import frozendict

from resotocore.ids import SubscriberId, TaskDescriptorId
from resotocore.message_bus import Action, ActionDone, ActionError, Event, ActionProgress, ActionInfo
from resotocore.model.typed_model import from_js, to_js
from resotocore.task.model import Subscriber, Subscription
from resotocore.task.task_description import (
    Workflow,
    Step,
    RunningTask,
    StepErrorBehaviour,
    PerformAction,
    WaitForEvent,
    EmitEvent,
    ExecuteCommand,
    EventTrigger,
    Job,
    TimeTrigger,
    SendMessage,
    ExecuteOnCLI,
)
from resotolib.core.progress import ProgressDone
from resotolib.utils import utc


def test_eq() -> None:
    s1 = Step("a", PerformAction("a"), timedelta())
    s2 = Step("a", WaitForEvent("a", {"foo": "bla"}), timedelta())
    s3 = Step("a", EmitEvent(Event("a", {"a": "b"})), timedelta())
    s4 = Step("a", ExecuteCommand("echo hello"), timedelta())
    assert s1 == Step("a", PerformAction("a"), timedelta())
    assert s2 == Step("a", WaitForEvent("a", {"foo": "bla"}), timedelta())
    assert s3 == Step("a", EmitEvent(Event("a", {"a": "b"})), timedelta())
    assert s4 == Step("a", ExecuteCommand("echo hello"), timedelta())
    trigger = [EventTrigger("start me up")]
    assert Workflow(TaskDescriptorId("a"), "a", [s1, s2, s3, s4], trigger) == Workflow(
        TaskDescriptorId("a"), "a", [s1, s2, s3, s4], trigger
    )


def test_ack_for(workflow_instance: Tuple[RunningTask, Subscriber, Subscriber, Dict[str, List[Subscriber]]]) -> None:
    wi, s1, s2, subscriptions = workflow_instance
    assert wi.ack_for("start_collect", s1) is not None
    assert wi.ack_for("start_collect", s2) is not None
    assert wi.ack_for("collect_done", s1) is None
    assert wi.ack_for("collect_done", s2) is None


def test_pending_action_for(
    workflow_instance: Tuple[RunningTask, Subscriber, Subscriber, Dict[str, List[Subscriber]]],
) -> None:
    wi, s1, s2, subscriptions = workflow_instance
    # s1 already sent a done message for the current step
    assert wi.pending_action_for(s1) is None
    # s2 is still expected to provide a done message
    assert wi.pending_action_for(s2) == Action("collect", wi.id, "collect")


def test_handle_done(
    workflow_instance: Tuple[RunningTask, Subscriber, Subscriber, Dict[str, List[Subscriber]]]
) -> None:
    wi, s1, s2, subscriptions = workflow_instance
    # we are in state collect. Another ack of start is ignored.
    events = wi.handle_done(ActionDone("start", wi.id, "start", s1.id))  #
    assert wi.current_step.name == "collect"
    assert len(events) == 0
    # This step is unknown to the workflow and should not change its state
    events = wi.handle_done(ActionDone("boom", wi.id, "boom", s1.id))  #
    assert wi.current_step.name == "collect"
    assert len(events) == 0
    # this event has been received already and should not emit any new action
    events = wi.handle_done(ActionDone("collect", wi.id, "collect", s1.id))
    assert wi.current_step.name == "collect"
    assert len(events) == 0
    # this event is the last missing event in this step
    events = wi.handle_done(ActionDone("collect", wi.id, "collect", s2.id))
    assert wi.current_step.name == "done"
    assert len(events) == 1


def test_handle_error(
    workflow_instance: Tuple[RunningTask, Subscriber, Subscriber, Dict[str, List[Subscriber]]]
) -> None:
    wi, s1, s2, subscriptions = workflow_instance
    # this event is the last missing event in this step. It fails but the workflow should continue at that point
    events = wi.handle_error(ActionError("collect", wi.id, "collect", s2.id, "boom"))
    assert wi.current_step.name == "done"
    assert len(events) == 1
    # this step is configured to fail the whole workflow instance
    events = wi.handle_error(ActionError("collect_done", wi.id, "done", s2.id, "boom"))
    assert wi.is_active is False
    assert wi.is_error is True
    assert len(events) == 0


def test_complete_workflow(
    workflow_instance: Tuple[RunningTask, Subscriber, Subscriber, Dict[str, List[Subscriber]]]
) -> None:
    init, s1, s2, subscriptions = workflow_instance
    # start new workflow instance
    wi, events = RunningTask.empty(init.descriptor, lambda: subscriptions)
    assert wi.current_step.name == "start"
    assert len(events) == 2
    assert wi.progress.percentage == 0
    events = wi.handle_done(ActionDone("start", wi.id, "start", s1.id))
    assert wi.current_step.name == "wait"
    assert len(events) == 0
    assert wi.progress.percentage == 33
    handled, events = wi.handle_event(Event("godot", {"a": 2}))
    assert wi.current_step.name == "wait"
    assert handled is False
    assert len(events) == 0
    handled, events = wi.handle_event(Event("godot", {"a": 1, "d": "test"}))
    assert wi.current_step.name == "collect"
    assert handled is True
    assert len(events) == 2  # event from EmitEvent and action from PerformAction
    assert wi.progress.percentage == 33
    wi.handle_progress(ActionProgress("start", wi.id, "start", s1.id, ProgressDone("Test", 0, 100), utc()))
    wi.handle_progress(ActionProgress("start", wi.id, "start", s2.id, ProgressDone("Test", 50, 100), utc()))
    assert wi.progress.percentage == 50
    wi.handle_progress(ActionProgress("start", wi.id, "start", s1.id, ProgressDone("Test", 90, 100), utc()))
    wi.handle_progress(ActionProgress("start", wi.id, "start", s2.id, ProgressDone("Test", 80, 100), utc()))
    assert wi.progress.percentage == 60
    wi.handle_info(ActionInfo("start", wi.id, "start", s1.id, "error", "echt jetzt"))
    assert len(wi.info_messages) == 1
    events = wi.handle_done(ActionDone("start", wi.id, "start", s1.id))
    assert wi.current_step.name == "collect"
    assert len(events) == 0
    events = wi.handle_done(ActionDone("collect", wi.id, "collect", s1.id))
    assert wi.current_step.name == "collect"
    assert len(events) == 0
    events = wi.handle_done(ActionDone("collect", wi.id, "collect", s2.id))
    assert wi.current_step.name == "done"
    assert len(events) == 1
    assert wi.progress.percentage == 66
    events = wi.handle_done(ActionDone("done", wi.id, "done", s1.id))
    assert len(events) == 0
    assert wi.current_step.name == "done"
    events = wi.handle_done(ActionDone("done", wi.id, "done", s2.id))
    assert len(events) == 1
    assert wi.progress.percentage == 100
    assert wi.is_active is False


def test_marshalling_trigger() -> None:
    roundtrip(EventTrigger("test", {"foo": True}))
    roundtrip(TimeTrigger("* * * * *"))


def test_marshalling_step_action() -> None:
    roundtrip(PerformAction("test"))
    roundtrip(EmitEvent(Event("test", {"foo": "hello"})))
    roundtrip(WaitForEvent("test", {"foo": "hello"}))
    roundtrip(ExecuteCommand("help"))


def test_marshalling_task_command() -> None:
    roundtrip(SendMessage(Event("test", {"foo": "hello"})))
    roundtrip(ExecuteOnCLI("test", frozendict({"fii": "bla"})))


def test_marshalling_step() -> None:
    roundtrip(Step("test", PerformAction("test")))
    roundtrip(Step("test", WaitForEvent("test"), timedelta(seconds=19), StepErrorBehaviour.Stop))


def test_marshalling_job() -> None:
    j = Job(TaskDescriptorId("id"), ExecuteCommand("echo hello"), timedelta(seconds=10), EventTrigger("run_job"))
    roundtrip(j)
    roundtrip(Job(j.id, j.command, j.timeout, j.trigger, (EventTrigger("test"), timedelta(hours=2))))


def test_marshalling_workflow(test_workflow: Workflow) -> None:
    roundtrip(test_workflow)


def roundtrip(obj: Any) -> None:
    js = to_js(obj)
    again = from_js(js, type(obj))
    assert DeepDiff(obj, again) == {}, f"Json: {js} serialized as {again}"


def test_task_progress() -> None:
    actions = ["collect", "encode"]
    wf = Workflow(TaskDescriptorId("test_workflow"), "name", [Step(a, PerformAction(a)) for a in actions], [])
    sb = Subscriber(SubscriberId("test"), {s: Subscription(s) for s in actions})
    rt, _ = RunningTask.empty(wf, lambda: {s: [sb] for s in actions})

    def progress(step: str, fn: Callable[[int], ProgressDone]) -> None:
        # use revers order to test that the correct order is used
        for idx in range(3):
            rt.handle_progress(ActionProgress(step, rt.id, step, sb.id, fn(idx), utc()))

    # report progress start on the collect step
    progress("collect", lambda idx: ProgressDone(str(idx), 0, 100, path=["foo", "bla"]))
    assert [x["name"] for x in rt.progress_json()["parts"]] == ["0", "1", "2", "encode"]

    # report index as progress on the collect step
    progress("collect", lambda idx: ProgressDone(str(idx), idx, 100, path=["foo", "bla"]))
    assert [x["name"] for x in rt.progress_json()["parts"]] == ["2", "1", "0", "encode"]

    # report progress done on the collect step
    rt.handle_done(ActionDone("collect", rt.id, "collect", sb.id))
    assert [x["name"] for x in rt.progress_json()["parts"]] == ["collect", "encode"]
    assert rt.progress.overall_progress().percentage == 50

    # report progress done on the encode step
    progress("encode", lambda idx: ProgressDone(str(idx), 0, 100, path=["foo", "bla"]))
    assert [x["name"] for x in rt.progress_json()["parts"]] == ["collect", "0", "1", "2"]

    # report index as progress on the collect step
    progress("collect", lambda idx: ProgressDone(str(idx), idx, 100, path=["foo", "bla"]))
    assert [x["name"] for x in rt.progress_json()["parts"]] == ["collect", "2", "1", "0"]

    rt.handle_done(ActionDone("encode", rt.id, "encode", sb.id))
    assert [x["name"] for x in rt.progress_json()["parts"]] == ["collect", "encode"]
    assert rt.progress.overall_progress().percentage == 100


def test_emitting_task_progress(
    workflow_instance: Tuple[RunningTask, Subscriber, Subscriber, Dict[str, List[Subscriber]]]
) -> None:
    task, sub, _, _ = workflow_instance
    # after the task is created, there is not emitted progress
    assert task.not_emitted_progress() is not None
    # no progress update, nothing to emit
    assert task.not_emitted_progress() is None
    assert task.not_emitted_progress() is None
    # add progress updates
    task.handle_progress(ActionProgress("collect", task.id, "collect", sub.id, ProgressDone("collect", 0, 100), utc()))
    assert task.not_emitted_progress() is not None
    # no progress update, nothing to emit
    assert task.not_emitted_progress() is None
    assert task.not_emitted_progress() is None
