import threading

from _pydevd_bundle.pydevd_utils import convert_dap_log_message_to_expression
from tests_python.debug_constants import TEST_GEVENT, IS_CPYTHON
import sys
from _pydevd_bundle.pydevd_constants import IS_WINDOWS, IS_PYPY, IS_JYTHON
import pytest
import os
from _pydevd_bundle.pydevd_thread_lifecycle import pydevd_find_thread_by_id


def test_expression_to_evaluate():
    from _pydevd_bundle.pydevd_vars import _expression_to_evaluate
    assert _expression_to_evaluate(b'expr') == b'expr'
    assert _expression_to_evaluate(b'  expr') == b'expr'
    assert _expression_to_evaluate(b'for a in b:\n  foo') == b'for a in b:\n  foo'
    assert _expression_to_evaluate(b'  for a in b:\n  foo') == b'for a in b:\nfoo'
    assert _expression_to_evaluate(b'  for a in b:\nfoo') == b'  for a in b:\nfoo'
    assert _expression_to_evaluate(b'\tfor a in b:\n\t\tfoo') == b'for a in b:\n\tfoo'

    assert _expression_to_evaluate(u'  expr') == u'expr'
    assert _expression_to_evaluate(u'  for a in expr:\n  pass') == u'for a in expr:\npass'


@pytest.mark.skipif(IS_WINDOWS, reason='Brittle on Windows.')
def test_is_main_thread():
    '''
    This is now skipped due to it failing sometimes (only on Windows).

    I (fabioz) am not 100% sure on why this happens, but when this happens the initial thread for
    the tests seems to be a non main thread.

    i.e.: With an autouse fixture with a scope='session' with the code and error message below, it's
    possible to see that at even at the `conftest` import (where indent_at_import is assigned) the
    current thread is already not the main thread.

    As far as I know this seems to be an issue in how pytest-xdist is running the tests (i.e.:
    I couldn't reproduce this without running with `python -m pytest -n 0 ...`).

    -------- Code to check error / error output ----------

    from _pydevd_bundle.pydevd_utils import is_current_thread_main_thread
    import threading
    indent_at_import = threading.get_ident()

    @pytest.yield_fixture(autouse=True, scope='session')
    def check_main_thread_session(request):
        if not is_current_thread_main_thread():
            error_msg = 'Current thread does not seem to be a main thread at the start of the session. Details:\n'
            current_thread = threading.current_thread()
            error_msg += 'Current thread: %s\n' % (current_thread,)
            error_msg += 'Current thread ident: %s\n' % (current_thread.ident,)
            error_msg += 'ident at import: %s\n' % (indent_at_import,)
            error_msg += 'curr ident: %s\n' % (threading.get_ident(),)

            if hasattr(threading, 'main_thread'):
                error_msg += 'Main thread found: %s\n' % (threading.main_thread(),)
                error_msg += 'Main thread id: %s\n' % (threading.main_thread().ident,)
            else:
                error_msg += 'Current main thread not instance of: %s (%s)\n' % (
                    threading._MainThread, current_thread.__class__.__mro__,)

>           raise AssertionError(error_msg)
E           AssertionError: Current thread does not seem to be a main thread at the start of the session. Details:
E           Current thread: <_DummyThread(Dummy-2, started daemon 7072)>
E           Current thread ident: 7072
E           ident at import: 7072
E           curr ident: 7072
E           Main thread found: <_MainThread(MainThread, started 5924)>
E           Main thread id: 5924

conftest.py:67: AssertionError
    '''
    from _pydevd_bundle.pydevd_utils import is_current_thread_main_thread
    from _pydevd_bundle.pydevd_utils import dump_threads
    if not is_current_thread_main_thread():
        error_msg = 'Current thread does not seem to be a main thread. Details:\n'
        current_thread = threading.current_thread()
        error_msg += 'Current thread: %s\n' % (current_thread,)

        if hasattr(threading, 'main_thread'):
            error_msg += 'Main thread found: %s\n' % (threading.main_thread(),)
        else:
            error_msg += 'Current main thread not instance of: %s (%s)' % (
                threading._MainThread, current_thread.__class__.__mro__,)

        from io import StringIO

        stream = StringIO()
        dump_threads(stream=stream)
        error_msg += '\n\n' + stream.getvalue()
        raise AssertionError(error_msg)

    class NonMainThread(threading.Thread):

        def run(self):
            self.is_main_thread = is_current_thread_main_thread()

    non_main_thread = NonMainThread()
    non_main_thread.start()
    non_main_thread.join()
    assert not non_main_thread.is_main_thread


