# encoding: utf-8
"""Tests for IPython.core.ultratb
"""
import io
import logging
import sys
import os.path
from textwrap import dedent
import traceback
import unittest
from unittest import mock

from ..ultratb import ColorTB, VerboseTB, find_recursion


from IPython.testing import tools as tt
from IPython.testing.decorators import onlyif_unicode_paths
from IPython.utils.syspathcontext import prepended_to_syspath
from IPython.utils.tempdir import TemporaryDirectory

ip = get_ipython()

file_1 = """1
2
3
def f():
  1/0
"""

file_2 = """def f():
  1/0
"""

class ChangedPyFileTest(unittest.TestCase):
    def test_changing_py_file(self):
        """Traceback produced if the line where the error occurred is missing?
        
        https://github.com/ipython/ipython/issues/1456
        """
        with TemporaryDirectory() as td:
            fname = os.path.join(td, "foo.py")
            with open(fname, "w") as f:
                f.write(file_1)
            
            with prepended_to_syspath(td):
                ip.run_cell("import foo")
            
            with tt.AssertPrints("ZeroDivisionError"):
                ip.run_cell("foo.f()")
            
            # Make the file shorter, so the line of the error is missing.
            with open(fname, "w") as f:
                f.write(file_2)
            
            # For some reason, this was failing on the *second* call after
            # changing the file, so we call f() twice.
            with tt.AssertNotPrints("Internal Python error", channel='stderr'):
                with tt.AssertPrints("ZeroDivisionError"):
                    ip.run_cell("foo.f()")
                with tt.AssertPrints("ZeroDivisionError"):
                    ip.run_cell("foo.f()")

iso_8859_5_file = u'''# coding: iso-8859-5

def fail():
    """дбИЖ"""
    1/0     # дбИЖ
'''

class NonAsciiTest(unittest.TestCase):
    @onlyif_unicode_paths
    def test_nonascii_path(self):
        # Non-ascii directory name as well.
        with TemporaryDirectory(suffix=u'é') as td:
            fname = os.path.join(td, u"fooé.py")
            with open(fname, "w") as f:
                f.write(file_1)
            
            with prepended_to_syspath(td):
                ip.run_cell("import foo")
            
            with tt.AssertPrints("ZeroDivisionError"):
                ip.run_cell("foo.f()")
    
    def test_iso8859_5(self):
        with TemporaryDirectory() as td:
            fname = os.path.join(td, 'dfghjkl.py')

            with io.open(fname, 'w', encoding='iso-8859-5') as f:
                f.write(iso_8859_5_file)
            
            with prepended_to_syspath(td):
                ip.run_cell("from dfghjkl import fail")
            
            with tt.AssertPrints("ZeroDivisionError"):
                with tt.AssertPrints(u'дбИЖ', suppress=False):
                    ip.run_cell('fail()')
    
    def test_nonascii_msg(self):
        cell = u"raise Exception('é')"
        expected = u"Exception('é')"
        ip.run_cell("%xmode plain")
        with tt.AssertPrints(expected):
            ip.run_cell(cell)

        ip.run_cell("%xmode verbose")
        with tt.AssertPrints(expected):
            ip.run_cell(cell)

        ip.run_cell("%xmode context")
        with tt.AssertPrints(expected):
            ip.run_cell(cell)


class NestedGenExprTestCase(unittest.TestCase):
    """
    Regression test for the following issues:
    https://github.com/ipython/ipython/issues/8293
    https://github.com/ipython/ipython/issues/8205
    """
    def test_nested_genexpr(self):
        code = dedent(
            """\
            class SpecificException(Exception):
                pass

            def foo(x):
                raise SpecificException("Success!")

            sum(sum(foo(x) for _ in [0]) for x in [0])
            """
        )
        with tt.AssertPrints('SpecificException: Success!', suppress=False):
            ip.run_cell(code)


indentationerror_file = """if True:
zoon()
"""

class IndentationErrorTest(unittest.TestCase):
    def test_indentationerror_shows_line(self):
        # See issue gh-2398
        with tt.AssertPrints("IndentationError"):
            with tt.AssertPrints("zoon()", suppress=False):
                ip.run_cell(indentationerror_file)
        
        with TemporaryDirectory() as td:
            fname = os.path.join(td, "foo.py")
            with open(fname, "w") as f:
                f.write(indentationerror_file)
            
            with tt.AssertPrints("IndentationError"):
                with tt.AssertPrints("zoon()", suppress=False):
                    ip.magic('run %s' % fname)

se_file_1 = """1
2
7/
"""

se_file_2 = """7/
"""

class SyntaxErrorTest(unittest.TestCase):
    def test_syntaxerror_without_lineno(self):
        with tt.AssertNotPrints("TypeError"):
            with tt.AssertPrints("line unknown"):
                ip.run_cell("raise SyntaxError()")

    def test_syntaxerror_no_stacktrace_at_compile_time(self):
        syntax_error_at_compile_time = """
def foo():
    ..
"""
        with tt.AssertPrints("SyntaxError"):
            ip.run_cell(syntax_error_at_compile_time)

        with tt.AssertNotPrints("foo()"):
            ip.run_cell(syntax_error_at_compile_time)

    def test_syntaxerror_stacktrace_when_running_compiled_code(self):
        syntax_error_at_runtime = """
def foo():
    eval("..")

def bar():
    foo()

bar()
"""
        with tt.AssertPrints("SyntaxError"):
            ip.run_cell(syntax_error_at_runtime)
        # Assert syntax error during runtime generate stacktrace
        with tt.AssertPrints(["foo()", "bar()"]):
            ip.run_cell(syntax_error_at_runtime)

    def test_changing_py_file(self):
        with TemporaryDirectory() as td:
            fname = os.path.join(td, "foo.py")
            with open(fname, 'w') as f:
                f.write(se_file_1)

            with tt.AssertPrints(["7/", "SyntaxError"]):
                ip.magic("run " + fname)

            # Modify the file
            with open(fname, 'w') as f:
                f.write(se_file_2)

            # The SyntaxError should point to the correct line
            with tt.AssertPrints(["7/", "SyntaxError"]):
                ip.magic("run " + fname)

    def test_non_syntaxerror(self):
        # SyntaxTB may be called with an error other than a SyntaxError
        # See e.g. gh-4361
        try:
            raise ValueError('QWERTY')
        except ValueError:
            with tt.AssertPrints('QWERTY'):
                ip.showsyntaxerror()


