# -*- coding: utf-8 -*-
"""
Tests for the various utility functions and classes in ``future.utils``
"""

from __future__ import absolute_import, unicode_literals, print_function
import re, sys, traceback
from future.builtins import *
from future.utils import (old_div, istext, isbytes, native, PY2, PY3,
                         native_str, raise_, as_native_str, ensure_new_type,
                         bytes_to_native_str, raise_from)
from future.tests.base import expectedFailurePY3

from numbers import Integral
from future.tests.base import unittest, skip26


TEST_UNICODE_STR = u'ℝεα∂@ßʟ℮ ☂ℯṧт υηḯ¢☺ḓ℮'


class MyExceptionIssue235(Exception):
    def __init__(self, a, b):
        super(MyExceptionIssue235, self).__init__('{0}: {1}'.format(a, b))


class TestUtils(unittest.TestCase):
    def setUp(self):
        self.s = TEST_UNICODE_STR
        self.s2 = str(self.s)
        self.b = b'ABCDEFG'
        self.b2 = bytes(self.b)

    def test_old_div(self):
        """
        Tests whether old_div(a, b) is always equal to Python 2's a / b.
        """
        self.assertEqual(old_div(1, 2), 0)
        self.assertEqual(old_div(2, 2), 1)
        self.assertTrue(isinstance(old_div(2, 2), int))

        self.assertEqual(old_div(3, 2), 1)
        self.assertTrue(isinstance(old_div(3, 2), int))

        self.assertEqual(old_div(3., 2), 1.5)
        self.assertTrue(not isinstance(old_div(3., 2), int))

        self.assertEqual(old_div(-1, 2.), -0.5)
        self.assertTrue(not isinstance(old_div(-1, 2.), int))

        with self.assertRaises(ZeroDivisionError):
            old_div(0, 0)
        with self.assertRaises(ZeroDivisionError):
            old_div(1, 0)

    def test_native_str(self):
        """
        Tests whether native_str is really equal to the platform str.
        """
        if PY2:
            import __builtin__
            builtin_str = __builtin__.str
        else:
            import builtins
            builtin_str = builtins.str

        inputs = [b'blah', u'blah', 'blah']
        for s in inputs:
            self.assertEqual(native_str(s), builtin_str(s))
            self.assertTrue(isinstance(native_str(s), builtin_str))

    def test_native(self):
        a = int(10**20)     # long int
        b = native(a)
        self.assertEqual(a, b)
        if PY2:
            self.assertEqual(type(b), long)
        else:
            self.assertEqual(type(b), int)

        c = bytes(b'ABC')
        d = native(c)
        self.assertEqual(c, d)
        if PY2:
            self.assertEqual(type(d), type(b'Py2 byte-string'))
        else:
            self.assertEqual(type(d), bytes)

        s = str(u'ABC')
        t = native(s)
        self.assertEqual(s, t)
        if PY2:
            self.assertEqual(type(t), unicode)
        else:
            self.assertEqual(type(t), str)

        d1 = dict({'a': 1, 'b': 2})
        d2 = native(d1)
        self.assertEqual(d1, d2)
        self.assertEqual(type(d2), type({}))

    def test_istext(self):
        self.assertTrue(istext(self.s))
        self.assertTrue(istext(self.s2))
        self.assertFalse(istext(self.b))
        self.assertFalse(istext(self.b2))

    def test_isbytes(self):
        self.assertTrue(isbytes(self.b))
        self.assertTrue(isbytes(self.b2))
        self.assertFalse(isbytes(self.s))
        self.assertFalse(isbytes(self.s2))

    def test_raise_(self):
        def valuerror():
            try:
                raise ValueError("Apples!")
            except Exception as e:
                raise_(e)

        self.assertRaises(ValueError, valuerror)

        def with_value():
            raise_(IOError, "This is an error")

        self.assertRaises(IOError, with_value)

        try:
            with_value()
        except IOError as e:
            self.assertEqual(str(e), "This is an error")

        def with_traceback():
            try:
                raise ValueError("An error")
            except Exception as e:
                _, _, traceback = sys.exc_info()
                raise_(IOError, str(e), traceback)

        self.assertRaises(IOError, with_traceback)

        try:
            with_traceback()
        except IOError as e:
            self.assertEqual(str(e), "An error")

        class Timeout(BaseException):
            pass

        self.assertRaises(Timeout, raise_, Timeout)
        self.assertRaises(Timeout, raise_, Timeout())

        if PY3:
            self.assertRaisesRegexp(
                TypeError, "class must derive from BaseException",
                raise_, int)

    def test_raise_from_None(self):
        try:
            try:
                raise TypeError("foo")
            except:
                raise_from(ValueError(), None)
        except ValueError as e:
            self.assertTrue(isinstance(e.__context__, TypeError))
            self.assertIsNone(e.__cause__)

    def test_issue_235(self):
        def foo():
            raise MyExceptionIssue235(3, 7)

        def bar():
            try:
                foo()
            except Exception as err:
                raise_from(ValueError('blue'), err)

        try:
            bar()
        except ValueError as e:
            pass
        # incorrectly raises a TypeError on Py3 as of v0.15.2.

    def test_raise_custom_exception(self):
        """
        Test issue #387.
        """
        class CustomException(Exception):
            def __init__(self, severity, message):
                super().__init__("custom message of severity %d: %s" % (
                    severity, message))

        def raise_custom_exception():
            try:
                raise CustomException(1, "hello")
            except CustomException:
                raise_(*sys.exc_info())

        self.assertRaises(CustomException, raise_custom_exception)

    @skip26
    def test_as_native_str(self):
        """
        Tests the decorator as_native_str()
        """
        class MyClass(object):
            @as_native_str()
            def __repr__(self):
                return u'abc'

        obj = MyClass()

        self.assertEqual(repr(obj), 'abc')
        if PY2:
            self.assertEqual(repr(obj), b'abc')
        else:
            self.assertEqual(repr(obj), u'abc')

    def test_ensure_new_type(self):
        s = u'abcd'
        s2 = str(s)
        self.assertEqual(ensure_new_type(s), s2)
        self.assertEqual(type(ensure_new_type(s)), str)

        b = b'xyz'
        b2 = bytes(b)
        self.assertEqual(ensure_new_type(b), b2)
        self.assertEqual(type(ensure_new_type(b)), bytes)

        i = 10000000000000
        i2 = int(i)
        self.assertEqual(ensure_new_type(i), i2)
        self.assertEqual(type(ensure_new_type(i)), int)

        l = []
        self.assertIs(ensure_new_type(l), l)

    def test_bytes_to_native_str(self):
        """
        Test for issue #47
        """
        b = bytes(b'abc')
        s = bytes_to_native_str(b)
        if PY2:
            self.assertEqual(s, b)
        else:
            self.assertEqual(s, 'abc')
        self.assertTrue(isinstance(s, native_str))
        self.assertEqual(type(s), native_str)