def test_find_thread():
    from _pydevd_bundle.pydevd_constants import get_current_thread_id
    assert pydevd_find_thread_by_id('123') is None

    assert pydevd_find_thread_by_id(
        get_current_thread_id(threading.current_thread())) is threading.current_thread()


def check_dap_log_message(log_message, expected, evaluated, eval_locals=None):
    ret = convert_dap_log_message_to_expression(log_message)
    assert ret == expected
    assert (eval(ret, eval_locals)) == evaluated
    return ret


def test_convert_dap_log_message_to_expression():
    assert check_dap_log_message(
        'a',
        "'a'",
        'a',
    )
    assert check_dap_log_message(
        'a {a}',
        "'a %s' % (a,)",
        'a value',
        {'a': 'value'}
    )
    assert check_dap_log_message(
        'a {1}',
        "'a %s' % (1,)",
        'a 1'
    )
    assert check_dap_log_message(
        'a {  }',
        "'a '",
        'a '
    )
    assert check_dap_log_message(
        'a {1} {2}',
        "'a %s %s' % (1, 2,)",
        'a 1 2',
    )
    assert check_dap_log_message(
        'a {{22:22}} {2}',
        "'a %s %s' % ({22:22}, 2,)",
        'a {22: 22} 2'
    )
    assert check_dap_log_message(
        'a {(22,33)}} {2}',
        "'a %s} %s' % ((22,33), 2,)",
        'a (22, 33)} 2'
    )

    assert check_dap_log_message(
        'a {{1: {1}}}',
        "'a %s' % ({1: {1}},)",
        'a {1: {1}}'
    )

    # Error condition.
    assert check_dap_log_message(
        'a {{22:22} {2}',
        "'Unbalanced braces in: a {{22:22} {2}'",
        'Unbalanced braces in: a {{22:22} {2}'
    )


def test_pydevd_log():
    from _pydev_bundle import pydev_log
    import io
    from _pydev_bundle.pydev_log import log_context

    stream = io.StringIO()
    with log_context(0, stream=stream):
        pydev_log.critical('always')
        pydev_log.info('never')

    assert stream.getvalue().endswith('always\n')

    stream = io.StringIO()
    with log_context(1, stream=stream):
        pydev_log.critical('always')
        pydev_log.info('this too')
        assert stream.getvalue().endswith('always\n0.00s - this too\n')

    stream = io.StringIO()
    with log_context(0, stream=stream):
        pydev_log.critical('always %s', 1)

    assert stream.getvalue().endswith('always 1\n')

    stream = io.StringIO()
    with log_context(0, stream=stream):
        pydev_log.critical('always %s %s', 1, 2)

    assert stream.getvalue().endswith('always 1 2\n')

    stream = io.StringIO()
    with log_context(0, stream=stream):
        pydev_log.critical('always %s %s', 1)

    # Even if there's an error in the formatting, don't fail, just print the message and args.
    assert stream.getvalue().endswith('always %s %s - (1,)\n')

    stream = io.StringIO()
    with log_context(0, stream=stream):
        try:
            raise RuntimeError()
        except:
            pydev_log.exception('foo')

        assert 'foo\n' in stream.getvalue()
        assert 'raise RuntimeError()' in stream.getvalue()

    stream = io.StringIO()
    with log_context(0, stream=stream):
        pydev_log.error_once('always %s %s', 1)

    # Even if there's an error in the formatting, don't fail, just print the message and args.
    assert stream.getvalue().endswith('always %s %s - (1,)\n')


