# -*- coding: utf-8 -*-
# Copyright 2020 Axis Communications AB.
#
# For a full list of individual contributors, please see the commit history.
#
# 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.
"""Main executable module."""

import argparse
import sys
import os
import logging
import time
import json

import requests
from halo import Halo
from packageurl import PackageURL
from etos_lib.etos import ETOS
from etos_lib.lib.debug import Debug

from etos_client import __version__
from etos_client.client import ETOSClient
from etos_client.lib import ETOSTestResultHandler, ETOSLogHandler

_logger = logging.getLogger(__name__)


def environ_or_required(key):
    """Get key from environment or make it a required input.

    If a key is available as an environment variable, use it as default.
    Else require the argument to be set.

    Note:
        It is still possible to override the value with input even if the
        environment variable is set.
    """
    if os.getenv(key):
        return {"default": os.getenv(key)}
    return {"required": True}


def parse_args(args):
    """Parse command line parameters.

    Args:
      args ([str]): command line parameters as list of strings

    Returns:
      :obj:`argparse.Namespace`: command line parameters namespace
    """
    parser = argparse.ArgumentParser(
        description="Client for executing test automation suites in ETOS"
    )
    parser.add_argument(
        "cluster",
        help="Cluster is the URL to the ETOS API.",
    )
    parser.add_argument(
        "-i",
        "--identity",
        help="Artifact created identity purl to execute test suite on.",
        **environ_or_required("IDENTITY")
    )
    parser.add_argument(
        "-s",
        "--test-suite",
        help="Test suite execute. Either URL or name.",
        **environ_or_required("TEST_SUITE")
    )

    parser.add_argument(
        "--no-tty", help="Disable features requiring a tty.", action="store_true"
    )
    parser.add_argument(
        "-w",
        "--workspace",
        default=os.getenv("WORKSPACE", os.getcwd()),
        help="Which workspace to do all the work in.",
    )
    parser.add_argument(
        "-a",
        "--artifact-dir",
        default="artifacts",
        help="Where test artifacts should be stored. Relative to workspace.",
    )
    parser.add_argument(
        "-r",
        "--report-dir",
        default="reports",
        help="Where test reports should be stored. Relative to workspace.",
    )
    parser.add_argument(
        "-d",
        "--download-reports",
        default=None,
        help="Should we download reports. Can be 'yes', 'y', 'no', 'n'.",
    )

    parser.add_argument(
        "--iut-provider", default="default", help="Which IUT provider to use."
    )
    parser.add_argument(
        "--execution-space-provider",
        default="default",
        help="Which execution space provider to use.",
    )
    parser.add_argument(
        "--log-area-provider", default="default", help="Which log area provider to use."
    )
    parser.add_argument(
        "--dataset",
        default="{}",
        help=(
            "Additional dataset information to the environment provider. "
            "Check with your provider which information can be supplied."
        ),
    )

    parser.add_argument(
        "--version",
        action="version",
        version="etos_client {ver}".format(ver=__version__),
    )
    parser.add_argument(
        "-v",
        "--verbose",
        dest="loglevel",
        help="set loglevel to INFO",
        action="store_const",
        default=logging.WARNING,
        const=logging.INFO,
    )
    parser.add_argument(
        "-vv",
        "--very-verbose",
        dest="loglevel",
        help="set loglevel to DEBUG",
        action="store_const",
        default=logging.WARNING,
        const=logging.DEBUG,
    )
    return parser.parse_args(args)


def setup_logging(loglevel):
    """Set up basic logging.

    Args:
      loglevel (int): minimum loglevel for emitting messages
    """
    logformat = "[%(asctime)s] %(levelname)s:%(message)s"
    logging.basicConfig(
        level=loglevel, stream=sys.stdout, format=logformat, datefmt="%Y-%m-%d %H:%M:%S"
    )


def check_etos_connectivity(url):
    """Check the connection to ETOS.

    :param url: URL to test connection to.
    :type url: str
    """
    try:
        response = requests.get(url, timeout=5)
        response.raise_for_status()
    except Exception as exception:  # pylint:disable=broad-except
        raise Exception(
            "Unable to connect to ETOS. Please check your network connection."
        ) from exception


