#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is almost entirely borrowed from
# https://github.com/Yelp/Testify/blob/master/testify/assertions.py
# The only modfifications are for py3k support
from __future__ import absolute_import, with_statement

import contextlib
import re
import sys

from . import utils

try:
    STRING_TYPE = basestring
except: # py3k
    STRING_TYPE = str


def _val_subtract(val1, val2, dict_subtractor, list_subtractor):
    """
    Find the difference between two container types

    Returns:

    The difference between the values as defined by list_subtractor() and
    dict_subtractor() if both values are the same container type.

    None if val1 == val2
    val1 if type(val1) != type(val1)
    Otherwise - the difference between the values
    """

    if val1 == val2:
        # if the values are the same, return a degenerate type
        # this case is not used by list_subtract or dict_subtract
        return type(val1)()

    if isinstance(val1, dict) and isinstance(val2, dict):
        val_diff = dict_subtractor(val1, val2)
    elif isinstance(val1, (list, tuple)) and isinstance(val2, (list, tuple)):
        val_diff = list_subtractor(val1, val2)
    else:
        val_diff = val1

    return val_diff


def _dict_subtract(dict1, dict2):
    """
    Return key,value pairs from dict1 that are not in dict2

    Returns:
    A new dict 'res_dict' with the following properties:

    For all (key, val) pairs where key appears in dict2:

    if dict1[val] == dict2[val] then res_dict[val] is not defined
    else res_dict[val] == dict1[val]

    If vals are themselves dictionaries the algorim is applied recursively.

    Example:
        _dict_subtract({
                       1: 'one',
                       2: 'two',
                       3: {'a': 'A', 'b': 'B'},
                       4: {'c': 'C', 'd': 'D'}
                      },
                      {
                       2: 'two',
                       3: {'a': 'A', 'b': 'B'},
                       4: {'d': 'D'},
                       5: {'e': 'E'}
                      }) => {1: 'one', 4: {'c': 'C'}}
    """

    # make a result we can edit
    result = dict(dict1)

    # find the common keys -- i.e., the ones we might need to subtract
    common_keys = set(dict1.keys()) & set(dict2.keys())
    for key in common_keys:
        val1, val2 = dict1[key], dict2[key]

        if val1 == val2:
            # values are the same: subtract
            del result[key]
        else:
            # values are different: set the output key to the different between the values
            result[key] = _val_subtract(val1, val2, _dict_subtract, _list_subtract)

    return result


def _list_subtract(list1, list2):
    """
    Returns the difference between list1 and list2.

    _list_subtract([1,2,3], [3,2,1]) == [1,3]

    If any items in the list are container types, the method recursively calls
    itself or _dict_subtract() to subtract the child
    containers.
    """

    # call val_subtract on all items that are not the same
    res_list = [_val_subtract(val1, val2, _dict_subtract, _list_subtract)
                for val1, val2 in zip(list1, list2) if val1 != val2]

    # now append items that come after any item in list1
    res_list += list1[len(list2):]

    # return a tuple of list1 is a tuple
    if isinstance(list1, tuple):
        return tuple(res_list)
    else:
        return res_list


def assert_raises(*args, **kwargs):
    """Assert an exception is raised as a context manager or by passing in a
    callable and its arguments.

    As a context manager:
    >>> with assert_raises(Exception):
    ...     raise Exception

    Pass in a callable:
    >>> def raise_exception(arg, kwarg=None):
    ...     raise Exception
    >>> assert_raises(Exception, raise_exception, 1, kwarg=234)
    """
    if (len(args) == 1) and not kwargs:
        return _assert_raises_context_manager(args[0])
    else:
        return _assert_raises(*args, **kwargs)


def assert_raises_and_contains(expected_exception_class, strings, callable_obj, *args, **kwargs):
    """Assert an exception is raised by passing in a callable and its
    arguments and that the string representation of the exception
    contains the case-insensetive list of passed in strings.

    Args
        strings -- can be a string or an iterable of strings
    """
    try:
        callable_obj(*args, **kwargs)
    except:
        _, e, _ = sys.exc_info()
        assert_isinstance(e, expected_exception_class)
        message = str(e).lower()
        if isinstance(strings, STRING_TYPE):
            strings = [strings]
        for string in strings:
            assert_in(string.lower(), message)
    else:
        assert_not_reached("No exception was raised (expected %s)" % expected_exception_class)


@contextlib.contextmanager
def _assert_raises_context_manager(exception_class):
    try:
        yield
    except:
        _, ex, _ = sys.exc_info()
        assert_isinstance(ex, exception_class)
    else:
        assert_not_reached("No exception was raised (expected %r)" %
                           exception_class)


def _assert_raises(exception_class, callable, *args, **kwargs):
    with _assert_raises_context_manager(exception_class):
        callable(*args, **kwargs)


def _diff_message(lhs, rhs):
    """If `lhs` and `rhs` are strings, return the a formatted message
    describing their differences. If they're not strings, describe the
    differences in their `repr()`s.

    NOTE: Only works well for strings not containing newlines.
    """
    lhs = repr(lhs) if not isinstance(lhs, STRING_TYPE) else lhs
    rhs = repr(rhs) if not isinstance(rhs, STRING_TYPE) else rhs

    return 'Diff:\nl: %s\nr: %s' % utils.highlight(lhs, rhs)


def assert_equal(lval, rval, message=None):
    """Assert that lval and rval are equal."""
    if message:
        assert lval == rval, message
    else:
        assert lval == rval, \
            "assertion failed: l == r\nl: %r\nr: %r\n\n%s" % \
                (lval, rval, _diff_message(lval, rval))

assert_equals = assert_equal


def assert_almost_equal(lval, rval, digits, message=None):
    """Assert that lval and rval, when rounded to the specified number of digits, are the same."""
    real_message = message or "%r !~= %r" % (lval, rval)
    assert round(lval, digits) == round(rval, digits), real_message


def assert_within_tolerance(lval, rval, tolerance, message=None):
    """Assert that the difference between the two values, as a fraction of the left value, is smaller than the tolerance specified.
    That is, abs(float(lval) - float(rval)) / float(lval) < tolerance"""
    real_message = message or "%r !~= %r" % (lval, rval)
    assert abs(float(lval) - float(rval)) / float(lval) < tolerance, real_message


def assert_not_equal(lval, rval, message=None):
    """Assert that lval and rval are unequal to each other."""
    assert lval != rval, message or 'assertion failed: %r != %r' % (lval, rval)


def assert_lt(lval, rval, message=None):
    """Assert that lval is less than rval."""
    assert lval < rval, message or 'assertion failed: %r < %r' % (lval, rval)


def assert_lte(lval, rval, message=None):
    """Assert that lval is less than or equal to rval"""
    assert lval <= rval, message or 'assertion failed: %r <= %r' % (lval, rval)


def assert_gt(lval, rval, message=None):
    """Assert that lval is greater than rval."""
    assert lval > rval, message or 'assertion failed: %r > %r' % (lval, rval)


def assert_gte(lval, rval, message=None):
    """Assert that lval is greater than or equal to rval"""
    assert lval >= rval, message or 'assertion failed: %r >= %r' % (lval, rval)


def assert_in_range(val, start, end, message=None, inclusive=False):
    """Assert that val is greater than start and less than end. If inclusive is true, val may be equal to start or end."""
    if inclusive:
        real_message = message or "! %s <= %r <= %r" % (start, val, end)
        assert start <= val <= end, real_message
    else: