import re
from datetime import datetime
from time import sleep

import pytest
from botocore.exceptions import ClientError
from dateutil.parser import parse as dtparse
from freezegun import freeze_time

from moto import mock_swf, settings

from ..utils import setup_workflow_boto3

# PollForDecisionTask endpoint


@mock_swf
def test_poll_for_decision_task_when_one_boto3():
    client = setup_workflow_boto3()

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == ["WorkflowExecutionStarted", "DecisionTaskScheduled"]

    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}, identity="srv01"
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
    ]

    assert (
        resp["events"][-1]["decisionTaskStartedEventAttributes"]["identity"] == "srv01"
    )


@mock_swf
def test_poll_for_decision_task_previous_started_event_id_boto3():
    client = setup_workflow_boto3()

    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert resp["workflowExecution"]["runId"] == client.run_id
    assert "previousStartedEventId" not in resp

    # Require a failing decision, in this case a non-existant activity type
    attrs = {
        "activityId": "spam",
        "activityType": {"name": "test-activity", "version": "v1.42"},
        "taskList": {"name": "eggs"},
    }
    decision = {
        "decisionType": "ScheduleActivityTask",
        "scheduleActivityTaskDecisionAttributes": attrs,
    }
    client.respond_decision_task_completed(
        taskToken=resp["taskToken"], decisions=[decision]
    )
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert resp["workflowExecution"]["runId"] == client.run_id
    assert resp["previousStartedEventId"] == 3


@mock_swf
def test_poll_for_decision_task_ensure_single_started_task():
    client = setup_workflow_boto3()

    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert "taskToken" in resp
    first_decision_task = resp["taskToken"]

    # History should have just the decision task triggered on workflow start
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
    ]

    # Schedule another decision task, before first one is completed
    client.signal_workflow_execution(
        domain="test-domain",
        signalName="my_signal",
        workflowId="uid-abcd1234",
        input="my_input",
        runId=client.run_id,
    )

    # Second poll should return no new tasks
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert resp["previousStartedEventId"] == 0
    assert resp["startedEventId"] == 0
    assert "taskToken" not in resp

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "WorkflowExecutionSignaled",
        "DecisionTaskScheduled",
    ]

    client.respond_decision_task_completed(taskToken=first_decision_task)

    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert "taskToken" in resp

    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "WorkflowExecutionSignaled",
        "DecisionTaskScheduled",
        "DecisionTaskCompleted",
        "DecisionTaskStarted",
    ]


@mock_swf
def test_poll_for_decision_task_exclude_completed_executions():
    client = setup_workflow_boto3()

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == ["WorkflowExecutionStarted", "DecisionTaskScheduled"]

    client.terminate_workflow_execution(
        domain="test-domain", runId=client.run_id, workflowId="uid-abcd1234"
    )
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert "taskToken" not in resp


@mock_swf
def test_poll_for_decision_task_when_none_boto3():
    client = setup_workflow_boto3()

    client.poll_for_decision_task(domain="test-domain", taskList={"name": "queue"})

    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    # this is the DecisionTask representation you get from the real SWF
    # after waiting 60s when there's no decision to be taken
    assert resp["previousStartedEventId"] == 0
    assert resp["startedEventId"] == 0


@mock_swf
def test_poll_for_decision_task_on_non_existent_queue_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "non-existent-queue"}
    )
    assert resp["previousStartedEventId"] == 0
    assert resp["startedEventId"] == 0


@mock_swf
def test_poll_for_decision_task_with_reverse_order_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}, reverseOrder=True
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "DecisionTaskStarted",
        "DecisionTaskScheduled",
        "WorkflowExecutionStarted",
    ]


# CountPendingDecisionTasks endpoint


