from datetime import datetime, timedelta
from unittest.mock import patch

from director.extensions import db
from director.models.workflows import Workflow


DEFAULT_PAYLOAD = {"project": "example", "name": "WORKFLOW", "payload": {}}


def test_pong(client):
    resp = client.get("/api/ping")
    assert resp.status_code == 200
    assert resp.json == {"message": "pong"}


def test_post_workflow_without_payload(client, no_worker):
    resp = client.post("/api/workflows", json=DEFAULT_PAYLOAD)
    assert resp.status_code == 201
    assert resp.json == {
        "fullname": "example.WORKFLOW",
        "name": "WORKFLOW",
        "payload": {},
        "periodic": False,
        "project": "example",
        "status": "pending",
    }


def test_post_workflow_with_payload(client, no_worker):
    payload = {**DEFAULT_PAYLOAD, "payload": {"nested": {"foo": "bar"}}}
    resp = client.post("/api/workflows", json=payload)
    assert resp.status_code == 201
    assert resp.json == {
        "fullname": "example.WORKFLOW",
        "name": "WORKFLOW",
        "payload": {"nested": {"foo": "bar"}},
        "periodic": False,
        "project": "example",
        "status": "pending",
    }


def test_post_workflow_required_props(client):
    resp = client.post("/api/workflows", json={})
    assert resp.status_code == 400
    assert resp.json["error"] == "Error validating against schema"
    assert len(resp.json["errors"]) == 3
    for prop in ["project", "name", "payload"]:
        assert f"{prop}" in " ".join(resp.json["errors"])

    resp = client.post("/api/workflows", json={"project": "example"})
    assert resp.status_code == 400
    assert len(resp.json["errors"]) == 2
    for prop in ["name", "payload"]:
        assert f"{prop}" in " ".join(resp.json["errors"])

    resp = client.post(
        "/api/workflows", json={"project": "example", "name": "WORKFLOW"}
    )
    assert resp.status_code == 400
    assert len(resp.json["errors"]) == 1
    assert "payload" in resp.json["errors"][0]


def test_post_workflow_unexpected_properties(client):
    payload = {**DEFAULT_PAYLOAD, "foo": "bar"}
    resp = client.post("/api/workflows", json=payload)
    assert resp.status_code == 400
    assert resp.json["error"] == "Error validating against schema"
    assert "'foo' was unexpected" in resp.json["errors"][0]


def test_relaunch_not_existing_workflow(client):
    resp = client.post(
        "/api/workflows/280d0c4065d04063a800a3b674562711/relaunch", json={}
    )
    assert resp.status_code == 404


def test_relaunch_workflow(client, no_worker):
    payload = {**DEFAULT_PAYLOAD, "payload": {"nested": {"foo": "bar"}}}
    resp = client.post("/api/workflows", json=payload)
    with patch("tests.conftest.DirectorResponse._KEYS_TO_REMOVE", new=[]):
        workflow_id = resp.json["id"]
    assert resp.status_code == 201

    resp = client.post(f"/api/workflows/{workflow_id}/relaunch", json={})
    assert resp.status_code == 201
    assert resp.json == {
        "fullname": "example.WORKFLOW",
        "name": "WORKFLOW",
        "payload": {"nested": {"foo": "bar"}},
        "periodic": False,
        "project": "example",
        "status": "pending",
    }

    # workflows are the same
    resp = client.get("/api/workflows")
    assert len(resp.json) == 2

    assert resp.json[0] == resp.json[1]

    # tasks are the same
    resp = client.get("/api/workflows")
    with patch("tests.conftest.DirectorResponse._KEYS_TO_REMOVE", new=[]):
        workflows = resp.json
    tasks1 = [
        t["key"]
        for t in client.get(f"/api/workflows/{workflows[0]['id']}").json.get("tasks")
    ]
    tasks2 = [
        t["key"]
        for t in client.get(f"/api/workflows/{workflows[1]['id']}").json.get("tasks")
    ]
    assert tasks1 == tasks2


def test_cancel_not_existing_workflow(client, no_worker):
    resp = client.post(
        "/api/workflows/280d0c4065d04063a800a3b674562711/cancel", json={}
    )
    assert resp.status_code == 404


def test_cancel_workflow(client, no_worker):
    resp = client.post(
        "/api/workflows",
        json={"project": "example", "name": "DELAY_TASK", "payload": {}},
    )
    with patch("tests.conftest.DirectorResponse._KEYS_TO_REMOVE", new=[]):
        workflow_id = resp.json["id"]
    assert resp.status_code == 201

    resp = client.post(f"/api/workflows/{workflow_id}/cancel", json={})
    assert resp.status_code == 201
    assert resp.json["status"] == "canceled"

    tasks = [
        t["status"]
        for t in client.get(f"/api/workflows/{workflow_id}").json.get("tasks")
    ]
    assert tasks[0] == "canceled"


