from _pydev_runfiles import pydev_runfiles_xml_rpc
import pickle
import zlib
import base64
import os
import py
from pydevd_file_utils import _NormFile
import pytest
import sys
import time


#=========================================================================
# Load filters with tests we should skip
#=========================================================================
py_test_accept_filter = None


def _load_filters():
    global py_test_accept_filter
    if py_test_accept_filter is None:
        py_test_accept_filter = os.environ.get('PYDEV_PYTEST_SKIP')
        if py_test_accept_filter:
            py_test_accept_filter = pickle.loads(
                zlib.decompress(base64.b64decode(py_test_accept_filter)))
        else:
            py_test_accept_filter = {}


def is_in_xdist_node():
    main_pid = os.environ.get('PYDEV_MAIN_PID')
    if main_pid and main_pid != str(os.getpid()):
        return True
    return False


connected = False
def connect_to_server_for_communication_to_xml_rpc_on_xdist():
    global connected
    if connected:
        return
    connected = True
    if is_in_xdist_node():
        port = os.environ.get('PYDEV_PYTEST_SERVER')
        if not port:
            sys.stderr.write(
                'Error: no PYDEV_PYTEST_SERVER environment variable defined.\n')
        else:
            pydev_runfiles_xml_rpc.initialize_server(int(port), daemon=True)


PY2 = sys.version_info[0] <= 2
PY3 = not PY2

#=========================================================================
# Mocking to get clickable file representations
#=========================================================================

_mock_code = []
try:
    from py._code import code  # @UnresolvedImport
    _mock_code.append(code)
except ImportError:
    pass
try:
    from _pytest._code import code  # @UnresolvedImport
    _mock_code.append(code)
except ImportError:
    pass

def _MockFileRepresentation():
    for code in _mock_code:
        code.ReprFileLocation._original_toterminal = code.ReprFileLocation.toterminal

        def toterminal(self, tw):
            # filename and lineno output for each entry,
            # using an output format that most editors understand
            msg = self.message
            i = msg.find("\n")
            if i != -1:
                msg = msg[:i]

            path = os.path.abspath(self.path)

            if PY2:
                # Note: it usually is NOT unicode...
                if not isinstance(path, unicode):
                    path = path.decode(sys.getfilesystemencoding(), 'replace')

                # Note: it usually is unicode...
                if not isinstance(msg, unicode):
                    msg = msg.decode('utf-8', 'replace')
                unicode_line = unicode('File "%s", line %s\n%s') % (
                    path, self.lineno, msg)
                tw.line(unicode_line)
            else:
                tw.line('File "%s", line %s\n%s' % (path, self.lineno, msg))

        code.ReprFileLocation.toterminal = toterminal


def _UninstallMockFileRepresentation():
    for code in _mock_code:
        # @UndefinedVariable
        code.ReprFileLocation.toterminal = code.ReprFileLocation._original_toterminal

#=========================================================================
# End mocking to get clickable file representations
#=========================================================================

class State:
    start_time = time.time()
    buf_err = None
    buf_out = None


def start_redirect():
    if State.buf_out is not None:
        return
    from _pydevd_bundle import pydevd_io
    State.buf_err = pydevd_io.start_redirect(keep_original_redirection=True, std='stderr')
    State.buf_out = pydevd_io.start_redirect(keep_original_redirection=True, std='stdout')


def get_curr_output():
    return State.buf_out.getvalue(), State.buf_err.getvalue()


def pytest_configure():
    _MockFileRepresentation()


def pytest_unconfigure():
    _UninstallMockFileRepresentation()
    if is_in_xdist_node():
        return
    # Only report that it finished when on the main node (we don't want to report
    # the finish on each separate node).
    pydev_runfiles_xml_rpc.notifyTestRunFinished(
        'Finished in: %.2f secs.' % (time.time() - State.start_time,))


def pytest_collection_modifyitems(session, config, items):
    # A note: in xdist, this is not called on the main process, only in the
    # secondary nodes, so, we'll actually make the filter and report it multiple
    # times.
    connect_to_server_for_communication_to_xml_rpc_on_xdist()

    _load_filters()
    if not py_test_accept_filter:
        pydev_runfiles_xml_rpc.notifyTestsCollected(len(items))
        return  # Keep on going (nothing to filter)

    new_items = []
    for item in items:
        f = _NormFile(str(item.parent.fspath))
        name = item.name

        if f not in py_test_accept_filter:
            # print('Skip file: %s' % (f,))
            continue  # Skip the file

        accept_tests = py_test_accept_filter[f]

        if item.cls is not None:
            class_name = item.cls.__name__
        else:
            class_name = None
        for test in accept_tests:
            # This happens when parameterizing pytest tests.
            i = name.find('[')
            if i > 0:
                name = name[:i]
            if test == name:
                # Direct match of the test (just go on with the default
                # loading)
                new_items.append(item)
                break

            if class_name is not None:
                if test == class_name + '.' + name:
                    new_items.append(item)
                    break

                if class_name == test:
                    new_items.append(item)
                    break
        else:
            pass
            # print('Skip test: %s.%s. Accept: %s' % (class_name, name, accept_tests))

    # Modify the original list
    items[:] = new_items
    pydev_runfiles_xml_rpc.notifyTestsCollected(len(items))