@mock_swf
def test_count_pending_decision_tasks_boto3():
    client = setup_workflow_boto3()
    client.poll_for_decision_task(domain="test-domain", taskList={"name": "queue"})
    resp = client.count_pending_decision_tasks(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert resp["count"] == 1
    assert resp["truncated"] is False


@mock_swf
def test_count_pending_decision_tasks_on_non_existent_task_list_boto3():
    client = setup_workflow_boto3()
    resp = client.count_pending_decision_tasks(
        domain="test-domain", taskList={"name": "non-existent"}
    )
    assert resp["count"] == 0
    assert resp["truncated"] is False


@mock_swf
def test_count_pending_decision_tasks_after_decision_completes_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    client.respond_decision_task_completed(taskToken=resp["taskToken"])

    resp = client.count_pending_decision_tasks(
        domain="test-domain", taskList={"name": "queue"}
    )
    assert resp["count"] == 0
    assert resp["truncated"] is False


# RespondDecisionTaskCompleted endpoint


@mock_swf
def test_respond_decision_task_completed_with_no_decision_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    client.respond_decision_task_completed(
        taskToken=task_token, executionContext="free-form context"
    )

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "DecisionTaskCompleted",
    ]
    evt = resp["events"][-1]
    assert evt["decisionTaskCompletedEventAttributes"] == {
        "executionContext": "free-form context",
        "scheduledEventId": 2,
        "startedEventId": 3,
    }

    resp = client.describe_workflow_execution(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    assert resp["latestExecutionContext"] == "free-form context"


@mock_swf
def test_respond_decision_task_completed_with_wrong_token_boto3():
    client = setup_workflow_boto3()
    client.poll_for_decision_task(domain="test-domain", taskList={"name": "queue"})
    with pytest.raises(ClientError) as ex:
        client.respond_decision_task_completed(taskToken="not-a-correct-token")
    assert ex.value.response["Error"]["Code"] == "ValidationException"
    assert ex.value.response["Error"]["Message"] == "Invalid token"
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400


@mock_swf
def test_respond_decision_task_completed_on_close_workflow_execution_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    client.terminate_workflow_execution(domain="test-domain", workflowId="uid-abcd1234")

    with pytest.raises(ClientError) as ex:
        client.respond_decision_task_completed(taskToken=task_token)
    assert ex.value.response["Error"]["Code"] == "UnknownResourceFault"
    assert ex.value.response["Error"]["Message"] == (
        f"Unknown execution: WorkflowExecution=[workflowId=uid-abcd1234, runId={client.run_id}]"
    )
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400


@mock_swf
def test_respond_decision_task_completed_with_task_already_completed_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]
    client.respond_decision_task_completed(taskToken=task_token)

    with pytest.raises(ClientError) as ex:
        client.respond_decision_task_completed(taskToken=task_token)
    assert ex.value.response["Error"]["Code"] == "UnknownResourceFault"
    assert ex.value.response["Error"]["Message"] == (
        "Unknown decision task, scheduledEventId = 2"
    )
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400


@mock_swf
def test_respond_decision_task_completed_with_complete_workflow_execution_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {
            "decisionType": "CompleteWorkflowExecution",
            "completeWorkflowExecutionDecisionAttributes": {"result": "foo bar"},
        }
    ]
    client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "DecisionTaskCompleted",
        "WorkflowExecutionCompleted",
    ]
    assert (
        resp["events"][-1]["workflowExecutionCompletedEventAttributes"]["result"]
        == "foo bar"
    )


@mock_swf
def test_respond_decision_task_completed_with_close_decision_not_last_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {"decisionType": "CompleteWorkflowExecution"},
        {"decisionType": "WeDontCare"},
    ]

    with pytest.raises(ClientError) as ex:
        client.respond_decision_task_completed(
            taskToken=task_token, decisions=decisions
        )
    assert ex.value.response["Error"]["Code"] == "ValidationException"
    assert ex.value.response["Error"]["Message"] == (
        "Close must be last decision in list"
    )
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400


@mock_swf
def test_respond_decision_task_completed_with_invalid_decision_type_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {"decisionType": "BadDecisionType"},
        {"decisionType": "CompleteWorkflowExecution"},
    ]

    with pytest.raises(ClientError) as ex:
        client.respond_decision_task_completed(
            taskToken=task_token, decisions=decisions
        )
    assert ex.value.response["Error"]["Code"] == "ValidationException"
    assert re.search(
        "Value 'BadDecisionType' at 'decisions.1.member.decisionType'",
        ex.value.response["Error"]["Message"],
    )
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400


@mock_swf
def test_respond_decision_task_completed_with_missing_attributes_totally_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [{"decisionType": "StartTimer"}]

    with pytest.raises(ClientError) as ex:
        client.respond_decision_task_completed(
            taskToken=task_token, decisions=decisions
        )
    assert ex.value.response["Error"]["Code"] == "ValidationException"
    assert re.search(
        (
            "Value null at "
            "'decisions.1.member.startTimerDecisionAttributes.timerId' "
            "failed to satisfy constraint"
        ),
        ex.value.response["Error"]["Message"],
    )
    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400