def test_not_found_workflows(client, no_worker):
    resp = client.post("/api/workflows", json=DEFAULT_PAYLOAD)
    assert resp.status_code == 201

    payload = {**DEFAULT_PAYLOAD, "name": "NOT_FOUND"}
    resp = client.post("/api/workflows", json=payload)
    assert resp.status_code == 404
    assert resp.json == {"error": "Workflow example.NOT_FOUND not found"}

    payload = {**DEFAULT_PAYLOAD, "project": "NOT_FOUND"}
    resp = client.post("/api/workflows", json=payload)
    assert resp.status_code == 404
    assert resp.json == {"error": "Workflow NOT_FOUND.WORKFLOW not found"}


def test_list_workflows(client, no_worker):
    resp = client.get("/api/workflows")
    assert resp.status_code == 200
    assert resp.json == []

    resp = client.post("/api/workflows", json=DEFAULT_PAYLOAD)
    assert resp.status_code == 201

    resp = client.get("/api/workflows")
    assert resp.json == [
        {
            "fullname": "example.WORKFLOW",
            "name": "WORKFLOW",
            "payload": {},
            "periodic": False,
            "project": "example",
            "status": "pending",
        }
    ]

    for i in range(9):
        client.post("/api/workflows", json=DEFAULT_PAYLOAD)
    resp = client.get("/api/workflows")
    assert len(resp.json) == 10


def test_list_paginated_workflows(app, client, no_worker):
    # SQLite db.Datetime() does not include microseconds, so
    # we need to fix dates to be able to sort workflows.
    now = datetime.now()
    with app.app_context():
        for i in range(20):
            date = now + timedelta(minutes=i)
            w = Workflow(
                created_at=date,
                updated_at=date,
                project="example",
                name="WORKFLOW",
                payload={"i": i + 1},
            )
            db.session.add(w)
        db.session.commit()

    # DIRECTOR_WORKFLOWS_PER_PAGE=15 in tests/workflows/.env
    resp = client.get("/api/workflows")
    assert len(resp.json) == 15

    # Last item
    resp = client.get("/api/workflows?per_page=1")
    assert len(resp.json) == 1
    assert resp.json[0]["payload"]["i"] == 20

    # Last X items
    resp = client.get("/api/workflows?per_page=4&page=1")
    assert len(resp.json) == 4
    assert [w["payload"]["i"] for w in resp.json] == [20, 19, 18, 17]

    # Last X items, second page
    resp = client.get("/api/workflows?per_page=4&page=2")
    assert len(resp.json) == 4
    assert [w["payload"]["i"] for w in resp.json] == [16, 15, 14, 13]

    # Remaining items
    resp = client.get("/api/workflows?per_page=3&page=7")
    assert len(resp.json) == 2
    assert [w["payload"]["i"] for w in resp.json] == [2, 1]

    # Out of range
    resp = client.get("/api/workflows?per_page=4&page=6")
    assert resp.status_code == 404


def test_get_workflow(client, no_worker):
    resp = client.post("/api/workflows", json=DEFAULT_PAYLOAD)
    with patch("tests.conftest.DirectorResponse._KEYS_TO_REMOVE", new=[]):
        workflow_id = resp.json["id"]

    resp = client.get(f"/api/workflows/{workflow_id}")
    assert resp.status_code == 200
    assert len(resp.json["tasks"]) == 1

    resp_json_copy = resp.json.copy()
    del resp_json_copy["tasks"][0]["task"]

    assert resp_json_copy == {
        "fullname": "example.WORKFLOW",
        "name": "WORKFLOW",
        "payload": {},
        "periodic": False,
        "project": "example",
        "status": "pending",
        "tasks": [
            {
                "key": "TASK_EXAMPLE",
                "previous": [],
                "result": None,
                "status": "pending",
                "is_hook": False,
            }
        ],
    }


@patch("tests.conftest.DirectorResponse._KEYS_TO_REMOVE", new=[])
def test_get_workflow_simple_chain(client, no_worker):
    payload = {**DEFAULT_PAYLOAD, "name": "SIMPLE_CHAIN"}
    workflow_id = client.post("/api/workflows", json=payload).json["id"]

    resp = client.get(f"/api/workflows/{workflow_id}")
    assert resp.status_code == 200
    assert len(resp.json["tasks"]) == 3


def test_get_not_found_workflow(client):
    resp = client.get("/api/workflows/e0ee42d8-e00f-4557-8f75-781106dbbea4")
    assert resp.status_code == 404