class Printer:
    """Generic printing class matching the interface of halo."""

    __text = None
    timeout = 60
    timer = None

    def __init__(self, text, **_):
        """Initialize the printer class.

        :param text: Initial text.
        :type text: str
        """
        self.timer = time.time() + self.timeout
        self.text = text

    def __enter__(self):
        """Enter printer context."""
        return self

    @property
    def text(self):
        """Printer text."""
        return self.__text

    @text.setter
    def text(self, text):
        """Set text for this object.

        Don't reprint unless the text is new or a timeout has been reached.
        This is to disable unecessary text spam into the console log.

        :param text: Text to print.
        :type text: str
        """
        timeout_reached = time.time() > self.timer
        if text != self.__text or timeout_reached:
            self.timer = time.time() + self.timeout
            self.info(text)
        self.__text = text

    @staticmethod
    def info(text):
        """Print text to stdout.

        :param text: Text to print.
        :type text: str
        """
        _logger.info(text)

    @staticmethod
    def fail(text):
        """Print text to stdout.

        :param text: Text to print.
        :type text: str
        """
        _logger.error(text)

    @staticmethod
    def warn(text):
        """Print text to stdout.

        :param text: Text to print.
        :type text: str
        """
        _logger.warning(text)

    succeed = info

    def start(self, text=None):
        """Set starting text."""
        if text is not None:
            self.text = text

    def __exit__(self, *_, **__):
        """Exit printer context."""


def generate_spinner(no_tty):
    """If no tty, return a generic 'printer' class. Else return halo.

    :param no_tty: Whether the executor has an interactive tty or not.
    :type no_tty: bool
    :return: Spinner text item.
    :rtype: :obj:`Spinner`
    """
    if no_tty:
        return Printer
    return Halo


def main(args):
    """Entry point allowing external calls.

    Args:
      args ([str]): command line parameter list
    """
    args = parse_args(args)
    etos = ETOS("ETOS Client", os.getenv("HOSTNAME"), "ETOS Client")

    setup_logging(args.loglevel)
    info = generate_spinner(args.no_tty)
    identity = PackageURL.from_string(args.identity)

    for key, value in args._get_kwargs():  # pylint:disable=protected-access
        etos.config.set(key, value)
    etos.config.set("identity", identity)
    etos.config.set("dataset", json.loads(args.dataset))

    with info(text="Checking connectivity to ETOS", spinner="dots") as spinner:
        spinner.info("Running in cluster: '{}'".format(args.cluster))
        spinner.info("Configuration:")
        spinner.info("{}".format(etos.config.config))
        try:
            check_etos_connectivity(f"{args.cluster}/selftest/ping")
        except Exception as exception:  # pylint:disable=broad-except
            spinner.fail(str(exception))
            sys.exit(1)
        spinner.start()
        spinner.succeed("Connection successful.")
        spinner.succeed("Ready to launch ETOS.")

        # Start execution
        etos_client = ETOSClient(etos, args.cluster)
        spinner.start("Triggering ETOS.")
        success = etos_client.start(spinner)
        if not success:
            # Unix  : 0 == Success, 1 == Fail
            # Python: 1 == True   , 0 == False
            sys.exit(not success)
        spinner.info("Suite ID: {}".format(etos_client.test_suite_id))
        etos.config.set("suite_id", etos_client.test_suite_id)
        os.environ["ETOS_GRAPHQL_SERVER"] = etos_client.event_repository
        spinner.info("Event repository: '{}'".format(etos.debug.graphql_server))

        # Wait for test results
        test_result_handler = ETOSTestResultHandler(etos)
        spinner.start("Waiting for ETOS.")
        success, results = test_result_handler.wait_for_test_suite_finished(spinner)
        if not success:
            spinner.fail(results)
        else:
            spinner.succeed(results)

        # Download reports
        if args.download_reports is None:
            answer = input(
                "Do you want to download all logs for this test execution? (y/n): "
            )
            while answer.lower() not in ("y", "yes", "no", "n"):
                print("Please answer 'yes' or 'no'")
                answer = input()
        else:
            answer = args.download_reports
        if answer.lower() in ("y", "yes"):
            log_handler = ETOSLogHandler(etos, test_result_handler.events)
            spinner.start("Downloading test logs.")
            logs_downloaded_successfully = log_handler.download_logs(spinner)
            if not logs_downloaded_successfully:
                sys.exit("ETOS logs did not download successfully.")


def run():
    """Entry point for console_scripts."""
    main(sys.argv[1:])


if __name__ == "__main__":
    run()
