"""pytest configuration

Extends output capture as needed by pybind11: ignore constructors, optional unordered lines.
Adds docstring and exceptions message sanitizers: ignore Python 2 vs 3 differences.
"""

import pytest
import textwrap
import difflib
import re
import os
import sys
import contextlib

_unicode_marker = re.compile(r'u(\'[^\']*\')')
_long_marker    = re.compile(r'([0-9])L')
_hexadecimal    = re.compile(r'0x[0-9a-fA-F]+')


def _strip_and_dedent(s):
    """For triple-quote strings"""
    return textwrap.dedent(s.lstrip('\n').rstrip())


def _split_and_sort(s):
    """For output which does not require specific line order"""
    return sorted(_strip_and_dedent(s).splitlines())


def _make_explanation(a, b):
    """Explanation for a failed assert -- the a and b arguments are List[str]"""
    return ["--- actual / +++ expected"] + [line.strip('\n') for line in difflib.ndiff(a, b)]


class Output(object):
    """Basic output post-processing and comparison"""
    def __init__(self, string):
        self.string = string
        self.explanation = []

    def __str__(self):
        return self.string

    def __eq__(self, other):
        # Ignore constructor/destructor output which is prefixed with "###"
        a = [line for line in self.string.strip().splitlines() if not line.startswith("###")]
        b = _strip_and_dedent(other).splitlines()
        if a == b:
            return True
        else:
            self.explanation = _make_explanation(a, b)
            return False


class Unordered(Output):
    """Custom comparison for output without strict line ordering"""
    def __eq__(self, other):
        a = _split_and_sort(self.string)
        b = _split_and_sort(other)
        if a == b:
            return True
        else:
            self.explanation = _make_explanation(a, b)
            return False


class Capture(object):
    def __init__(self, capfd):
        self.capfd = capfd
        self.out = ""

    def _flush_stdout(self):
        sys.stdout.flush()
        os.fsync(sys.stdout.fileno())  # make sure C++ output is also read
        return self.capfd.readouterr()[0]

    def __enter__(self):
        self._flush_stdout()
        return self

    def __exit__(self, *_):
        self.out = self._flush_stdout()

    def __eq__(self, other):
        a = Output(self.out)
        b = other
        if a == b:
            return True
        else:
            self.explanation = a.explanation
            return False

    def __str__(self):
        return self.out

    def __contains__(self, item):
        return item in self.out

    @property
    def unordered(self):
        return Unordered(self.out)


@pytest.fixture
def capture(capfd):
    """Extended `capfd` with context manager and custom equality operators"""
    return Capture(capfd)


class SanitizedString(object):
    def __init__(self, sanitizer):
        self.sanitizer = sanitizer
        self.string = ""
        self.explanation = []

    def __call__(self, thing):
        self.string = self.sanitizer(thing)
        return self

    def __eq__(self, other):
        a = self.string
        b = _strip_and_dedent(other)
        if a == b:
            return True
        else:
            self.explanation = _make_explanation(a.splitlines(), b.splitlines())
            return False


def _sanitize_general(s):
    s = s.strip()
    s = s.replace("pybind11_tests.", "m.")
    s = s.replace("unicode", "str")
    s = _long_marker.sub(r"\1", s)
    s = _unicode_marker.sub(r"\1", s)
    return s


def _sanitize_docstring(thing):
    s = thing.__doc__
    s = _sanitize_general(s)
    return s


@pytest.fixture
def doc():
    """Sanitize docstrings and add custom failure explanation"""
    return SanitizedString(_sanitize_docstring)


def _sanitize_message(thing):
    s = str(thing)
    s = _sanitize_general(s)
    s = _hexadecimal.sub("0", s)
    return s


@pytest.fixture
def msg():
    """Sanitize messages and add custom failure explanation"""
    return SanitizedString(_sanitize_message)


# noinspection PyUnusedLocal
def pytest_assertrepr_compare(op, left, right):
    """Hook to insert custom failure explanation"""
    if hasattr(left, 'explanation'):
        return left.explanation


@contextlib.contextmanager
def suppress(exception):
    """Suppress the desired exception"""
    try:
        yield
    except exception:
        pass


def pytest_namespace():
    """Add import suppression and test requirements to `pytest` namespace"""
    try:
        import numpy as np
    except ImportError:
        np = None
    try:
        import scipy
    except ImportError:
        scipy = None
    try:
        from pybind11_tests import have_eigen
    except ImportError:
        have_eigen = False

    skipif = pytest.mark.skipif
    return {
        'suppress': suppress,
        'requires_numpy': skipif(not np, reason="numpy is not installed"),
        'requires_scipy': skipif(not np, reason="scipy is not installed"),
        'requires_eigen_and_numpy': skipif(not have_eigen or not np,
                                           reason="eigen and/or numpy are not installed"),
        'requires_eigen_and_scipy': skipif(not have_eigen or not scipy,
                                           reason="eigen and/or scipy are not installed"),
    }