from py.io import TerminalWriter

def _get_error_contents_from_report(report):
    if report.longrepr is not None:
        tw = TerminalWriter(stringio=True)
        tw.hasmarkup = False
        report.toterminal(tw)
        exc = tw.stringio.getvalue()
        s = exc.strip()
        if s:
            return s

    return ''

def pytest_collectreport(report):
    error_contents = _get_error_contents_from_report(report)
    if error_contents:
        report_test('fail', '<collect errors>', '<collect errors>', '', error_contents, 0.0)

def append_strings(s1, s2):
    if s1.__class__ == s2.__class__:
        return s1 + s2

    if sys.version_info[0] == 2:
        if not isinstance(s1, basestring):
            s1 = str(s1)

        if not isinstance(s2, basestring):
            s2 = str(s2)

        # Prefer bytes
        if isinstance(s1, unicode):
            s1 = s1.encode('utf-8')

        if isinstance(s2, unicode):
            s2 = s2.encode('utf-8')

        return s1 + s2
    else:
        # Prefer str
        if isinstance(s1, bytes):
            s1 = s1.decode('utf-8', 'replace')

        if isinstance(s2, bytes):
            s2 = s2.decode('utf-8', 'replace')

        return s1 + s2



def pytest_runtest_logreport(report):
    if is_in_xdist_node():
        # When running with xdist, we don't want the report to be called from the node, only
        # from the main process.
        return
    report_duration = report.duration
    report_when = report.when
    report_outcome = report.outcome

    if hasattr(report, 'wasxfail'):
        if report_outcome != 'skipped':
            report_outcome = 'passed'

    if report_outcome == 'passed':
        # passed on setup/teardown: no need to report if in setup or teardown
        # (only on the actual test if it passed).
        if report_when in ('setup', 'teardown'):
            return

        status = 'ok'

    elif report_outcome == 'skipped':
        status = 'skip'

    else:
        # It has only passed, skipped and failed (no error), so, let's consider
        # error if not on call.
        if report_when in ('setup', 'teardown'):
            status = 'error'

        else:
            # any error in the call (not in setup or teardown) is considered a
            # regular failure.
            status = 'fail'

    # This will work if pytest is not capturing it, if it is, nothing will
    # come from here...
    captured_output, error_contents = getattr(report, 'pydev_captured_output', ''), getattr(report, 'pydev_error_contents', '')
    for type_section, value in report.sections:
        if value:
            if type_section in ('err', 'stderr', 'Captured stderr call'):
                error_contents = append_strings(error_contents, value)
            else:
                captured_output = append_strings(error_contents, value)

    filename = getattr(report, 'pydev_fspath_strpath', '<unable to get>')
    test = report.location[2]

    if report_outcome != 'skipped':
        # On skipped, we'll have a traceback for the skip, which is not what we
        # want.
        exc = _get_error_contents_from_report(report)
        if exc:
            if error_contents:
                error_contents = append_strings(error_contents, '----------------------------- Exceptions -----------------------------\n')
            error_contents = append_strings(error_contents, exc)

    report_test(status, filename, test, captured_output, error_contents, report_duration)


def report_test(status, filename, test, captured_output, error_contents, duration):
    '''
    @param filename: 'D:\\src\\mod1\\hello.py'
    @param test: 'TestCase.testMet1'
    @param status: fail, error, ok
    '''
    time_str = '%.2f' % (duration,)
    pydev_runfiles_xml_rpc.notifyTest(
        status, captured_output, error_contents, filename, test, time_str)

if not hasattr(pytest, 'hookimpl'):
    raise AssertionError('Please upgrade pytest (the current version of pytest: %s is unsupported)' % (pytest.__version__,))

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    report = outcome.get_result()
    report.pydev_fspath_strpath = item.fspath.strpath
    report.pydev_captured_output, report.pydev_error_contents = get_curr_output()


@pytest.mark.tryfirst
def pytest_runtest_setup(item):
    '''
    Note: with xdist will be on a secondary process.
    '''
    # We have our own redirection: if xdist does its redirection, we'll have
    # nothing in our contents (which is OK), but if it does, we'll get nothing
    # from pytest but will get our own here.
    start_redirect()
    filename = item.fspath.strpath
    test = item.location[2]

    pydev_runfiles_xml_rpc.notifyStartTest(filename, test)
