# pytest custom collection adapter for legacy pyyaml unit tests/data files; surfaces each
# legacy test case as a pyyaml item

import os
import pathlib
import pytest
import warnings

from test_appliance import find_test_filenames, DATA

try:
    from yaml import _yaml
    HAS_LIBYAML_EXT = True
    del _yaml
except ImportError:
    HAS_LIBYAML_EXT = False


_test_filenames = find_test_filenames(DATA)

# ignore all datafiles
collect_ignore_glob = ['data/*']


class PyYAMLItem(pytest.Item):
    def __init__(self, parent=None, config=None, session=None, nodeid=None, function=None, filenames=None, **kwargs):
        self._function = function
        self._fargs = filenames or []

        super().__init__(os.path.basename(filenames[0]) if filenames else parent.name, parent, config, session, nodeid)
        # this is gnarly since the type of fspath is private; fixed in pytest 7 to use pathlib on the `path` attr
        if filenames:  # pass the data file location as the test path
            self.fspath = parent.fspath.__class__(filenames[0])
            self.lineno = 1
        else:  # pass the function location in the code
            self.fspath = parent.fspath.__class__(function.__code__.co_filename)
            self.lineno = function.__code__.co_firstlineno

    def runtest(self):
        self._function(verbose=True, *self._fargs)

    def reportinfo(self):
        return self.fspath, self.lineno, ''


class PyYAMLCollector(pytest.Collector):
    def __init__(self, name, parent=None, function=None, **kwargs):
        self._function = function
        self.fspath = parent.fspath.__class__(function.__code__.co_filename)
        self.lineno = function.__code__.co_firstlineno

        # avoid fspath deprecation warnings on pytest < 7
        if hasattr(self, 'path') and 'fspath' in kwargs:
            del kwargs['fspath']

        super().__init__(name=name, parent=parent, **kwargs)

    def collect(self):
        items = []

        unittest = getattr(self._function, 'unittest', None)

        if unittest is True:  # no filenames
            items.append(PyYAMLItem.from_parent(parent=self, function=self._function, filenames=None))
        else:
            for base, exts in _test_filenames:
                filenames = []
                for ext in unittest:
                    if ext not in exts:
                        break
                    filenames.append(os.path.join(DATA, base + ext))
                else:
                    skip_exts = getattr(self._function, 'skip', [])
                    for skip_ext in skip_exts:
                        if skip_ext in exts:
                            break
                    else:
                        items.append(PyYAMLItem.from_parent(parent=self, function=self._function, filenames=filenames))

        return items or None

    def reportinfo(self):
        return self.fspath, self.lineno, ''

    @classmethod
    def from_parent(cls, parent, fspath, **kwargs):
        return super().from_parent(parent=parent, fspath=fspath, **kwargs)


@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_pycollect_makeitem(collector, name: str, obj: object):
    outcome = yield
    outcome.get_result()
    if not callable(obj):
        outcome.force_result(None)
        return
    unittest = getattr(obj, 'unittest', None)

    if not unittest:
        outcome.force_result(None)
        return

    if unittest is True:  # no file list to run against, just return a test item instead of a collector
        outcome.force_result(PyYAMLItem.from_parent(name=name, parent=collector, fspath=collector.fspath, function=obj))
        return

    # there's a file list; return a collector to create individual items for each
    outcome.force_result(PyYAMLCollector.from_parent(name=name, parent=collector, fspath=collector.fspath, function=obj))
    return


def pytest_collection_modifyitems(session, config, items):
    pass


def pytest_ignore_collect(collection_path: pathlib.Path):
    basename = collection_path.name
    # ignore all Python files in this subtree for normal pytest collection
    if basename not in ['test_yaml.py', 'test_yaml_ext.py']:
        return True

    # ignore extension tests (depending on config)
    if basename == 'test_yaml_ext.py':
        require_libyaml = os.environ.get('PYYAML_FORCE_LIBYAML', None)
        if require_libyaml == '1' and not HAS_LIBYAML_EXT:
            raise RuntimeError('PYYAML_FORCE_LIBYAML envvar is set, but libyaml extension is not available')
        if require_libyaml == '0':
            return True
        if not HAS_LIBYAML_EXT:
            warnings.warn('libyaml extension is not available, skipping libyaml tests')
            return True