class Python3ChainedExceptionsTest(unittest.TestCase):
    DIRECT_CAUSE_ERROR_CODE = """
try:
    x = 1 + 2
    print(not_defined_here)
except Exception as e:
    x += 55
    x - 1
    y = {}
    raise KeyError('uh') from e
    """

    EXCEPTION_DURING_HANDLING_CODE = """
try:
    x = 1 + 2
    print(not_defined_here)
except Exception as e:
    x += 55
    x - 1
    y = {}
    raise KeyError('uh')
    """

    SUPPRESS_CHAINING_CODE = """
try:
    1/0
except Exception:
    raise ValueError("Yikes") from None
    """

    def test_direct_cause_error(self):
        with tt.AssertPrints(["KeyError", "NameError", "direct cause"]):
            ip.run_cell(self.DIRECT_CAUSE_ERROR_CODE)

    def test_exception_during_handling_error(self):
        with tt.AssertPrints(["KeyError", "NameError", "During handling"]):
            ip.run_cell(self.EXCEPTION_DURING_HANDLING_CODE)

    def test_suppress_exception_chaining(self):
        with tt.AssertNotPrints("ZeroDivisionError"), \
             tt.AssertPrints("ValueError", suppress=False):
            ip.run_cell(self.SUPPRESS_CHAINING_CODE)


class RecursionTest(unittest.TestCase):
    DEFINITIONS = """
def non_recurs():
    1/0

def r1():
    r1()

def r3a():
    r3b()

def r3b():
    r3c()

def r3c():
    r3a()

def r3o1():
    r3a()

def r3o2():
    r3o1()
"""
    def setUp(self):
        ip.run_cell(self.DEFINITIONS)

    def test_no_recursion(self):
        with tt.AssertNotPrints("frames repeated"):
            ip.run_cell("non_recurs()")

    def test_recursion_one_frame(self):
        with tt.AssertPrints("1 frames repeated"):
            ip.run_cell("r1()")

    def test_recursion_three_frames(self):
        with tt.AssertPrints("3 frames repeated"):
            ip.run_cell("r3o2()")

    def test_find_recursion(self):
        captured = []
        def capture_exc(*args, **kwargs):
            captured.append(sys.exc_info())
        with mock.patch.object(ip, 'showtraceback', capture_exc):
            ip.run_cell("r3o2()")

        self.assertEqual(len(captured), 1)
        etype, evalue, tb = captured[0]
        self.assertIn("recursion", str(evalue))

        records = ip.InteractiveTB.get_records(tb, 3, ip.InteractiveTB.tb_offset)
        for r in records[:10]:
            print(r[1:4])

        # The outermost frames should be:
        # 0: the 'cell' that was running when the exception came up
        # 1: r3o2()
        # 2: r3o1()
        # 3: r3a()
        # Then repeating r3b, r3c, r3a
        last_unique, repeat_length = find_recursion(etype, evalue, records)
        self.assertEqual(last_unique, 2)
        self.assertEqual(repeat_length, 3)


#----------------------------------------------------------------------------

# module testing (minimal)
def test_handlers():
    def spam(c, d_e):
        (d, e) = d_e
        x = c + d
        y = c * d
        foo(x, y)

    def foo(a, b, bar=1):
        eggs(a, b + bar)

    def eggs(f, g, z=globals()):
        h = f + g
        i = f - g
        return h / i

    buff = io.StringIO()

    buff.write('')
    buff.write('*** Before ***')
    try:
        buff.write(spam(1, (2, 3)))
    except:
        traceback.print_exc(file=buff)

    handler = ColorTB(ostream=buff)
    buff.write('*** ColorTB ***')
    try:
        buff.write(spam(1, (2, 3)))
    except:
        handler(*sys.exc_info())
    buff.write('')

    handler = VerboseTB(ostream=buff)
    buff.write('*** VerboseTB ***')
    try:
        buff.write(spam(1, (2, 3)))
    except:
        handler(*sys.exc_info())
    buff.write('')


class TokenizeFailureTest(unittest.TestCase):
    """Tests related to https://github.com/ipython/ipython/issues/6864."""

    def testLogging(self):
        message = "An unexpected error occurred while tokenizing input"
        cell = 'raise ValueError("""a\nb""")'

        stream = io.StringIO()
        handler = logging.StreamHandler(stream)
        logger = logging.getLogger()
        loglevel = logger.level
        logger.addHandler(handler)
        self.addCleanup(lambda: logger.removeHandler(handler))
        self.addCleanup(lambda: logger.setLevel(loglevel))

        logger.setLevel(logging.INFO)
        with tt.AssertNotPrints(message):
            ip.run_cell(cell)
        self.assertNotIn(message, stream.getvalue())

        logger.setLevel(logging.DEBUG)
        with tt.AssertNotPrints(message):
            ip.run_cell(cell)
        self.assertIn(message, stream.getvalue())
