# Copyright 2013: Mirantis Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Tests for db.task layer."""

import collections
import datetime as dt
from unittest import mock

import ddt

from rally import consts
from rally import exceptions
from rally.common import objects
from tests.unit import test


@ddt.ddt
class TaskTestCase(test.TestCase):
    def setUp(self):
        super().setUp()
        self.task = {
            "uuid": "00ef46a2-c5b8-4aea-a5ca-0f54a10cbca1",
            "status": consts.TaskStatus.INIT,
            "validation_result": {},
        }

    @mock.patch("rally.common.objects.task.db.task_create")
    def test_init_with_create(self, mock_task_create):
        mock_task_create.return_value = self.task
        task = objects.Task(status=consts.TaskStatus.CRASHED)
        mock_task_create.assert_called_once_with({
            "status": consts.TaskStatus.CRASHED})
        self.assertEqual(task["uuid"], self.task["uuid"])

    @mock.patch("rally.common.objects.task.db.task_create")
    def test_init_without_create(self, mock_task_create):
        task = objects.Task(task=self.task)
        self.assertFalse(mock_task_create.called)
        self.assertEqual(task["uuid"], self.task["uuid"])

    @mock.patch("rally.common.objects.task.uuid.uuid4",
                return_value="some_uuid")
    @mock.patch("rally.common.objects.task.db.task_create")
    def test_init_with_fake_true(self, mock_task_create, mock_uuid4):
        task = objects.Task(temporary=True)
        self.assertFalse(mock_task_create.called)
        self.assertTrue(mock_uuid4.called)
        self.assertEqual(task["uuid"], mock_uuid4.return_value)

    @mock.patch("rally.common.db.api.task_get")
    def test_get(self, mock_task_get):
        mock_task_get.return_value = self.task
        task = objects.Task.get(self.task["uuid"])
        mock_task_get.assert_called_once_with(self.task["uuid"],
                                              detailed=False)
        self.assertEqual(task["uuid"], self.task["uuid"])

    @mock.patch("rally.common.objects.task.db.task_get_status")
    def test_get_status(self, mock_task_get_status):
        task = objects.Task(task=self.task)
        status = task.get_status(task["uuid"])
        self.assertEqual(status, mock_task_get_status.return_value)

    @mock.patch("rally.common.objects.task.db.task_delete")
    @mock.patch("rally.common.objects.task.db.task_create")
    def test_create_and_delete(self, mock_task_create, mock_task_delete):
        mock_task_create.return_value = self.task
        task = objects.Task()
        task.delete()
        mock_task_delete.assert_called_once_with(
            self.task["uuid"], status=None)

    @mock.patch("rally.common.objects.task.db.task_delete")
    @mock.patch("rally.common.objects.task.db.task_create")
    def test_create_and_delete_status(self, mock_task_create,
                                      mock_task_delete):
        mock_task_create.return_value = self.task
        task = objects.Task()
        task.delete(status=consts.TaskStatus.FINISHED)
        mock_task_delete.assert_called_once_with(
            self.task["uuid"], status=consts.TaskStatus.FINISHED)

    @mock.patch("rally.common.objects.task.db.task_delete")
    def test_delete_by_uuid(self, mock_task_delete):
        objects.Task.delete_by_uuid(self.task["uuid"])
        mock_task_delete.assert_called_once_with(
            self.task["uuid"], status=None)

    @mock.patch("rally.common.objects.task.db.task_delete")
    def test_delete_by_uuid_status(self, mock_task_delete):
        objects.Task.delete_by_uuid(self.task["uuid"],
                                    consts.TaskStatus.FINISHED)
        mock_task_delete.assert_called_once_with(
            self.task["uuid"], status=consts.TaskStatus.FINISHED)

    @mock.patch("rally.common.objects.task.db.task_list",
                return_value=[{"uuid": "a",
                               "created_at": "b",
                               "status": consts.TaskStatus.CRASHED,
                               "tag": "d",
                               "deployment_name": "some_name"}])
    def list(self, mock_db_task_list):
        tasks = objects.Task.list(status="somestatus")
        mock_db_task_list.assert_called_once_with("somestatus", None)
        self.assertIs(type(tasks), list)
        self.assertIsInstance(tasks[0], objects.Task)
        self.assertEqual(mock_db_task_list.return_value["uuis"],
                         tasks[0]["uuid"])

    @mock.patch("rally.common.objects.task.db.task_update")
    @mock.patch("rally.common.objects.task.db.task_create")
    def test_update(self, mock_task_create, mock_task_update):
        mock_task_create.return_value = self.task
        mock_task_update.return_value = {"opt": "val2"}
        deploy = objects.Task(opt="val1")
        deploy._update({"opt": "val2"})
        mock_task_update.assert_called_once_with(
            self.task["uuid"], {"opt": "val2"})
        self.assertEqual(deploy["opt"], "val2")

    @ddt.data(
        {
            "status": "some_status", "allowed_statuses": ("s_1", "s_2")
        },
        {
            "status": "some_status", "allowed_statuses": None
        }
    )
    @ddt.unpack
    @mock.patch("rally.common.objects.task.db.task_update_status")
    @mock.patch("rally.common.objects.task.db.task_update")
    def test_update_status(self, mock_task_update, mock_task_update_status,
                           status, allowed_statuses):
        task = objects.Task(task=self.task)
        task.update_status(consts.TaskStatus.FINISHED, allowed_statuses)
        if allowed_statuses:
            self.assertFalse(mock_task_update.called)
            mock_task_update_status.assert_called_once_with(
                self.task["uuid"],
                consts.TaskStatus.FINISHED,
                allowed_statuses
            )
        else:
            self.assertFalse(mock_task_update_status.called)
            mock_task_update.assert_called_once_with(
                self.task["uuid"],
                {"status": consts.TaskStatus.FINISHED},
            )

    @mock.patch("rally.common.objects.task.db.task_update")
    def test_update_verification_log(self, mock_task_update):
        mock_task_update.return_value = self.task
        task = objects.Task(task=self.task)
        task.set_validation_failed({"a": "fake"})
        mock_task_update.assert_called_once_with(
            self.task["uuid"],
            {"status": consts.TaskStatus.VALIDATION_FAILED,
             "validation_result": {"a": "fake"}}
        )

    @mock.patch("rally.common.objects.task.db.env_get")
    def test_to_dict(self, mock_env_get):
        workloads = [{"created_at": dt.datetime.now(),
                      "updated_at": dt.datetime.now()}]
        self.task.update({"env_uuid": "deployment_uuid",
                          "deployment_uuid": "deployment_uuid",
                          "created_at": dt.datetime.now(),
                          "updated_at": dt.datetime.now()})

        mock_env_get.return_value = {"name": "deployment_name"}

        task = objects.Task(task=self.task)
        serialized_task = task.to_dict()

        mock_env_get.assert_called_once_with(self.task["env_uuid"])
        self.assertEqual(self.task, serialized_task)

        self.task["subtasks"] = [{"workloads": workloads}]

    @mock.patch("rally.common.db.api.task_get")
    def test_get_detailed(self, mock_task_get):
        mock_task_get.return_value = {"results": [{
            "created_at": dt.datetime.now(),
            "updated_at": dt.datetime.now()}]}
        task_detailed = objects.Task.get("task_id", detailed=True)
        mock_task_get.assert_called_once_with("task_id", detailed=True)
        self.assertEqual(mock_task_get.return_value, task_detailed.task)

    @mock.patch("rally.common.objects.task.db.task_update")
    def test_set_failed(self, mock_task_update):
        mock_task_update.return_value = self.task
        task = objects.Task(task=self.task)
        task.set_failed("foo_type", "foo_error_message", "foo_trace")
        mock_task_update.assert_called_once_with(
            self.task["uuid"],
            {"status": consts.TaskStatus.CRASHED,
             "validation_result": {"etype": "foo_type",
                                   "msg": "foo_error_message",
                                   "trace": "foo_trace"}},
        )

    @mock.patch("rally.common.objects.task.Subtask")
    def test_add_subtask(self, mock_subtask):
        task = objects.Task(task=self.task)
        subtask = task.add_subtask(title="foo")
        mock_subtask.assert_called_once_with(
            self.task["uuid"], title="foo", contexts=None, description=None)
        self.assertIs(subtask, mock_subtask.return_value)

    @ddt.data(
        {
            "soft": True, "status": consts.TaskStatus.INIT
        },
        {
            "soft": True, "status": consts.TaskStatus.ABORTED
        },
        {
            "soft": True, "status": consts.TaskStatus.FINISHED
        },
        {
            "soft": True, "status": consts.TaskStatus.CRASHED
        },
        {
            "soft": False, "status": consts.TaskStatus.ABORTED
        },
        {
            "soft": False, "status": consts.TaskStatus.FINISHED
        },
        {
            "soft": False, "status": consts.TaskStatus.CRASHED
        }
    )
    @ddt.unpack
    def test_abort_with_finished_states(self, soft, status):
        task = objects.Task(mock.MagicMock(), fake=True)
        task.get_status = mock.MagicMock(return_value=status)
        task.update_status = mock.MagicMock()

        self.assertRaises(exceptions.RallyException, task.abort, soft)

        self.assertEqual(1, task.get_status.call_count)
        self.assertFalse(task.update_status.called)

    @ddt.data(True, False)
    def test_abort_with_running_state(self, soft):
        task = objects.Task(mock.MagicMock(), fake=True)
        task.get_status = mock.MagicMock(return_value="running")
        task.update_status = mock.MagicMock()

        task.abort(soft)
        if soft:
            status = consts.TaskStatus.SOFT_ABORTING
        else:
            status = consts.TaskStatus.ABORTING

        task.update_status.assert_called_once_with(
            status,
            allowed_statuses=(consts.TaskStatus.RUNNING,
                              consts.TaskStatus.SOFT_ABORTING)
        )

    @ddt.data(
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "output": {"additive": [], "complete": []},
                  "error": ["err1", "err2"], "atomic_actions": []},
         "expected": True},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": [{"name": "foo", "started_at": 1.0,
                                      "finished_at": 5.2, "children": []}]},
         "expected": True},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": ["a1", "a2"],
                                          "complete": ["c1", "c2"]},
                  "atomic_actions": [{"name": "foo", "started_at": 1.0,
                                      "finished_at": 5.2, "children": []}]},
         "validate_output_calls": [("additive", "a1"), ("additive", "a2"),
                                   ("complete", "c1"), ("complete", "c2")],
         "expected": True},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": ["a1", "a2"],
                                          "complete": ["c1", "c2"]},
                  "atomic_actions": [{"name": "foo", "started_at": 1.0,
                                      "finished_at": 5.2, "children": []}]},
         "validate_output_return_value": "validation error message"},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [42], "output": {"additive": [], "complete": []},
                  "atomic_actions": [{"name": "foo", "started_at": 1.0,
                                      "finished_at": 5.2, "children": []}]}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": [{"name": "foo", "started_at": 10,
                                      "finished_at": 52, "children": []}]}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": [{"name": "non-float", "started_at": 1.0,
                                      "children": []}]}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": [{"name": "foo", "started_at": 1.0,
                                      "finished_at": 4.0,
                                      "children": [{"name": "foo1",
                                                    "started_at": 2.0,
                                                    "finished_at": 3.0,
                                                    "children": []}]}]},
         "expected": True},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": [{"name": "foo", "started_at": 1.0,
                                      "finished_at": 4.0,
                                      "children": [{"name": "foo1",
                                                    "started_at": 20,
                                                    "finished_at": 30,
                                                    "children": []}]}]}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": [{"name": "foo", "started_at": 1.0,
                                      "finished_at": 4.0,
                                      "children": [{"name": "foo1",
                                                    "started_at": 2.0,
                                                    "finished_at": 3.0}]}]}},
        {"data": {"duration": 1, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1,
                  "error": [], "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": "foo", "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {}, "atomic_actions": []}},
        {"data": {"timestamp": 1.0, "idle_duration": 1.0, "error": [],
                  "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "idle_duration": 1.0, "error": [],
                  "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "error": [],
                  "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "output": {"additive": [], "complete": []},
                  "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "atomic_actions": []}},
        {"data": {"duration": 1.0, "timestamp": 1.0, "idle_duration": 1.0,
                  "error": [], "output": {"additive": [], "complete": []}}},
        {"data": []},
        {"data": {}},
        {"data": "foo"})
    @ddt.unpack
    @mock.patch("rally.common.objects.task.LOG")
    @mock.patch("rally.common.objects.task.charts.validate_output")
    def test_result_has_valid_schema(self, mock_validate_output, mock_log,
                                     data, expected=False,
                                     validate_output_return_value=None,
                                     validate_output_calls=None):
        task = objects.Task(task=self.task)
        mock_validate_output.return_value = validate_output_return_value
        self.assertEqual(expected,
                         task.result_has_valid_schema(data),
                         msg=repr(data))
        if validate_output_calls:
            mock_validate_output.assert_has_calls(
                [mock.call(*args) for args in validate_output_calls],
                any_order=True)


class SubtaskTestCase(test.TestCase):

    def setUp(self):
        super().setUp()
        self.subtask = {
            "task_uuid": "00ef46a2-c5b8-4aea-a5ca-0f54a10cbca1",
            "uuid": "00ef46a2-c5b8-4aea-a5ca-0f54a10cbca2",
            "title": "foo",
        }

    @mock.patch("rally.common.objects.task.db.subtask_create")
    def test_init(self, mock_subtask_create):
        mock_subtask_create.return_value = self.subtask
        subtask = objects.Subtask("bar", title="foo")
        mock_subtask_create.assert_called_once_with(
            "bar", title="foo", contexts=None, description=None)
        self.assertEqual(subtask["uuid"], self.subtask["uuid"])

    @mock.patch("rally.common.objects.task.db.subtask_update")
    @mock.patch("rally.common.objects.task.db.subtask_create")
    def test_update_status(self, mock_subtask_create, mock_subtask_update):
        mock_subtask_create.return_value = self.subtask
        subtask = objects.Subtask("bar", title="foo")
        subtask.update_status(consts.SubtaskStatus.FINISHED)
        mock_subtask_update.assert_called_once_with(
            self.subtask["uuid"], {"status": consts.SubtaskStatus.FINISHED})

    @mock.patch("rally.common.objects.task.Workload")
    @mock.patch("rally.common.objects.task.db.subtask_create")
    def test_add_workload(self, mock_subtask_create, mock_workload):
        mock_subtask_create.return_value = self.subtask
        subtask = objects.Subtask("bar", title="foo")

        name = "w"
        description = "descr"
        position = 0
        runner_type = "runner"
        runner = {}
        contexts = {"users": {}}
        sla = {"failure_rate": {"max": 0}}
        args = {"arg": "xxx"}
        hooks = [{"foo": "bar"}]

        workload = subtask.add_workload(
            name, description=description, position=position,
            runner_type=runner_type, runner=runner, contexts=contexts, sla=sla,
            args=args, hooks=hooks)
        mock_workload.assert_called_once_with(
            task_uuid=self.subtask["task_uuid"],
            subtask_uuid=self.subtask["uuid"], name=name,
            description=description, position=position,
            runner_type=runner_type, runner=runner,
            contexts=contexts, sla=sla, args=args,
            hooks=[{"config": h} for h in hooks])
        self.assertIs(workload, mock_workload.return_value)


class WorkloadTestCase(test.TestCase):

    def setUp(self):
        super().setUp()
        self.workload = {
            "task_uuid": "00ef46a2-c5b8-4aea-a5ca-0f54a10cbca1",
            "subtask_uuid": "00ef46a2-c5b8-4aea-a5ca-0f54a10cbca2",
            "uuid": "00ef46a2-c5b8-4aea-a5ca-0f54a10cbca3",
        }

    @mock.patch("rally.common.objects.task.db.workload_create")
    def test_init(self, mock_workload_create):
        mock_workload_create.return_value = self.workload
        name = "w"
        description = "descr"
        position = 0
        runner_type = "constant"
        runner = {"times": 3}
        contexts = {"users": {}}
        sla = {"failure_rate": {"max": 0}}
        args = {"arg": "xxx"}
        hooks = [{"config": {"foo": "bar"}}]
        workload = objects.Workload("uuid1", "uuid2", name=name,
                                    description=description, position=position,
                                    runner=runner, runner_type=runner_type,
                                    contexts=contexts, sla=sla,
                                    args=args, hooks=hooks)
        mock_workload_create.assert_called_once_with(
            task_uuid="uuid1", subtask_uuid="uuid2", name=name, hooks=hooks,
            description=description, position=position, runner=runner,
            runner_type="constant", contexts=contexts, sla=sla, args=args)
        self.assertEqual(workload["uuid"], self.workload["uuid"])

    @mock.patch("rally.common.objects.task.db.workload_data_create")
    @mock.patch("rally.common.objects.task.db.workload_create")
    def test_add_workload_data(self, mock_workload_create,
                               mock_workload_data_create):
        mock_workload_create.return_value = self.workload
        workload = objects.Workload("uuid1", "uuid2", name="w",
                                    description="descr", position=0,
                                    runner_type="foo", runner={},
                                    contexts=None,
                                    sla=None, args=None, hooks=[])

        workload.add_workload_data(0, {"data": "foo"})
        mock_workload_data_create.assert_called_once_with(
            self.workload["task_uuid"], self.workload["uuid"],
            0, {"data": "foo"})

    @mock.patch("rally.common.objects.task.db.workload_set_results")
    @mock.patch("rally.common.objects.task.db.workload_create")
    def test_set_results(self, mock_workload_create,
                         mock_workload_set_results):
        mock_workload_create.return_value = self.workload
        name = "w"
        description = "descr"
        position = 0
        runner_type = "constant"
        runner = {"times": 3}
        contexts = {"users": {}}
        sla = {"failure_rate": {"max": 0}}
        args = {"arg": "xxx"}
        load_duration = 88
        full_duration = 99
        start_time = 1231231277.22
        sla_results = []
        hooks = []
        contexts_results = [{"name": "setup:something"}]
        workload = objects.Workload("uuid1", "uuid2", name=name,
                                    description=description, position=position,
                                    runner=runner, runner_type=runner_type,
                                    contexts=contexts, sla=sla, args=args,
                                    hooks=hooks)

        workload.set_results(load_duration=load_duration,
                             full_duration=full_duration,
                             start_time=start_time,
                             sla_results=sla_results,
                             contexts_results=contexts_results)
        mock_workload_set_results.assert_called_once_with(
            workload_uuid=self.workload["uuid"],
            subtask_uuid=self.workload["subtask_uuid"],
            task_uuid=self.workload["task_uuid"],
            load_duration=load_duration, full_duration=full_duration,
            start_time=start_time, sla_results=sla_results,
            contexts_results=contexts_results,
            hooks_results=None)

    def test_to_task(self):
        workload = {
            "id": 777,
            "uuid": "uuiiidd",
            "task_uuid": "task-uuid",
            "subtask_uuid": "subtask-uuid",
            "name": "Foo.bar",
            "description": "Make something useful (or not).",
            "position": 3,
            "runner_type": "constant",
            "runner": {"times": 3},
            "contexts": {"users": {}},
            "sla": {"failure_rate": {"max": 0}},
            "args": {"key1": "value1"},
            "hooks": [{"config": {
                "action": ["foo", {"arg1": "v1"}],
                "trigger": ["bar", {"arg2": "v2"}]
            }}],
            "sla_results": {"sla": []},
            "context_execution": {},
            "start_time": "2997.23.12",
            "load_duration": 42,
            "full_duration": 37,
            "min_duration": 1,
            "max_duration": 2,
            "total_iteration_count": 7,
            "failed_iteration_count": 2,
            "statistics": {},
            "pass_sla": False
        }
        expected_task = collections.OrderedDict([
            ("version", 2),
            ("title", "A cropped version of a bigger task."),
            ("description", "Auto-generated task from a single workload "
                            "(uuid=%s)" % workload["uuid"]),
            ("subtasks",
             [collections.OrderedDict([
                 ("title", workload["name"]),
                 ("description", workload["description"]),
                 ("scenario", {workload["name"]: workload["args"]}),
                 ("contexts", workload["contexts"]),
                 ("runner", {"constant": {"times": 3}}),
                 ("hooks", [{"action": {"foo": {"arg1": "v1"}},
                             "trigger": {"bar": {"arg2": "v2"}},
                             "description": None}]),
                 ("sla", workload["sla"])])])])
        self.assertEqual(expected_task, objects.Workload.to_task(workload))
