"""Metaclass and class for validating instance APIs"""

import os

import pytest


class validator2test(type):
    """Wrap ``validator_*`` methods with test method testing instances

    * Find methods with names starting with 'validate_'
    * Create test method with same name
    * Test method iterates, running validate method over all obj, param pairs
    """

    def __new__(mcs, name, bases, dict):
        klass = type.__new__(mcs, name, bases, dict)

        def make_test(name, validator):
            def meth(self):
                for imaker, params in self.obj_params():
                    validator(self, imaker, params)

            meth.__name__ = 'test_' + name[len('validate_') :]
            meth.__doc__ = f'autogenerated test from {klass.__name__}.{name}'
            if hasattr(validator, 'pytestmark'):
                meth.pytestmark = validator.pytestmark
            return meth

        for name in dir(klass):
            if not name.startswith('validate_'):
                continue
            # Assume this is a validation method; make a test
            test_meth = make_test(name, getattr(klass, name))
            setattr(klass, test_meth.__name__, test_meth)
        return klass


class ValidateAPI(metaclass=validator2test):
    """A class to validate APIs

    Your job is twofold:

    * define an ``obj_params`` iterable, where the iterator returns (``obj``,
      ``params``) pairs.  ``obj`` is something that you want to validate against
      an API.  ``params`` is a mapping giving parameters for this object to test
      against.
    * define ``validate_xxx`` methods, that accept ``obj`` and
      ``params`` as arguments, and check ``obj`` against ``params``

    The metaclass finds each ``validate_xxx`` method and makes a new
    ``test_xxx`` method that calls ``validate_xxx`` for each (``obj``,
    ``params``) pair returned from ``obj_params``

    See :class:`TextValidateSomething` for an example
    """


class TestValidateSomething(ValidateAPI):
    """Example implementing an API validator test class"""

    def obj_params(self):
        """Iterator returning (obj, params) pairs

        ``obj`` is some instance for which we want to check the API.

        ``params`` is a mapping with parameters that you are going to check
        against ``obj``.  See the :meth:`validate_something` method for an
        example.
        """

        class C:
            def __init__(self, var):
                self.var = var

            def get_var(self):
                return self.var

        yield C(5), {'var': 5}
        yield C('easypeasy'), {'var': 'easypeasy'}

    def validate_something(self, obj, params):
        """Do some checks of the `obj` API against `params`

        The metaclass sets up a ``test_something`` function that runs these
        checks on each (
        """
        assert obj.var == params['var']
        assert obj.get_var() == params['var']


@pytest.mark.xfail(
    os.getenv('PYTEST_XDIST_WORKER') is not None,
    reason='Execution in the same scope cannot be guaranteed',
)
class TestRunAllTests(ValidateAPI):
    """Class to test that each validator test gets run

    We check this in the module teardown function
    """

    run_tests = set()

    def obj_params(self):
        yield 1, 2

    def validate_first(self, obj, param):
        self.run_tests.add('first')

    def validate_second(self, obj, param):
        self.run_tests.add('second')

    @classmethod
    def teardown_class(cls):
        # Check that both validate_xxx tests got run
        assert cls.run_tests == {'first', 'second'}
