# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
#   Copyright 2021-2022 Valory AG
#   Copyright 2018-2019 Fetch.AI Limited
#
#   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.
#
# ------------------------------------------------------------------------------

"""This module contains testing utilities."""
import logging
import re
import shutil
import subprocess  # nosec
import time
from abc import ABC, abstractmethod
from typing import Any, Generator, cast

import pytest


try:
    import docker
    from docker import DockerClient
    from docker.models.containers import Container
except ImportError:  # pragma: no cover
    # to avoid having to make docker a framework dependency
    docker = DockerClient = Container = Any


logger = logging.getLogger(__name__)


class DockerImage(ABC):
    """A class to wrap interaction with a Docker image."""

    MINIMUM_DOCKER_VERSION = (19, 0, 0)

    def __init__(self, client: DockerClient):
        """Initialize."""
        self._client = client

    def check_skip(self) -> None:
        """
        Check whether the test should be skipped.

        By default, nothing happens.
        """
        self._check_docker_binary_available()

    def _check_docker_binary_available(self) -> None:
        """Check the 'Docker' CLI tool is in the OS PATH."""
        result = shutil.which("docker")
        if result is None:
            pytest.skip("Docker not in the OS Path; skipping the test")
            return  # pragma: no cover

        proc_result = subprocess.run(  # pylint: disable=subprocess-run-check # nosec
            ["docker", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        if proc_result.returncode != 0:
            pytest.skip(
                f"'docker --version' failed with exit code {proc_result.returncode}"
            )
            return  # pragma: no cover

        match = re.search(
            r"Docker version ([0-9]+)\.([0-9]+)\.([0-9]+)",
            proc_result.stdout.decode("utf-8"),
        )
        if match is None:
            pytest.skip("cannot read version from the output of 'docker --version'")
            return  # pragma: no cover

        match = cast(re.Match, match)
        version = (int(match.group(1)), int(match.group(2)), int(match.group(3)))
        if version < self.MINIMUM_DOCKER_VERSION:
            pytest.skip(
                f"expected Docker version to be at least {'.'.join([str(item) for item in self.MINIMUM_DOCKER_VERSION])}, found {'.'.join([str(item) for item in version])}"
            )
            return  # pragma: no cover

    @property
    @abstractmethod
    def tag(self) -> str:
        """Return the tag of the image."""

    def stop_if_already_running(self) -> None:
        """Stop the running images with the same tag, if any."""

        client = docker.from_env()
        for container in client.containers.list():
            if self.tag in container.image.tags:
                logger.info(f"Stopping image {self.tag}...")
                container.stop()

    @abstractmethod
    def create(self) -> Container:
        """Instantiate the image in a container."""

    @abstractmethod
    def wait(self, max_attempts: int = 15, sleep_rate: float = 1.0) -> bool:
        """
        Wait until the image is running.

        :param max_attempts: max number of attempts.
        :param sleep_rate: the amount of time to sleep between different requests.
        :return: True if the wait was successful, False otherwise.
        """
        return True  # pragma: no cover


def launch_image(
    image: DockerImage, timeout: float = 2.0, max_attempts: int = 10
) -> Generator:
    """Launch image."""

    image.check_skip()
    image.stop_if_already_running()
    container = image.create()
    container.start()
    logger.info(f"Setting up image {image.tag}...")
    success = image.wait(max_attempts, timeout)
    if not success:
        container.stop()
        container.remove()
        pytest.fail(f"{image.tag} doesn't work. Exiting...")

    try:
        logger.info("Done!")
        time.sleep(timeout)
        yield
    finally:
        logger.info(f"Stopping the image {image.tag}...")
        container.stop()
        container.remove()