def test_pydevd_logging_files(tmpdir):
    from _pydev_bundle import pydev_log
    from _pydevd_bundle.pydevd_constants import DebugInfoHolder
    import os.path
    from _pydev_bundle.pydev_log import _LoggingGlobals

    import io
    from _pydev_bundle.pydev_log import log_context

    stream = io.StringIO()
    with log_context(0, stream=stream):
        d1 = str(tmpdir.join('d1'))
        d2 = str(tmpdir.join('d2'))

        for d in (d1, d2):
            DebugInfoHolder.PYDEVD_DEBUG_FILE = os.path.join(d, 'file.txt')
            pydev_log.initialize_debug_stream(reinitialize=True)

            assert os.path.normpath(_LoggingGlobals._debug_stream_filename) == \
                os.path.normpath(os.path.join(d, 'file.%s.txt' % os.getpid()))

            assert os.path.exists(_LoggingGlobals._debug_stream_filename)

            assert pydev_log.list_log_files(DebugInfoHolder.PYDEVD_DEBUG_FILE) == [
                _LoggingGlobals._debug_stream_filename]


def _check_tracing_other_threads():
    import pydevd_tracing
    import time
    from tests_python.debugger_unittest import wait_for_condition
    try:
        import _thread
    except ImportError:
        import thread as _thread

    # This method is called in a subprocess, so, make sure we exit properly even if we somehow
    # deadlock somewhere else.
    def dump_threads_and_kill_on_timeout():
        time.sleep(10)
        from _pydevd_bundle import pydevd_utils
        pydevd_utils.dump_threads()
        time.sleep(1)
        import os
        os._exit(77)

    _thread.start_new_thread(dump_threads_and_kill_on_timeout, ())

    def method():
        while True:
            trace_func = sys.gettrace()
            if trace_func:
                threading.current_thread().trace_func = trace_func
                break
            time.sleep(.01)

    def dummy_thread_method():
        threads.append(threading.current_thread())
        method()

    threads = []
    threads.append(threading.Thread(target=method))
    threads[-1].daemon = True
    threads[-1].start()
    _thread.start_new_thread(dummy_thread_method, ())

    wait_for_condition(lambda: len(threads) == 2, msg=lambda:'Found threads: %s' % (threads,))

    def tracing_func(frame, event, args):
        return tracing_func

    assert pydevd_tracing.set_trace_to_threads(tracing_func) == 0

    def check_threads_tracing_func():
        for t in threads:
            if getattr(t, 'trace_func', None) != tracing_func:
                return False
        return True

    wait_for_condition(check_threads_tracing_func)

    assert tracing_func == sys.gettrace()


def _build_launch_env():
    import os
    import pydevd

    environ = os.environ.copy()
    cwd = os.path.abspath(os.path.dirname(__file__))
    assert os.path.isdir(cwd)

    resources_dir = os.path.join(os.path.dirname(pydevd.__file__), 'tests_python', 'resources')
    assert os.path.isdir(resources_dir)

    attach_to_process_dir = os.path.join(os.path.dirname(pydevd.__file__), 'pydevd_attach_to_process')
    assert os.path.isdir(attach_to_process_dir)

    pydevd_dir = os.path.dirname(pydevd.__file__)
    assert os.path.isdir(pydevd_dir)

    environ['PYTHONPATH'] = (
            cwd + os.pathsep +
            resources_dir + os.pathsep +
            attach_to_process_dir + os.pathsep +
            pydevd_dir + os.pathsep +
            environ.get('PYTHONPATH', '')
    )
    return cwd, environ


