# 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.

import datetime as dt
import io
import json
import os.path
import sys

import ddt
import mock

import rally
from rally import api
from rally.cli import cliutils
from rally.cli.commands import task
from rally.common import yamlutils as yaml
from rally import consts
from rally import exceptions
from tests.unit import fakes
from tests.unit import test


@ddt.ddt
class TaskCommandsTestCase(test.TestCase):

    def setUp(self):
        super(TaskCommandsTestCase, self).setUp()
        self.task = task.TaskCommands()
        self.fake_api = fakes.FakeAPI()

        with mock.patch("rally.api.API.check_db_revision"):
            self.real_api = api.API()

    @mock.patch("rally.cli.commands.task.open", create=True)
    def test__load_and_validate_task(self, mock_open):
        input_task = "{'ab': {{test}}}"
        input_args = "{'test': 2}"

        mock_open.side_effect = [
            mock.mock_open(read_data=input_task).return_value,
            mock.mock_open(read_data="{'test': 1}").return_value
        ]
        task_conf = self.task._load_and_validate_task(
            self.real_api, "in_task", args_file="in_args_path")
        self.assertEqual({"ab": 1}, task_conf)

        mock_open.side_effect = [
            mock.mock_open(read_data=input_task).return_value
        ]
        task_conf = self.task._load_and_validate_task(
            self.real_api, "in_task", raw_args=input_args)
        self.assertEqual({"ab": 2}, task_conf)

        mock_open.side_effect = [
            mock.mock_open(read_data=input_task).return_value,
            mock.mock_open(read_data="{'test': 1}").return_value
        ]
        task_conf = self.task._load_and_validate_task(
            self.real_api, "in_task", raw_args=input_args,
            args_file="any_file")
        self.assertEqual({"ab": 2}, task_conf)

        mock_open.side_effect = [
            mock.mock_open(read_data=input_task).return_value,
            mock.mock_open(read_data="{'test': 1}").return_value
        ]
        task_conf = self.task._load_and_validate_task(
            self.real_api, "in_task", raw_args="test=2",
            args_file="any_file")
        self.assertEqual({"ab": 2}, task_conf)

    @mock.patch("rally.cli.commands.task.open", create=True)
    def test__load_task_wrong_task_args_file(self, mock_open):

        def open_return_value(filename):
            if filename == "in_task":
                return mock.Mock()
            else:
                raise IOError()

        mock_open.side_effect = open_return_value

        e = self.assertRaises(task.FailedToLoadTask,
                              self.task._load_and_validate_task,
                              self.fake_api, task_file="in_task",
                              args_file="in_args_path")
        self.assertEqual("Invalid --task-args-file passed:\n\n\t Error "
                         "reading in_args_path: ", e.format_message())

    @mock.patch("rally.cli.commands.task.yaml.safe_load")
    def test__load_task_wrong_input_task_args(self, mock_safe_load):
        mock_safe_load.side_effect = yaml.ParserError("foo")
        # use real file to avoid mocking open
        task_file = __file__

        e = self.assertRaises(task.FailedToLoadTask,
                              self.task._load_and_validate_task, self.real_api,
                              task_file, raw_args="{'test': {}")
        self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
                         "YAML or JSON. Details:\n\nfoo", e.format_message())
        mock_safe_load.assert_called_once_with("{'test': {}")

        # the case #2
        mock_safe_load.reset_mock()
        e = self.assertRaises(task.FailedToLoadTask,
                              self.task._load_and_validate_task, self.real_api,
                              task_file, raw_args="[]")
        self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
                         "YAML or JSON. Details:\n\nfoo", e.format_message())
        mock_safe_load.assert_called_once_with("[]")

        # the case #3
        mock_safe_load.reset_mock()
        e = self.assertRaises(task.FailedToLoadTask,
                              self.task._load_and_validate_task, self.real_api,
                              task_file, raw_args="foo")
        self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
                         "YAML or JSON. Details:\n\nfoo", e.format_message())
        mock_safe_load.assert_called_once_with("foo")

    @mock.patch("rally.cli.commands.task.open", create=True)
    def test__load_task_task_render_raise_exc(self, mock_open):
        mock_open.side_effect = [
            mock.mock_open(read_data="{'test': {{t}}}").return_value
        ]
        e = self.assertRaises(task.FailedToLoadTask,
                              self.task._load_and_validate_task, self.real_api,
                              "in_task")
        self.assertEqual("Invalid --task passed:\n\n\t Failed to render task "
                         "template.\n\nPlease specify template task argument: "
                         "t", e.format_message())

    @mock.patch("rally.cli.commands.task.yaml")
    @mock.patch("rally.cli.commands.task.open", create=True)
    def test__load_task_task_not_in_yaml(self, mock_open, mock_yaml):
        mock_open.side_effect = [
            mock.mock_open(read_data="{'test': {}").return_value
        ]
        mock_yaml.safe_load.side_effect = Exception("ERROR!!!PANIC!!!")

        e = self.assertRaises(task.FailedToLoadTask,
                              self.task._load_and_validate_task, self.fake_api,
                              "in_task")
        self.assertEqual("Invalid --task passed:\n\n\t Wrong format of "
                         "rendered input task. It should be YAML or JSON. "
                         "Details:\n\nERROR!!!PANIC!!!", e.format_message())

    def test_load_task_including_other_template(self):
        other_template_path = os.path.join(
            os.path.dirname(rally.__file__), os.pardir,
            "samples/tasks/scenarios/dummy/dummy.json")
        input_task = "{%% include \"%s\" %%}" % os.path.basename(
            other_template_path)
        expect = self.task._load_and_validate_task(self.real_api,
                                                   other_template_path)

        with mock.patch("rally.cli.commands.task.open",
                        create=True) as mock_open:
            mock_open.side_effect = [
                mock.mock_open(read_data=input_task).return_value
            ]
            input_task_file = os.path.join(
                os.path.dirname(other_template_path), "input_task.json")
            actual = self.task._load_and_validate_task(self.real_api,
                                                       input_task_file)
        self.assertEqual(expect, actual)

    @mock.patch("rally.cli.commands.task.open", create=True)
    def test__load_and_validate_file_failed(self, mock_open):
        mock_open.side_effect = IOError

        e = self.assertRaises(task.FailedToLoadTask,
                              self.task._load_and_validate_task,
                              api=self.fake_api, task_file="some_task",
                              raw_args="task_args", args_file="task_args_file")
        self.assertEqual(
            "Invalid --task passed:\n\n\t Error reading some_task: ",
            e.format_message())

    @mock.patch("rally.cli.commands.task.version")
    @mock.patch("rally.cli.commands.task.TaskCommands.use")
    @mock.patch("rally.cli.commands.task.TaskCommands._detailed")
    @mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
                return_value={"some": "json"})
    def test_start(self, mock__load_and_validate_task, mock__detailed,
                   mock_use, mock_version):
        deployment_id = "e0617de9-77d1-4875-9b49-9d5789e29f20"
        task_path = "path_to_config.json"
        fake_task = fakes.FakeTask(uuid="some_new_uuid", tags=["tag"])
        mock__detailed.return_value = 1
        self.fake_api.task.create.return_value = fake_task
        self.fake_api.task.validate.return_value = fakes.FakeTask(
            some="json", uuid="some_uuid", temporary=True)

        val = self.task.start(self.fake_api, task_path,
                              deployment_id, do_use=True)
        self.assertEqual(2, val)
        mock_version.version_string.assert_called_once_with()
        self.fake_api.task.create.assert_called_once_with(
            deployment=deployment_id, tags=None)
        self.fake_api.task.start.assert_called_once_with(
            deployment=deployment_id,
            config=mock__load_and_validate_task.return_value,
            task=fake_task["uuid"],
            abort_on_sla_failure=False)
        mock__load_and_validate_task.assert_called_once_with(
            self.fake_api, task_path, args_file=None, raw_args=None)
        mock_use.assert_called_once_with(self.fake_api, "some_new_uuid")
        mock__detailed.assert_called_once_with(self.fake_api,
                                               task_id=fake_task["uuid"])
        mock__detailed.return_value = 0
        val1 = self.task.start(self.fake_api, task_path,
                               deployment_id, do_use=True)
        self.assertEqual(0, val1)

    @mock.patch("rally.cli.commands.task.TaskCommands._detailed")
    @mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
                return_value="some_config")
    def test_start_on_unfinished_deployment(self, mock__load_and_validate_task,
                                            mock__detailed):
        deployment_id = "e0617de9-77d1-4875-9b49-9d5789e29f20"
        deployment_name = "xxx_name"
        task_path = "path_to_config.json"
        fake_task = fakes.FakeTask(uuid="some_new_uuid", tag="tag")
        self.fake_api.task.create.return_value = fake_task

        exc = exceptions.DeploymentNotFinishedStatus(
            name=deployment_name,
            uuid=deployment_id,
            status=consts.DeployStatus.DEPLOY_INIT)
        self.fake_api.task.create.side_effect = exc
        self.assertEqual(1, self.task.start(self.fake_api, task_path,
                                            deployment="any",
                                            tags=["some_tag"]))
        self.assertFalse(mock__detailed.called)

    @mock.patch("rally.cli.commands.task.TaskCommands._detailed")
    @mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
                return_value="some_config")
    def test_start_with_task_args(self, mock__load_and_validate_task,
                                  mock__detailed):
        fake_task = fakes.FakeTask(uuid="new_uuid", tags=["some_tag"])
        self.fake_api.task.create.return_value = fakes.FakeTask(
            uuid="new_uuid", tags=["some_tag"])
        self.fake_api.task.validate.return_value = fakes.FakeTask(
            uuid="some_id")

        task_path = "path_to_config.json"
        task_args = "task_args"
        task_args_file = "task_args_file"
        self.task.start(self.fake_api, task_path, deployment="any",
                        task_args=task_args, task_args_file=task_args_file,
                        tags=["some_tag"])

        mock__load_and_validate_task.assert_called_once_with(
            self.fake_api, task_path, raw_args=task_args,
            args_file=task_args_file)

        self.fake_api.task.start.assert_called_once_with(
            deployment="any",
            config=mock__load_and_validate_task.return_value,
            task=fake_task["uuid"],
            abort_on_sla_failure=False)
        mock__detailed.assert_called_once_with(
            self.fake_api,
            task_id=fake_task["uuid"])
        self.fake_api.task.create.assert_called_once_with(
            deployment="any", tags=["some_tag"])

    @mock.patch("rally.cli.commands.task.envutils.get_global")
    def test_start_no_deployment_id(self, mock_get_global):
        mock_get_global.side_effect = exceptions.InvalidArgumentsException
        self.assertRaises(exceptions.InvalidArgumentsException,
                          self.task.start, "path_to_config.json", None)

    @mock.patch("rally.cli.commands.task.TaskCommands._detailed")
    @mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task")
    def test_start_invalid_task(self, mock__load_and_validate_task,
                                mock__detailed):
        task_obj = fakes.FakeTask(temporary=False, tag="tag", uuid="uuid")
        self.fake_api.task.create.return_value = task_obj
        exc = exceptions.InvalidTaskException("foo")

        mock__load_and_validate_task.side_effect = exc

        self.assertRaises(exceptions.InvalidTaskException,
                          self.task.start, self.fake_api, "task_path",
                          "deployment", tags=["tag"])

        self.assertFalse(self.fake_api.task.create.called)
        self.assertFalse(self.fake_api.task.start.called)

        # the case 2
        task_cfg = {"some": "json"}
        mock__load_and_validate_task.side_effect = (task_cfg, )
        self.fake_api.task.start.side_effect = KeyError()

        self.assertRaises(KeyError,
                          self.task.start, self.fake_api, "task_path",
                          "deployment", tags=["tag"])

        self.fake_api.task.create.assert_called_once_with(
            deployment="deployment", tags=["tag"])

        self.fake_api.task.start.assert_called_once_with(
            deployment="deployment", config=task_cfg,
            task=task_obj["uuid"],
            abort_on_sla_failure=False)

        self.assertFalse(mock__detailed.called)

    def test_abort(self):
        test_uuid = "17860c43-2274-498d-8669-448eff7b073f"
        self.task.abort(self.fake_api, test_uuid)
        self.fake_api.task.abort.assert_called_once_with(
            task_uuid=test_uuid, soft=False, wait=True)

    @mock.patch("rally.cli.commands.task.envutils.get_global")
    def test_abort_no_task_id(self, mock_get_global):
        mock_get_global.side_effect = exceptions.InvalidArgumentsException
        self.assertRaises(exceptions.InvalidArgumentsException,
                          self.task.abort, self.fake_api, None)

    def test_status(self):
        test_uuid = "a3e7cefb-bec2-4802-89f6-410cc31f71af"
        value = {"task_id": "task", "status": "status"}
        self.fake_api.task.get.return_value = value
        self.task.status(self.fake_api, test_uuid)
        self.fake_api.task.get.assert_called_once_with(task_id=test_uuid)

    @mock.patch("rally.cli.commands.task.envutils.get_global")
    def test_status_no_task_id(self, mock_get_global):
        mock_get_global.side_effect = exceptions.InvalidArgumentsException
        self.assertRaises(exceptions.InvalidArgumentsException,
                          self.task.status, None)

    @ddt.data({"iterations_data": False, "has_output": True,
               "filters": None},
              {"iterations_data": True, "has_output": False,
               "filters": ["scenario=fake_name", "sla_failures"]})
    @ddt.unpack
    def test_detailed(self, iterations_data, has_output, filters):
        test_uuid = "c0d874d4-7195-4fd5-8688-abe82bfad36f"
        detailed_value = {
            "id": "task", "uuid": test_uuid,
            "pass_sla": False, "status": "finished",
            "subtasks": [{"workloads": [{
                "name": "fake_name", "position": "fake_pos",
                "args": "args", "contexts": "context", "sla": "sla",
                "runner": "runner", "hooks": [],
                "statistics": {
                    "durations": {
                        "atomics": [
                            {
                                "name": "foo",
                                "display_name": "foo (x2)",
                                "count_per_iteration": 2,
                                "children": [
                                    {"name": "inner_foo",
                                     "display_name": "inner_foo",
                                     "count_per_iteration": 1,
                                     "children": [],
                                     "data": {
                                         "min": 1,
                                         "median": 2,
                                         "90%ile": 1.5,
                                         "95%ile": 1.6,
                                         "max": 3,
                                         "avg": 1.4,
                                         "success": 3,
                                         "iteration_count": 3}
                                     }
                                ],
                                "data": {
                                    "min": 1,
                                    "median": 2,
                                    "90%ile": 1.5,
                                    "95%ile": 1.6,
                                    "max": 3,
                                    "avg": 1.4,
                                    "success": 3,
                                    "iteration_count": 3}},
                            {
                                "name": "bar",
                                "display_name": "bar",
                                "count_per_iteration": 1,
                                "children": [],
                                "data": {
                                    "min": 1.1,
                                    "median": 2.2,
                                    "90%ile": 1.6,
                                    "95%ile": 1.65,
                                    "max": 3,
                                    "avg": 1.5,
                                    "success": 3,
                                    "iteration_count": 3}
                            }],
                        "total": {
                            "name": "total",
                            "display_name": "bar",
                            "count_per_iteration": 1,
                            "children": [],
                            "data": {
                                "min": 1,
                                "median": 2.1,
                                "90%ile": 1.55,
                                "95%ile": 1.62,
                                "max": 3,
                                "avg": 1.45,
                                "success": 6,
                                "iteration_count": 6}}}},
                "load_duration": 3.2,
                "full_duration": 3.5,
                "total_iteration_count": 4,
                "data": [
                    {
                        "duration": 0.9,
                        "idle_duration": 0.1,
                        "output": {"additive": [], "complete": []},
                        "atomic_actions": [
                            {"name": "foo", "started_at": 0.0,
                             "finished_at": 0.6, "children": []},
                            {"name": "bar", "started_at": 0.6,
                             "finished_at": 1.3, "children": []}
                        ],
                        "error": ["type", "message", "traceback"]
                    },
                    {
                        "duration": 1.2,
                        "idle_duration": 0.3,
                        "output": {"additive": [], "complete": []},
                        "atomic_actions": [
                            {"name": "foo", "started_at": 0.0,
                             "finished_at": 0.6, "children": []},
                            {"name": "bar", "started_at": 0.6,
                             "finished_at": 1.3, "children": []}
                        ],
                        "error": ["type", "message", "traceback"]
                    },
                    {
                        "duration": 0.7,
                        "idle_duration": 0.5,
                        "output": {
                            "additive": [
                                {"data": [("foo", 0.6), ("bar", 0.7)],
                                 "title": "Scenario output",
                                 "description": "",
                                 "chart_plugin": "StackedArea"}
                            ],
                            "complete": []
                        },
                        "atomic_actions": [
                            {"name": "foo", "started_at": 0.0,
                             "finished_at": 0.6, "children": []},
                            {"name": "bar", "started_at": 0.6,
                             "finished_at": 1.3, "children": []}
                        ],
                        "error": ["type", "message", "traceback"]
                    },
                    {
                        "duration": 0.5,
                        "idle_duration": 0.5,
                        "atomic_actions": [
                            {"name": "foo", "started_at": 0.0,
                             "finished_at": 0.6, "children": []},
                            {"name": "bar", "started_at": 0.6,
                             "finished_at": 1.3, "children": []}
                        ],
                        "error": ["type", "message", "traceback"]
                    }],
            }]}]}
        if has_output:
            detailed_value["subtasks"][0]["workloads"][0]["output"] = {
                "additive": [], "complete": []}
        self.fake_api.task.get.return_value = detailed_value
        self.task.detailed(self.fake_api, test_uuid,
                           iterations_data=iterations_data,
                           filters=filters)
        self.fake_api.task.get.assert_called_once_with(
            task_id=test_uuid, detailed=True)

    @mock.patch("rally.cli.commands.task.sys.stdout")
    @mock.patch("rally.cli.commands.task.logging")
    @ddt.data({"debug": True},
              {"debug": False})
    @ddt.unpack
    def test_detailed_task_failed(self, mock_logging, mock_stdout, debug):
        test_uuid = "test_task_id"
        value = {
            "id": "task",
            "uuid": test_uuid,
            "status": consts.TaskStatus.CRASHED,
            "results": [],
            "validation_result": {"etype": "error_type",
                                  "msg": "error_message",
                                  "trace": "error_traceback"}
        }
        self.fake_api.task.get.return_value = value

        mock_logging.is_debug.return_value = debug
        self.task.detailed(self.fake_api, test_uuid)
        if debug:
            expected_calls = [
                mock.call("Task test_task_id: crashed"),
                mock.call("%s" % value["validation_result"]["trace"])]
            mock_stdout.write.assert_has_calls(expected_calls, any_order=True)
        else:
            expected_calls = [
                mock.call("Task test_task_id: crashed"),
                mock.call("%s" % value["validation_result"]["etype"]),
                mock.call("%s" % value["validation_result"]["msg"]),
                mock.call("\nFor more details run:\n"
                          "rally -d task detailed %s" % test_uuid)]
            mock_stdout.write.assert_has_calls(expected_calls, any_order=True)

    @mock.patch("rally.cli.commands.task.sys.stdout")
    def test_detailed_task_status_not_in_finished_abort(self, mock_stdout):
        test_uuid = "test_task_id"
        value = {
            "id": "task",
            "uuid": test_uuid,
            "status": consts.TaskStatus.INIT,
            "results": []
        }
        self.fake_api.task.get.return_value = value
        self.task.detailed(self.fake_api, test_uuid)
        expected_calls = [mock.call("Task test_task_id: init"),
                          mock.call("\nThe task test_task_id marked as "
                                    "'init'. Results available when it "
                                    "is 'finished'.")]
        mock_stdout.write.assert_has_calls(expected_calls, any_order=True)

    @mock.patch("rally.cli.commands.task.envutils.get_global")
    def test_detailed_no_task_id(self, mock_get_global):
        mock_get_global.side_effect = exceptions.InvalidArgumentsException
        self.assertRaises(exceptions.InvalidArgumentsException,
                          self.task.detailed, None)

    def test_detailed_wrong_id(self):
        test_uuid = "eb290c30-38d8-4c8f-bbcc-fc8f74b004ae"
        self.fake_api.task.get.side_effect = None
        self.task.detailed(self.fake_api, test_uuid)
        self.fake_api.task.get.assert_called_once_with(
            task_id=test_uuid, detailed=True)

    def _make_task(self, status=None, data=None):
        return {
            "status": status or consts.TaskStatus.FINISHED,
            "subtasks": [{"workloads": [{
                "full_duration": 1, "load_duration": 2,
                "created_at": "2017-09-27T07:22:55",
                "name": "Foo.bar", "description": "descr",
                "position": 2,
                "args": {"key1": "value1"},
                "runner_type": "rruunneerr",
                "runner": {"arg1": "args2"},
                "hooks": [],
                "sla": {"failure_rate": {"max": 0}},
                "sla_results": {"sla": [{"success": True}]},
                "contexts": {"users": {}},
                "data": data or []}]}]}

    @mock.patch("rally.cli.commands.task.json.dumps")
    def test_results(self, mock_json_dumps):
        task_id = "foo_task_id"

        task_obj = self._make_task(data=[{"atomic_actions": {"foo": 1.1}}])
        task_obj["subtasks"][0]["workloads"][0]["hooks"] = [{
            "config": {
                "action": ("foo", "arg"),
                "trigger": ("bar", "arg2")
            },
            "summary": {"success": 1}}
        ]

        def fix_r(workload):
            cfg = workload["runner"]
            cfg["type"] = workload["runner_type"]
            return cfg

        result = map(
            lambda x: {
                "key": {"kw": {"sla": x["sla"],
                               "args": x["args"],
                               "context": x["contexts"],
                               "runner": fix_r(x),
                               "hooks": [{"description": "",
                                          "name": "foo",
                                          "args": "arg",
                                          "trigger": {"name": "bar",
                                                      "args": "arg2"}}]},
                        "pos": x["position"],
                        "name": x["name"],
                        "description": x["description"]},
                "result": x["data"],
                "load_duration": x["load_duration"],
                "full_duration": x["full_duration"],
                "created_at": dt.datetime.strptime(
                    x["created_at"], "%Y-%m-%dT%H:%M:%S").strftime(
                    "%Y-%d-%mT%H:%M:%S"),
                "hooks": [{
                    "config": {"description": "",
                               "name": "foo",
                               "args": "arg",
                               "trigger": {"name": "bar", "args": "arg2"}},
                    "summary": {"success": 1}}],
                "sla": x["sla_results"]["sla"]},
            task_obj["subtasks"][0]["workloads"])

        self.fake_api.task.get.return_value = task_obj

        self.assertIsNone(self.task.results(self.fake_api, task_id))
        self.assertEqual(1, mock_json_dumps.call_count)
        self.assertEqual(1, len(mock_json_dumps.call_args[0]))
        self.assertSequenceEqual(result, mock_json_dumps.call_args[0][0])
        self.assertEqual({"sort_keys": False, "indent": 4},
                         mock_json_dumps.call_args[1])
        self.fake_api.task.get.assert_called_once_with(
            task_id=task_id, detailed=True)

    @mock.patch("rally.cli.commands.task.sys.stdout")
    def test_results_no_data(self, mock_stdout):
        task_id = "foo"
        self.fake_api.task.get.return_value = self._make_task(
            status=consts.TaskStatus.CRASHED)

        self.assertEqual(1, self.task.results(self.fake_api, task_id))

        self.fake_api.task.get.assert_called_once_with(
            task_id=task_id, detailed=True)

        expected_out = ("Task status is %s. Results "
                        "available when it is one of %s.") % (
            consts.TaskStatus.CRASHED,
            ", ".join((consts.TaskStatus.FINISHED,
                       consts.TaskStatus.ABORTED)))
        mock_stdout.write.assert_has_calls([mock.call(expected_out)])

    def test_trends(self):
        self.task.export = mock.MagicMock()
        self.task.trends(self.fake_api,
                         tasks=["uuid"],
                         out="output.html")
        self.task.export.assert_called_once_with(
            self.fake_api, tasks=["uuid"], output_type="trends-html",
            output_dest="output.html", open_it=False)

    def test_trends_no_tasks_given(self):
        ret = self.task.trends(self.fake_api, tasks=[],
                               out="output.html", out_format="html")
        self.assertEqual(1, ret)

    def test_report(self):
        self.task.export = mock.MagicMock()
        self.task.report(self.fake_api, tasks="uuid",
                         out="out", open_it=False, out_format="junit-xml")
        self.task.export.assert_called_once_with(
            self.fake_api, tasks="uuid", output_type="junit-xml",
            output_dest="out", open_it=False, deployment=None
        )

    @mock.patch("rally.cli.commands.task.cliutils.print_list")
    @mock.patch("rally.cli.commands.task.envutils.get_global",
                return_value="123456789")
    def test_list(self, mock_get_global, mock_print_list):
        self.fake_api.task.list.return_value = [
            {"uuid": "a",
             "created_at": "2007-01-01T00:00:01",
             "updated_at": "2007-01-01T00:00:03",
             "status": consts.TaskStatus.RUNNING,
             "tags": ["d"],
             "deployment_name": "some_name"}]
        self.task.list(self.fake_api, status="running")
        self.fake_api.task.list.assert_called_once_with(
            deployment=mock_get_global.return_value,
            status=consts.TaskStatus.RUNNING)

        headers = ["UUID", "Deployment name", "Created at", "Load duration",
                   "Status", "Tag(s)"]

        mock_print_list.assert_called_once_with(
            self.fake_api.task.list.return_value, fields=headers,
            normalize_field_names=True,
            sortby_index=headers.index("Created at"),
            formatters=mock.ANY)

    @mock.patch("rally.cli.commands.task.envutils.get_global",
                return_value="123456789")
    def test_list_uuids_only(self, mock_get_global):
        self.fake_api.task.list.return_value = [
            {"uuid": "a",
             "created_at": "2007-01-01T00:00:01",
             "updated_at": "2007-01-01T00:00:03",
             "status": consts.TaskStatus.RUNNING,
             "tags": ["d"],
             "deployment_name": "some_name"}]
        out = io.StringIO()
        with mock.patch.object(sys, "stdout", new=out):
            self.task.list(self.fake_api, status="running", uuids_only=True)
            self.assertEqual("a\n", out.getvalue())
        self.fake_api.task.list.assert_called_once_with(
            deployment=mock_get_global.return_value,
            status=consts.TaskStatus.RUNNING)

    def test_list_wrong_status(self):
        self.assertEqual(1, self.task.list(self.fake_api, deployment="fake",
                                           status="wrong non existing status"))

    def test_list_no_results(self):
        self.fake_api.task.list.return_value = []
        self.assertIsNone(self.task.list(self.fake_api, deployment="fake",
                                         all_deployments=True))
        self.fake_api.task.list.assert_called_once_with()
        self.fake_api.task.list.reset_mock()

        self.assertIsNone(self.task.list(self.fake_api, deployment="d",
                                         status=consts.TaskStatus.RUNNING))
        self.fake_api.task.list.assert_called_once_with(
            deployment="d", status=consts.TaskStatus.RUNNING)

    @mock.patch("rally.cli.commands.task.envutils.get_global",
                return_value="123456789")
    def test_list_output(self, mock_get_global):
        self.fake_api.task.list.return_value = [
            {"uuid": "UUID-1",
             "created_at": "2007-01-01T00:00:01",
             "task_duration": 0.0000009,
             "status": consts.TaskStatus.INIT,
             "tags": [],
             "deployment_name": "some_name"},
            {"uuid": "UUID-2",
             "created_at": "2007-02-01T00:00:01",
             "task_duration": 123.99992,
             "status": consts.TaskStatus.FINISHED,
             "tags": ["tag-1", "tag-2"],
             "deployment_name": "some_name"}]

        # It is a hard task to mock default value of function argument, so we
        # need to apply this workaround
        original_print_list = cliutils.print_list
        print_list_calls = []

        def print_list(*args, **kwargs):
            print_list_calls.append(io.StringIO())
            kwargs["out"] = print_list_calls[-1]
            original_print_list(*args, **kwargs)

        with mock.patch.object(task.cliutils, "print_list",
                               new=print_list):
            self.task.list(self.fake_api, status="running")

        self.assertEqual(1, len(print_list_calls))

        self.assertEqual(
            "+--------+-----------------+---------------------"
            "+---------------+----------+------------------+\n"
            "| UUID   | Deployment name | Created at          "
            "| Load duration | Status   | Tag(s)           |\n"
            "+--------+-----------------+---------------------"
            "+---------------+----------+------------------+\n"
            "| UUID-1 | some_name       | 2007-01-01 00:00:01 "
            "| 0.0           | init     |                  |\n"
            "| UUID-2 | some_name       | 2007-02-01 00:00:01 "
            "| 124.0         | finished | 'tag-1', 'tag-2' |\n"
            "+--------+-----------------+---------------------"
            "+---------------+----------+------------------+\n",
            print_list_calls[0].getvalue())

    def test_delete(self):
        task_uuid = "8dcb9c5e-d60b-4022-8975-b5987c7833f7"
        force = False
        self.task.delete(self.fake_api, task_uuid, force=force)
        self.fake_api.task.delete.assert_called_once_with(
            task_uuid=task_uuid, force=force)

    def test_delete_multiple_uuid(self):
        task_uuids = ["4bf35b06-5916-484f-9547-12dce94902b7",
                      "52cad69d-d3e4-47e1-b445-dec9c5858fe8",
                      "6a3cb11c-ac75-41e7-8ae7-935732bfb48f",
                      "018af931-0e5a-40d5-9d6f-b13f4a3a09fc"]
        force = False
        self.task.delete(self.fake_api, task_uuids, force=force)
        self.assertTrue(
            self.fake_api.task.delete.call_count == len(task_uuids))
        expected_calls = [mock.call(task_uuid=task_uuid,
                                    force=force) for task_uuid in task_uuids]
        self.assertTrue(self.fake_api.task.delete.mock_calls == expected_calls)

    @mock.patch("rally.cli.commands.task.cliutils.print_list")
    def test_sla_check(self, mock_print_list):
        task_obj = self._make_task()
        task_obj["subtasks"][0]["workloads"][0]["sla_results"]["sla"] = [
            {"benchmark": "KeystoneBasic.create_user",
             "criterion": "max_seconds_per_iteration",
             "pos": 0, "success": False, "detail": "Max foo, actually bar"}]

        self.fake_api.task.get.return_value = task_obj
        result = self.task.sla_check(self.fake_api, task_id="fake_task_id")
        self.assertEqual(1, result)
        self.fake_api.task.get.assert_called_with(
            task_id="fake_task_id", detailed=True)

        task_obj["subtasks"][0]["workloads"][0]["sla_results"]["sla"][0][
            "success"] = True

        result = self.task.sla_check(self.fake_api, task_id="fake_task_id",
                                     tojson=True)
        self.assertEqual(0, result)

    @mock.patch("rally.cli.commands.task.os.path.isfile", return_value=True)
    @mock.patch("rally.cli.commands.task.open",
                side_effect=mock.mock_open(read_data="{\"some\": \"json\"}"),
                create=True)
    def test_validate(self, mock_open, mock_os_path_isfile):
        self.fake_api.task.render_template = self.real_api.task.render_template

        self.task.validate(self.fake_api, "path_to_config.json", "fake_id")

        self.fake_api.task.validate.assert_called_once_with(
            deployment="fake_id", config={"some": "json"})

    @mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
                side_effect=task.FailedToLoadTask)
    def test_validate_failed_to_load_task(self, mock__load_and_validate_task):
        args = "args"
        args_file = "args_file"

        mock__load_and_validate_task.side_effect = KeyError("foo")

        self.assertRaises(KeyError, self.task.validate, self.real_api,
                          "path_to_task", "fake_deployment_id",
                          task_args=args, task_args_file=args_file)
        self.assertFalse(self.fake_api.task.validate.called)

        mock__load_and_validate_task.assert_called_once_with(
            self.real_api, "path_to_task", raw_args=args, args_file=args_file)

    @mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task")
    def test_validate_invalid(self, mock__load_and_validate_task):
        exc = exceptions.InvalidTaskException("foo")
        self.fake_api.task.validate.side_effect = exc
        self.assertRaises(exceptions.InvalidTaskException,
                          self.task.validate, self.fake_api, "path_to_task",
                          "deployment")
        self.fake_api.task.validate.assert_called_once_with(
            deployment="deployment",
            config=mock__load_and_validate_task.return_value)

    @mock.patch("rally.common.fileutils._rewrite_env_file")
    def test_use(self, mock__rewrite_env_file):
        task_id = "80422553-5774-44bd-98ac-38bd8c7a0feb"
        self.task.use(self.fake_api, task_id)
        mock__rewrite_env_file.assert_called_once_with(
            os.path.expanduser("~/.rally/globals"),
            ["RALLY_TASK=%s\n" % task_id])

    def test_use_not_found(self):
        task_id = "ddc3f8ba-082a-496d-b18f-72cdf5c10a14"
        exc = exceptions.DBRecordNotFound(criteria="uuid: %s" % task_id,
                                          table="tasks")
        self.fake_api.task.get.side_effect = exc
        self.assertRaises(exceptions.DBRecordNotFound, self.task.use,
                          self.fake_api, task_id)

    @mock.patch("rally.cli.commands.task.os.path")
    @mock.patch("rally.cli.commands.task.webbrowser.open_new_tab")
    @mock.patch("rally.cli.commands.task.open", create=True)
    @mock.patch("rally.cli.commands.task.print")
    def test_export(self, mock_print, mock_open, mock_open_new_tab,
                    mock_path):

        # file
        self.fake_api.task.export.return_value = {
            "files": {"output_dest": "content"}, "open": "output_dest"}
        mock_path.exists.side_effect = [False, True, False]
        mock_path.expanduser.return_value = "output_file"
        mock_path.realpath.return_value = "real_path"
        mock_fd = mock.mock_open()
        mock_open.side_effect = mock_fd
        self.task._load_task_results_file = mock.MagicMock(
            return_value=[{"task": "task_1"}, {"task": "task2"}])

        self.task.export(self.fake_api, tasks=["uuid", "file"],
                         output_type="json", output_dest="output_dest",
                         open_it=True)

        self.fake_api.task.export.assert_called_once_with(
            tasks=["uuid"] + self.task._load_task_results_file.return_value,
            output_type="json",
            output_dest="output_dest"
        )
        mock_open.assert_called_once_with("output_file", "w+")
        mock_fd.return_value.write.assert_called_once_with("content")

        # print
        self.fake_api.task.export.reset_mock()
        self.fake_api.task.export.return_value = {"print": "content"}
        self.task.export(self.fake_api, tasks="uuid", output_type="json")
        self.fake_api.task.export.assert_called_once_with(
            tasks=["uuid"],
            output_type="json", output_dest=None
        )
        mock_print.assert_called_once_with("content")

    @mock.patch("rally.cli.commands.task.sys.stdout")
    @ddt.data({"error_type": "test_no_trace_type",
               "error_message": "no_trace_error_message",
               "error_traceback": None,
               },
              {"error_type": "test_error_type",
               "error_message": "test_error_message",
               "error_traceback": "test\nerror\ntraceback",
               })
    @ddt.unpack
    def test_show_task_errors_no_trace(self, mock_stdout,
                                       error_type, error_message,
                                       error_traceback=None):
        test_uuid = "test_task_id"
        error_data = [error_type, error_message]
        if error_traceback:
            error_data.append(error_traceback)
        self.fake_api.task.get.return_value = {
            "id": "task",
            "uuid": test_uuid,
            "status": "finished",
            "pass_sla": True,
            "subtasks": [{"workloads": [{
                "name": "fake_name",
                "position": "fake_pos",
                "args": {}, "runner_type": "foo",
                "runner": {}, "contexts": {}, "sla": {},
                "hooks": {},
                "load_duration": 3.2,
                "full_duration": 3.5,
                "statistics": {
                    "durations": {
                        "atomics": [
                            {
                                "name": "foo",
                                "display_name": "foo (x2)",
                                "count_per_iteration": 2,
                                "children": [],
                                "data": {
                                    "min": 1,
                                    "median": 2,
                                    "90%ile": 1.5,
                                    "95%ile": 1.6,
                                    "max": 3,
                                    "avg": 1.4,
                                    "success": 3,
                                    "iteration_count": 3}},
                            {
                                "name": "bar",
                                "display_name": "bar",
                                "count_per_iteration": 1,
                                "children": [],
                                "data": {
                                    "min": 1.1,
                                    "median": 2.2,
                                    "90%ile": 1.6,
                                    "95%ile": 1.65,
                                    "max": 3,
                                    "avg": 1.5,
                                    "success": 3,
                                    "iteration_count": 3}
                            }],
                        "total": {
                            "name": "total",
                            "display_name": "bar",
                            "count_per_iteration": 1,
                            "children": [],
                            "data": {
                                "min": 1,
                                "median": 2.1,
                                "90%ile": 1.55,
                                "95%ile": 1.62,
                                "max": 3,
                                "avg": 1.45,
                                "success": 6,
                                "iteration_count": 6}}}
                },
                "total_iteration_count": 1,
                "total_iteration_failed": 1,
                "data": [
                    {"duration": 0.9,
                     "idle_duration": 0.1,
                     "output": {"additive": [], "complete": []},
                     "atomic_actions": {"foo": 0.6, "bar": 0.7},
                     "error": error_data
                     },
                ]},
            ]}],
            "validation_result": json.dumps([error_type, error_message,
                                             error_traceback])
        }
        self.task.detailed(self.fake_api, test_uuid)
        self.fake_api.task.get.assert_called_once_with(
            task_id=test_uuid, detailed=True)
        mock_stdout.write.assert_has_calls([
            mock.call(error_traceback or "No traceback available.")
        ], any_order=False)

    @mock.patch("rally.cli.commands.task.open", create=True)
    @mock.patch("rally.cli.commands.task.json.loads")
    @mock.patch("rally.cli.commands.task.jsonschema.validate",
                return_value=None)
    def test__load_task_results_file(self, mock_validate, mock_loads,
                                     mock_open):
        task_file = "/tmp/task.json"
        workload = {
            "uuid": "n/a",
            "full_duration": 2, "load_duration": 1,
            "created_at": "2017-07-01T07:03:01",
            "updated_at": "2017-07-01T07:03:03",
            "total_iteration_count": 2,
            "failed_iteration_count": 1,
            "min_duration": 3,
            "max_duration": 5,
            "start_time": 1,
            "name": "Foo.bar", "description": "descr",
            "position": 2,
            "args": {"key1": "value1"},
            "runner_type": "constant",
            "runner": {"time": 3},
            "hooks": [{"config": {
                "description": "descr",
                "action": ("foo", {"arg1": "v1"}),
                "trigger": ("t", {"a2", "v2"})}}],
            "pass_sla": True,
            "sla": {"failure_rate": {"max": 0}},
            "sla_results": {"sla": [{"success": True}]},
            "contexts": {"users": {}},
            "contexts_results": [],
            "data": [{"timestamp": 1, "atomic_actions": {"foo": 1.0,
                                                         "bar": 1.0},
                      "duration": 5, "idle_duration": 0, "error": [{}]},
                     {"timestamp": 2, "atomic_actions": {"bar": 1.1},
                      "duration": 3, "idle_duration": 0, "error": []}],
            "statistics": {"durations": mock.ANY}
        }

        results = [{
            "hooks": [{
                "config": {
                    "name": "foo",
                    "args": {"arg1": "v1"},
                    "description": "descr",
                    "trigger": {"name": "t", "args": {"a2", "v2"}}}}],
            "key": {
                "name": workload["name"],
                "description": workload["description"],
                "pos": workload["position"],
                "kw": {
                    "args": workload["args"],
                    "runner": {"type": "constant", "time": 3},
                    "hooks": [{
                        "name": "foo",
                        "args": {"arg1": "v1"},
                        "description": "descr",
                        "trigger": {"name": "t", "args": {"a2", "v2"}}}],
                    "sla": workload["sla"],
                    "context": workload["contexts"]}},
            "sla": workload["sla_results"]["sla"],
            "result": workload["data"],
            "full_duration": workload["full_duration"],
            "load_duration": workload["load_duration"],
            "created_at": "2017-01-07T07:03:01"}
        ]
        mock_loads.return_value = results
        ret = self.task._load_task_results_file(self.fake_api, task_file)
        self.assertEqual([{
            "version": 2,
            "title": "Task loaded from a file.",
            "description": "Auto-ported from task format V1.",
            "uuid": "n/a",
            "env_uuid": "n/a",
            "env_name": "n/a",
            "status": "finished",
            "tags": [],
            "subtasks": [{
                "title": "A SubTask",
                "description": "",
                "workloads": [workload]}]}], ret)

    @mock.patch("rally.cli.commands.task.open", create=True)
    @mock.patch("rally.cli.commands.task.json.loads")
    @mock.patch("rally.cli.commands.task.jsonschema.validate")
    def test__load_task_new_results_file(self, mock_validate,
                                         mock_loads, mock_open):
        task_file = "/tmp/task.json"
        results = {
            "tasks": [{
                "env_uuid": "env-uuid-1",
                "env_name": "env-name-1",
                "subtasks": [{
                    "workloads": [{
                        "contexts": "contexts",
                        "scenario": {"Foo.bar": {}},
                        "runner": {"constant": {
                            "times": 100,
                            "concurrency": 5
                        }}
                    }]
                }]
            }]
        }

        mock_loads.return_value = results
        ret = self.task._load_task_results_file(self.fake_api, task_file)
        self.assertEqual([{
            "env_uuid": "env-uuid-1",
            "env_name": "env-name-1",
            "subtasks": [{
                "workloads": [{
                    "args": {},
                    "name": "Foo.bar",
                    "contexts": "contexts",
                    "contexts_results": [],
                    "runner_type": "constant",
                    "runner": {
                        "times": 100,
                        "concurrency": 5
                    }
                }]
            }]
        }], ret)

    @mock.patch("rally.cli.commands.task.open", create=True)
    @mock.patch("rally.cli.commands.task.json.loads")
    def test__load_task_results_file_wrong_format(self, mock_loads, mock_open):
        task_id = "/tmp/task.json"
        mock_loads.return_value = "results"
        self.assertRaises(task.FailedToLoadResults,
                          self.task._load_task_results_file,
                          api=self.real_api, task_id=task_id)

        mock_loads.return_value = ["results"]
        self.assertRaises(task.FailedToLoadResults,
                          self.task._load_task_results_file,
                          api=self.real_api, task_id=task_id)

    @mock.patch("rally.cli.commands.task.os.path")
    def test_import_results(self, mock_os_path):
        mock_os_path.exists.return_value = True
        mock_os_path.expanduser = lambda path: path
        self.task._load_task_results_file = mock.MagicMock(
            return_value=["results"]
        )

        self.task.import_results(self.fake_api,
                                 "deployment_uuid",
                                 "task_file", tags=["tag"])

        self.task._load_task_results_file.assert_called_once_with(
            self.fake_api, "task_file"
        )
        self.fake_api.task.import_results.assert_called_once_with(
            deployment="deployment_uuid", task_results="results",
            tags=["tag"])

        # not exist
        mock_os_path.exists.return_value = False
        self.assertEqual(
            1,
            self.task.import_results(self.fake_api,
                                     "deployment_uuid",
                                     "task_file", ["tag"])
        )
