# 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 os
import re
import shutil
import subprocess

import yaml

from rally.common.io import subunit_v2
from rally import exceptions
from rally.plugins.verification import testr
from rally.verification import manager
from rally.verification import utils

import rally_openstack
from rally_openstack.verification.tempest import config
from rally_openstack.verification.tempest import consts


AVAILABLE_SETS = (list(consts.TempestTestSets)
                  + list(consts.TempestApiTestSets)
                  + list(consts.TempestScenarioTestSets))


@manager.configure(name="tempest", platform="openstack",
                   default_repo="https://opendev.org/openstack/tempest",
                   context={"tempest": {}, "testr": {}})
class TempestManager(testr.TestrLauncher):
    """Tempest verifier.

    **Description**:

    Quote from official documentation:

      This is a set of integration tests to be run against a live OpenStack
      cluster. Tempest has batteries of tests for OpenStack API validation,
      Scenarios, and other specific tests useful in validating an OpenStack
      deployment.

    Rally supports features listed below:

    * *cloning Tempest*: repository and version can be specified
    * *installation*: system-wide with checking existence of required
      packages or in virtual environment
    * *configuration*: options are discovered via OpenStack API, but you can
      override them if you need
    * *running*: pre-creating all required resources(i.e images, tenants,
      etc), prepare arguments, launching Tempest, live-progress output
    * *results*: all verifications are stored in db, you can built reports,
      compare verification at whatever you want time.

    Appeared in Rally 0.8.0 *(actually, it appeared long time ago with first
    revision of Verification Component, but 0.8.0 is mentioned since it is
    first release after Verification Component redesign)*
    """

    RUN_ARGS = {"set": "Name of predefined set of tests. Known names: %s"
                       % ", ".join(AVAILABLE_SETS)}

    @property
    def run_environ(self):
        env = super(TempestManager, self).run_environ
        env["TEMPEST_CONFIG_DIR"] = os.path.dirname(self.configfile)
        env["TEMPEST_CONFIG"] = os.path.basename(self.configfile)
        # TODO(andreykurilin): move it to Testr base class
        env["OS_TEST_PATH"] = os.path.join(self.repo_dir,
                                           "tempest/test_discover")
        return env

    @property
    def configfile(self):
        return os.path.join(self.home_dir, "tempest.conf")

    def validate_args(self, args):
        """Validate given arguments."""
        super(TempestManager, self).validate_args(args)

        if args.get("pattern"):
            pattern = args["pattern"].split("=", 1)
            if len(pattern) == 1:
                pass  # it is just a regex
            elif pattern[0] == "set":
                if pattern[1] not in AVAILABLE_SETS:
                    raise exceptions.ValidationError(
                        "Test set '%s' not found in available "
                        "Tempest test sets. Available sets are '%s'."
                        % (pattern[1], "', '".join(AVAILABLE_SETS)))
            else:
                raise exceptions.ValidationError(
                    "'pattern' argument should be a regexp or set name "
                    "(format: 'tempest.api.identity.v3', 'set=smoke').")

    def configure(self, extra_options=None):
        """Configure Tempest."""
        utils.create_dir(self.home_dir)
        tcm = config.TempestConfigfileManager(self.verifier.env)
        return tcm.create(self.configfile, extra_options)

    def is_configured(self):
        """Check whether Tempest is configured or not."""
        return os.path.exists(self.configfile)

    def get_configuration(self):
        """Get Tempest configuration."""
        with open(self.configfile) as f:
            return f.read()

    def extend_configuration(self, extra_options):
        """Extend Tempest configuration with extra options."""
        return utils.extend_configfile(extra_options, self.configfile)

    def override_configuration(self, new_configuration):
        """Override Tempest configuration by new configuration."""
        with open(self.configfile, "w") as f:
            f.write(new_configuration)

    def install_extension(self, source, version=None, extra_settings=None):
        """Install a Tempest plugin."""
        if extra_settings:
            raise NotImplementedError(
                "'%s' verifiers don't support extra installation settings "
                "for extensions." % self.get_name())
        version = version or "master"
        egg = re.sub(r"\.git$", "", os.path.basename(source.strip("/")))
        full_source = "git+{0}@{1}#egg={2}".format(source, version, egg)
        # NOTE(ylobankov): Use 'develop mode' installation to provide an
        #                  ability to advanced users to change tests or
        #                  develop new ones in verifier repo on the fly.
        cmd = ["pip", "install",
               "--src", os.path.join(self.base_dir, "extensions"),
               "-e", full_source]
        if self.verifier.system_wide:
            cmd.insert(2, "--no-deps")
        utils.check_output(cmd, cwd=self.base_dir, env=self.environ)

        # Very often Tempest plugins are inside projects and requirements
        # for plugins are listed in the test-requirements.txt file.
        test_reqs_path = os.path.join(self.base_dir, "extensions",
                                      egg, "test-requirements.txt")
        if os.path.exists(test_reqs_path):
            if not self.verifier.system_wide:
                utils.check_output(["pip", "install", "-r", test_reqs_path],
                                   cwd=self.base_dir, env=self.environ)
            else:
                self.check_system_wide(reqs_file_path=test_reqs_path)

    def list_extensions(self):
        """List all installed Tempest plugins."""
        # TODO(andreykurilin): find a better way to list tempest plugins
        cmd = ("from tempest.test_discover import plugins; "
               "plugins_manager = plugins.TempestTestPluginManager(); "
               "plugins_map = plugins_manager.get_plugin_load_tests_tuple(); "
               "plugins_list = ["
               "    {'name': p.name, "
               "     'entry_point': p.entry_point_target, "
               "     'location': plugins_map[p.name][1]} "
               "    for p in plugins_manager.ext_plugins.extensions]; "
               "print(plugins_list)")
        try:
            output = utils.check_output(["python", "-c", cmd],
                                        cwd=self.base_dir, env=self.environ,
                                        debug_output=False).strip()
        except subprocess.CalledProcessError:
            raise exceptions.RallyException(
                "Cannot list installed Tempest plugins for verifier %s." %
                self.verifier)

        return yaml.safe_load(output)

    def uninstall_extension(self, name):
        """Uninstall a Tempest plugin."""
        for ext in self.list_extensions():
            if ext["name"] == name and os.path.exists(ext["location"]):
                shutil.rmtree(ext["location"])
                break
        else:
            raise exceptions.RallyException(
                "There is no Tempest plugin with name '%s'. "
                "Are you sure that it was installed?" % name)

    def list_tests(self, pattern=""):
        """List all Tempest tests."""
        if pattern:
            pattern = self._transform_pattern(pattern)
        return super(TempestManager, self).list_tests(pattern)

    def prepare_run_args(self, run_args):
        """Prepare 'run_args' for testr context."""
        if run_args.get("pattern"):
            run_args["pattern"] = self._transform_pattern(run_args["pattern"])
        return run_args

    @staticmethod
    def _transform_pattern(pattern):
        """Transform pattern into Tempest-specific pattern."""
        parsed_pattern = pattern.split("=", 1)
        if len(parsed_pattern) == 2:
            if parsed_pattern[0] == "set":
                if parsed_pattern[1] in consts.TempestTestSets:
                    return "smoke" if parsed_pattern[1] == "smoke" else ""
                elif parsed_pattern[1] in consts.TempestApiTestSets:
                    return "tempest.api.%s" % parsed_pattern[1]
                else:
                    return "tempest.%s" % parsed_pattern[1]

        return pattern  # it is just a regex

    def run(self, context):
        """Run tests."""

        if rally_openstack.__rally_version__ >= (3, 4, 1):
            return super().run(context)

        testr_cmd = context["testr_cmd"]
        stream = subprocess.Popen(testr_cmd, env=self.run_environ,
                                  cwd=self.repo_dir,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.STDOUT)
        xfail_list = context.get("xfail_list")
        skip_list = context.get("skip_list")

        class SubunitV2StreamResult(subunit_v2.SubunitV2StreamResult):
            def status(self, *args, **kwargs):
                if isinstance(kwargs.get("file_bytes"), memoryview):
                    kwargs["file_bytes"] = kwargs["file_bytes"].tobytes()
                return super().status(*args, **kwargs)

        results = SubunitV2StreamResult(
            xfail_list, skip_list, live=True, logger_name=self.verifier.name
        )
        subunit_v2.v2.ByteStreamToStreamResult(
            stream.stdout, "non-subunit").run(results)

        stream.wait()

        return results