def _check_in_separate_process(method_name, module_name='test_utilities', update_env={}):
    import subprocess
    cwd, environ = _build_launch_env()
    environ.update(update_env)

    subprocess.check_call(
        [sys.executable, '-c', 'import %(module_name)s;%(module_name)s.%(method_name)s()' % dict(
            method_name=method_name, module_name=module_name)],
        env=environ,
        cwd=cwd
    )


@pytest.mark.skipif(not IS_CPYTHON, reason='Functionality to trace other threads requires CPython.')
def test_tracing_other_threads():
    # Note: run this test in a separate process so that it doesn't mess with any current tracing
    # in our current process.
    _check_in_separate_process('_check_tracing_other_threads')


def _check_basic_tracing():
    import pydevd_tracing

    # Note: run this test in a separate process so that it doesn't mess with any current tracing
    # in our current process.
    called = [0]

    def tracing_func(frame, event, args):
        called[0] = called[0] + 1
        return tracing_func

    assert pydevd_tracing.set_trace_to_threads(tracing_func) == 0

    def foo():
        pass

    foo()
    assert called[0] > 2


@pytest.mark.skipif(not IS_CPYTHON, reason='Functionality to trace other threads requires CPython.')
def test_tracing_basic():
    _check_in_separate_process('_check_basic_tracing')


@pytest.mark.skipif(not IS_CPYTHON, reason='Functionality to trace other threads requires CPython.')
def test_find_main_thread_id():
    # Note: run the checks below in a separate process because they rely heavily on what's available
    # in the env (such as threads or having threading imported).
    _check_in_separate_process('check_main_thread_id_simple', '_pydevd_test_find_main_thread_id')
    _check_in_separate_process('check_main_thread_id_multiple_threads', '_pydevd_test_find_main_thread_id')
    _check_in_separate_process('check_win_threads', '_pydevd_test_find_main_thread_id')
    _check_in_separate_process('check_fix_main_thread_id_multiple_threads', '_pydevd_test_find_main_thread_id')

    import subprocess
    import pydevd
    cwd, environ = _build_launch_env()

    subprocess.check_call(
        [sys.executable, '-m', '_pydevd_test_find_main_thread_id'],
        env=environ,
        cwd=cwd
    )

    resources_dir = os.path.join(os.path.dirname(pydevd.__file__), 'tests_python', 'resources')

    subprocess.check_call(
        [sys.executable, os.path.join(resources_dir, '_pydevd_test_find_main_thread_id.py') ],
        env=environ,
        cwd=cwd
    )


@pytest.mark.skipif(not IS_WINDOWS or IS_JYTHON, reason='Windows-only test.')
def test_get_ppid():
    from _pydevd_bundle.pydevd_api import PyDevdAPI
    api = PyDevdAPI()
    # On python 3 we can check that our internal api which is used for Python 2 gives the
    # same result as os.getppid.
    ppid = os.getppid()
    assert api._get_windows_ppid() == ppid


def _check_gevent(expect_msg):
    from _pydevd_bundle.pydevd_utils import notify_about_gevent_if_needed
    assert not notify_about_gevent_if_needed()
    import gevent
    assert not notify_about_gevent_if_needed()
    import gevent.monkey
    assert not notify_about_gevent_if_needed()
    gevent.monkey.patch_all()
    assert notify_about_gevent_if_needed() == expect_msg


def check_notify_on_gevent_loaded():
    _check_gevent(True)


def check_dont_notify_on_gevent_loaded():
    _check_gevent(False)


@pytest.mark.skipif(not TEST_GEVENT, reason='Gevent not installed.')
def test_gevent_notify():
    _check_in_separate_process('check_notify_on_gevent_loaded', update_env={'GEVENT_SUPPORT': ''})
    _check_in_separate_process('check_dont_notify_on_gevent_loaded', update_env={'GEVENT_SUPPORT': 'True'})