def test_schema(client, no_worker):
    payload = {"project": "schemas", "name": "SIMPLE_SCHEMA", "payload": {}}
    resp = client.post("/api/workflows", json=payload)
    assert resp.status_code == 400
    assert resp.json == {
        "error": "Payload is not valid",
        "errors": ["'name' is a required property"],
    }

    payload["payload"]["price"] = "foo"
    resp = client.post("/api/workflows", json=payload)
    assert resp.status_code == 400
    assert resp.json["error"] == "Payload is not valid"
    assert sorted(resp.json["errors"]) == sorted(
        ["'name' is a required property", "'foo' is not of type 'number'"]
    )

    payload["payload"] = {"name": "foo", "price": 100}
    resp = client.post("/api/workflows", json=payload)
    assert resp.status_code == 201


def test_list_definitions(client, no_worker):
    resp = client.get("/api/definitions")
    assert resp.status_code == 200
    assert resp.json == [
        {
            "fullname": "example.CELERY_ERROR_MULTIPLE_TASKS",
            "name": "CELERY_ERROR_MULTIPLE_TASKS",
            "project": "example",
            "tasks": ["TASK_A", "TASK_CELERY_ERROR"],
        },
        {
            "fullname": "example.CELERY_ERROR_ONE_TASK",
            "name": "CELERY_ERROR_ONE_TASK",
            "project": "example",
            "tasks": ["TASK_CELERY_ERROR"],
        },
        {
            "fullname": "example.DELAY_TASK",
            "name": "DELAY_TASK",
            "project": "example",
            "tasks": ["DELAY"],
        },
        {
            "fullname": "example.ERROR",
            "name": "ERROR",
            "project": "example",
            "tasks": ["TASK_ERROR"],
        },
        {
            "fullname": "example.FAILURE_HOOK",
            "hooks": {"failure": "TASK_B"},
            "name": "FAILURE_HOOK",
            "project": "example",
            "tasks": ["TASK_A", "TASK_ERROR"],
        },
        {
            "fullname": "example.RETURN_EXCEPTION",
            "name": "RETURN_EXCEPTION",
            "project": "example",
            "tasks": ["STR", "TASK_ERROR"],
        },
        {
            "fullname": "example.RETURN_VALUES",
            "name": "RETURN_VALUES",
            "project": "example",
            "tasks": ["STR", "INT", "LIST", "NONE", "DICT", "NESTED"],
        },
        {
            "fullname": "example.SIMPLE_CHAIN",
            "name": "SIMPLE_CHAIN",
            "project": "example",
            "tasks": ["TASK_A", "TASK_B", "TASK_C"],
        },
        {
            "fullname": "example.SIMPLE_CHAIN_ERROR",
            "name": "SIMPLE_CHAIN_ERROR",
            "project": "example",
            "tasks": ["TASK_A", "TASK_B", "TASK_ERROR"],
        },
        {
            "fullname": "example.SIMPLE_GROUP",
            "name": "SIMPLE_GROUP",
            "project": "example",
            "tasks": [
                "TASK_A",
                {"EXAMPLE_GROUP": {"tasks": ["TASK_B", "TASK_C"], "type": "group"}},
            ],
        },
        {
            "fullname": "example.SIMPLE_GROUP_ERROR",
            "name": "SIMPLE_GROUP_ERROR",
            "project": "example",
            "tasks": [
                "TASK_A",
                {"EXAMPLE_GROUP": {"tasks": ["TASK_ERROR", "TASK_C"], "type": "group"}},
            ],
        },
        {
            "fullname": "example.SUCCESSANDFAILURE_HOOK",
            "hooks": {"failure": "TASK_C", "success": "TASK_B"},
            "name": "SUCCESSANDFAILURE_HOOK",
            "project": "example",
            "tasks": ["TASK_A"],
        },
        {
            "fullname": "example.TASK_ROUTING",
            "name": "TASK_ROUTING",
            "project": "example",
            "queue": {"customs": {"TASK_B": "q2"}, "default": "q1"},
            "tasks": [
                "TASK_A",
                {"EXAMPLE_GROUP": {"tasks": ["TASK_B", "TASK_C"], "type": "group"}},
            ],
        },
        {
            "fullname": "example.WORKFLOW",
            "name": "WORKFLOW",
            "project": "example",
            "tasks": ["TASK_EXAMPLE"],
        },
        {
            "fullname": "schemas.SIMPLE_SCHEMA",
            "name": "SIMPLE_SCHEMA",
            "project": "schemas",
            "schema": {
                "properties": {"name": {"type": "string"}, "price": {"type": "number"}},
                "required": ["name"],
                "type": "object",
            },
            "tasks": ["TASK_EXAMPLE"],
        },
    ]
