# This file is part of Build Your Own Tests
#
# Copyright 2018 Vincent Ladeuil
# Copyright 2014, 2015 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License version 3, as published by the
# Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see <http://www.gnu.org/licenses/>.
"""A collection of commonly used 'Features' to optionally run tests."""

from distutils import spawn
import unittest


class Feature(object):
    """An operating system Feature."""

    def __init__(self):
        self._available = None

    def available(self):
        """Is the feature available?

        :return: True if the feature is available.
        """
        if self._available is None:
            self._available = self._probe()
        return self._available

    def _probe(self):
        """Implement this method in concrete features.

        :return: True if the feature is available.
        """
        raise NotImplementedError

    def feature_name(self):
        return self.__class__.__name__

    def __repr__(self):
        return self.feature_name()

    def __str__(self):
        return self.feature_name()


class ExecutableFeature(Feature):
    """Feature testing whether an executable of a given name is on the PATH."""

    def __init__(self, name):
        super().__init__()
        self.name = name
        self._path = None

    @property
    def path(self):
        # This is a property, so accessing path ensures _probe was called
        self.available()
        return self._path

    def _probe(self):
        self._path = spawn.find_executable(self.name)
        return self._path is not None

    def feature_name(self):
        return '{} executable'.format(self.name)


class PlatformFeature(Feature):
    """Feature testing the current platform."""

    def __init__(self):
        super(PlatformFeature, self).__init__()
        # self.distro structure may vary between distros, the daughter classes
        # handle the differences.
        try:
            # Removed in python3.8 according to
            # https://pypi.org/project/distro/
            # FIXME: We don't support python 3.8 anyway ? -- vila 2024-07-04
            import platform
            self.distro = platform.linux_distribution()
        except AttributeError:
            import distro
            self.distro = distro.linux_distribution()


class UbuntuPlatformFeature(PlatformFeature):
    """Feature testing the current ubuntu release where the tests are run."""

    def __init__(self, expected_id):
        super(UbuntuPlatformFeature, self).__init__()
        self.expected_id = expected_id
        self._name, self._version, self.actual_id = self.distro

    def _force_release(self, name, version, actual_id):
        """For tests: force values ignoring actual platform."""
        self._name = name
        self._version = version
        self.actual_id = actual_id

    def feature_name(self):
        return self.expected_id

    def _probe(self):
        return (self._name == 'Ubuntu' and self.expected_id == self.actual_id)

    @property
    def version(self):
        """The Ubuntu version (yy.mm) string.

        This property is available even when we run on a different version,
        so we *don't* call _probe().
        """
        return self._version

xenial = UbuntuPlatformFeature('xenial')
artful = UbuntuPlatformFeature('artful')
bionic = UbuntuPlatformFeature('bionic')
# Tested releases. The order and the names is what matters.
ubuntu_releases = ('xenial', 'artful', 'bionic')


class AtLeastUbuntu(UbuntuPlatformFeature):

    def _probe(self):
        try:
            expected = ubuntu_releases.index(self.expected_id)
            actual = ubuntu_releases.index(self.actual_id)
            return (self._name == 'Ubuntu' and expected <= actual)
        except ValueError:
            return False


at_least_xenial = AtLeastUbuntu('xenial')
at_least_bionic = AtLeastUbuntu('bionic')


def requires(feature):
    """A decorator to skip a test if a feature is not available.

    If the feature is not available, a test case or a test suite using this
    decorator will be skipped.
    """
    if not feature.available():
        reason = '{} is not available'.format(feature.feature_name())
        return unittest.skip(reason)

    # If the feature is available, there is no need to decorate
    return lambda obj: obj


def test_requires(test, feature):
    """Skip a test if a feature is not available.

    This function can be used in a setUp or test method.
    """
    if not feature.available():
        reason = '{} is not available'.format(feature.feature_name())
        test.skipTest(reason)
    return feature  # For convenience