@pytest.mark.skipif(True, reason='Skipping because running this test can interrupt the test suite execution.')
def test_interrupt_main_thread():
    from _pydevd_bundle.pydevd_utils import interrupt_main_thread
    import time

    main_thread = threading.current_thread()

    def interrupt():
        # sleep here so that the main thread in the test can get to the sleep too (otherwise
        # if we interrupt too fast we won't really check that the sleep itself
        # got interrupted -- although if that happens on some tests runs it's
        # not really an issue either).
        time.sleep(1)
        interrupt_main_thread(main_thread)

    if IS_PYPY:
        # On PyPy a time.sleep() is not being properly interrupted,
        # so, let's just check that it throws the KeyboardInterrupt in the
        # next instruction.
        timeout = 2
    else:
        timeout = 20
    initial_time = time.time()
    try:
        t = threading.Thread(target=interrupt)
        t.start()
        time.sleep(timeout)
    except KeyboardInterrupt:
        if not IS_PYPY:
            actual_timeout = time.time() - initial_time
            # If this fails it means that although we interrupted Python actually waited for the next
            # instruction to send the event and didn't really interrupt the thread.
            assert actual_timeout < timeout, 'Expected the actual timeout (%s) to be < than the timeout (%s)' % (
                actual_timeout, timeout)
    else:
        raise AssertionError('KeyboardInterrupt not generated in main thread.')


@pytest.mark.skipif(sys.version_info[0] < 3, reason='Only available for Python 3.')
def test_get_smart_step_into_variant_from_frame_offset():
    from _pydevd_bundle.pydevd_bytecode_utils import get_smart_step_into_variant_from_frame_offset
    variants = []
    assert get_smart_step_into_variant_from_frame_offset(0, variants) is None

    class DummyVariant(object):

        def __init__(self, offset):
            self.offset = offset

    variants.append(DummyVariant(1))
    assert get_smart_step_into_variant_from_frame_offset(0, variants) is None
    assert get_smart_step_into_variant_from_frame_offset(1, variants) is variants[0]
    assert get_smart_step_into_variant_from_frame_offset(2, variants) is variants[0]

    variants.append(DummyVariant(3))
    assert get_smart_step_into_variant_from_frame_offset(0, variants) is None
    assert get_smart_step_into_variant_from_frame_offset(1, variants) is variants[0]
    assert get_smart_step_into_variant_from_frame_offset(2, variants) is variants[0]
    assert get_smart_step_into_variant_from_frame_offset(3, variants) is variants[1]
    assert get_smart_step_into_variant_from_frame_offset(4, variants) is variants[1]


def test_threading_hide_pydevd():

    class T(threading.Thread):

        def __init__(self, is_pydev_daemon_thread):
            from _pydevd_bundle.pydevd_daemon_thread import mark_as_pydevd_daemon_thread
            threading.Thread.__init__(self)
            if is_pydev_daemon_thread:
                mark_as_pydevd_daemon_thread(self)
            else:
                self.daemon = True
            self.event = threading.Event()

        def run(self):
            self.event.wait(10)

    current_count = threading.active_count()
    t0 = T(True)
    t1 = T(False)
    t0.start()
    t1.start()

    # i.e.: the patching doesn't work for other implementations.
    if IS_CPYTHON:
        assert threading.active_count() == current_count + 1
        assert t0 not in threading.enumerate()
    else:
        assert threading.active_count() == current_count + 2
        assert t0 in threading.enumerate()

    assert t1 in threading.enumerate()
    t0.event.set()
    t1.event.set()


def test_import_token_from_module():
    from _pydevd_bundle.pydevd_utils import import_attr_from_module

    with pytest.raises(ImportError):
        import_attr_from_module('sys')

    with pytest.raises(ImportError):
        import_attr_from_module('sys.settrace.foo')

    assert import_attr_from_module('sys.settrace') == sys.settrace
    assert import_attr_from_module('threading.Thread.start') == threading.Thread.start