class TestCause(unittest.TestCase):
    """
    Except for the first method, these were adapted from Py3.3's
    Lib/test/test_raise.py.
    """
    def test_normal_use(self):
        """
        Adapted from PEP 3134 docs
        """
        # Setup:
        class DatabaseError(Exception):
            pass

        # Python 2 and 3:
        from future.utils import raise_from

        class FileDatabase:
            def __init__(self, filename):
                try:
                    self.file = open(filename)
                except IOError as exc:
                    raise_from(DatabaseError('failed to open'), exc)

        # Testing the above:
        try:
            fd = FileDatabase('non_existent_file.txt')
        except Exception as e:
            assert isinstance(e.__cause__, IOError)   # FileNotFoundError on
                                                      # Py3.3+ inherits from IOError

    def testCauseSyntax(self):
        try:
            try:
                try:
                    raise TypeError
                except Exception:
                    raise_from(ValueError, None)
            except ValueError as exc:
                self.assertIsNone(exc.__cause__)
                self.assertTrue(exc.__suppress_context__)
                exc.__suppress_context__ = False
                raise exc
        except ValueError as exc:
            e = exc

        self.assertIsNone(e.__cause__)
        self.assertFalse(e.__suppress_context__)
        self.assertIsInstance(e.__context__, TypeError)

    def test_invalid_cause(self):
        try:
            raise_from(IndexError, 5)
        except TypeError as e:
            self.assertIn("exception cause", str(e))
        else:
            self.fail("No exception raised")

    def test_class_cause(self):
        try:
            raise_from(IndexError, KeyError)
        except IndexError as e:
            self.assertIsInstance(e.__cause__, KeyError)
        else:
            self.fail("No exception raised")

    def test_instance_cause(self):
        cause = KeyError('blah')
        try:
            raise_from(IndexError, cause)
        except IndexError as e:
            # FAILS:
            self.assertTrue(e.__cause__ is cause)
            # Even this weaker version seems to fail, although repr(cause) looks correct.
            # Is there something strange about testing exceptions for equality?
            self.assertEqual(e.__cause__, cause)
        else:
            self.fail("No exception raised")

    def test_erroneous_cause(self):
        class MyException(Exception):
            def __init__(self):
                raise RuntimeError()

        try:
            raise_from(IndexError, MyException)
        except RuntimeError:
            pass
        else:
            self.fail("No exception raised")

    def test_single_exception_stacktrace(self):
        expected = '''Traceback (most recent call last):
  File "/opt/python-future/tests/test_future/test_utils.py", line 328, in test_single_exception_stacktrace
    raise CustomException('ERROR')
'''
        if PY2:
            expected += 'CustomException: ERROR\n'
        else:
            expected += 'test_future.test_utils.CustomException: ERROR\n'

        try:
            raise CustomException('ERROR')
        except:
            ret = re.sub(r'"[^"]*tests/test_future', '"/opt/python-future/tests/test_future', traceback.format_exc())
            ret = re.sub(r', line \d+,', ', line 328,', ret)
            self.assertEqual(expected, ret)
        else:
            self.fail('No exception raised')

    if PY2:
        def test_chained_exceptions_stacktrace(self):
            expected = '''Traceback (most recent call last):
  File "/opt/python-future/tests/test_future/test_utils.py", line 1, in test_chained_exceptions_stacktrace
    raise_from(CustomException('ERROR'), val_err)
  File "/opt/python-future/src/future/utils/__init__.py", line 1, in raise_from
    raise e
CustomException: ERROR

The above exception was the direct cause of the following exception:

  File "/opt/python-future/tests/test_future/test_utils.py", line 1, in test_chained_exceptions_stacktrace
    raise ValueError('Wooops')
ValueError: Wooops
'''

            try:
                try:
                    raise ValueError('Wooops')
                except ValueError as val_err:
                    raise_from(CustomException('ERROR'), val_err)
            except Exception as err:
                ret = re.sub(r'"[^"]*tests/test_future', '"/opt/python-future/tests/test_future', traceback.format_exc())
                ret = re.sub(r'"[^"]*future/utils/__init__.py', '"/opt/python-future/src/future/utils/__init__.py', ret)
                ret = re.sub(r', line \d+,', ', line 1,', ret)
                self.assertEqual(expected.splitlines(), ret.splitlines())
            else:
                self.fail('No exception raised')


class CustomException(Exception):
    if PY2:
        def __str__(self):
            try:
                out = Exception.__str__(self)
                if hasattr(self, '__cause__') and self.__cause__ and hasattr(self.__cause__, '__traceback__') and self.__cause__.__traceback__:
                    out += '\n\nThe above exception was the direct cause of the following exception:\n\n'
                    out += ''.join(traceback.format_tb(self.__cause__.__traceback__) + ['{0}: {1}'.format(self.__cause__.__class__.__name__, self.__cause__)])
                return out
            except Exception as e:
                print(e)
    else:
        pass


if __name__ == '__main__':
    unittest.main()