@mock_swf
def test_respond_decision_task_completed_with_fail_workflow_execution_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {
            "decisionType": "FailWorkflowExecution",
            "failWorkflowExecutionDecisionAttributes": {
                "reason": "my rules",
                "details": "foo",
            },
        }
    ]
    client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "DecisionTaskCompleted",
        "WorkflowExecutionFailed",
    ]
    attrs = resp["events"][-1]["workflowExecutionFailedEventAttributes"]
    assert attrs["reason"] == "my rules"
    assert attrs["details"] == "foo"


@mock_swf
@freeze_time("2015-01-01 12:00:00 UTC")
def test_respond_decision_task_completed_with_schedule_activity_task_boto3():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {
            "decisionType": "ScheduleActivityTask",
            "scheduleActivityTaskDecisionAttributes": {
                "activityId": "my-activity-001",
                "activityType": {"name": "test-activity", "version": "v1.1"},
                "heartbeatTimeout": "60",
                "input": "123",
                "taskList": {"name": "my-task-list"},
            },
        }
    ]
    client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "DecisionTaskCompleted",
        "ActivityTaskScheduled",
    ]
    assert resp["events"][-1]["activityTaskScheduledEventAttributes"] == {
        "decisionTaskCompletedEventId": 4,
        "activityId": "my-activity-001",
        "activityType": {"name": "test-activity", "version": "v1.1"},
        "heartbeatTimeout": "60",
        "input": "123",
        "taskList": {"name": "my-task-list"},
    }

    resp = client.describe_workflow_execution(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    assert isinstance(resp["latestActivityTaskTimestamp"], datetime)
    if not settings.TEST_SERVER_MODE:
        ts = resp["latestActivityTaskTimestamp"]
        assert ts == dtparse("2015-01-01 12:00:00 UTC")


@mock_swf
def test_record_marker_decision():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {
            "decisionType": "RecordMarker",
            "recordMarkerDecisionAttributes": {"markerName": "TheMarker"},
        }
    ]
    client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "DecisionTaskCompleted",
        "MarkerRecorded",
    ]
    assert resp["events"][-1]["markerRecordedEventAttributes"] == {
        "decisionTaskCompletedEventId": 4,
        "markerName": "TheMarker",
    }


@mock_swf
def test_start_and_fire_timer_decision():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {
            "decisionType": "StartTimer",
            "startTimerDecisionAttributes": {
                "startToFireTimeout": "1",
                "timerId": "timer1",
            },
        }
    ]
    client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)
    sleep(1.1)

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "DecisionTaskCompleted",
        "TimerStarted",
        "TimerFired",
        "DecisionTaskScheduled",
    ]
    assert resp["events"][-3]["timerStartedEventAttributes"] == {
        "decisionTaskCompletedEventId": 4,
        "startToFireTimeout": "1",
        "timerId": "timer1",
    }
    assert resp["events"][-2]["timerFiredEventAttributes"] == {
        "startedEventId": 5,
        "timerId": "timer1",
    }


@mock_swf
def test_cancel_workflow_decision():
    client = setup_workflow_boto3()
    resp = client.poll_for_decision_task(
        domain="test-domain", taskList={"name": "queue"}
    )
    task_token = resp["taskToken"]

    decisions = [
        {
            "decisionType": "CancelWorkflowExecution",
            "cancelWorkflowExecutionDecisionAttributes": {
                "details": "decide to cancel"
            },
        }
    ]
    client.respond_decision_task_completed(taskToken=task_token, decisions=decisions)

    resp = client.get_workflow_execution_history(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )
    types = [evt["eventType"] for evt in resp["events"]]
    assert types == [
        "WorkflowExecutionStarted",
        "DecisionTaskScheduled",
        "DecisionTaskStarted",
        "DecisionTaskCompleted",
        "WorkflowExecutionCanceled",
    ]
    assert resp["events"][-1]["workflowExecutionCanceledEventAttributes"] == {
        "decisionTaskCompletedEventId": 4,
        "details": "decide to cancel",
    }
    workflow_result = client.describe_workflow_execution(
        domain="test-domain",
        execution={"runId": client.run_id, "workflowId": "uid-abcd1234"},
    )["executionInfo"]
    assert "closeTimestamp" in workflow_result
    assert workflow_result["executionStatus"] == "CLOSED"
    assert workflow_result["closeStatus"] == "CANCELED"
    assert workflow_result["cancelRequested"] is True
