import unittest

from Aspidites._vendor.contracts import new_contract, check, Contract, contract
from Aspidites._vendor.contracts.library.extensions import identifier_expression

from .utils import check_contracts_fail, check_contracts_ok
from Aspidites._vendor.contracts.main_actual import Storage
from Aspidites._vendor.contracts.syntax import ParsingTmp

# The different patterns


def ok1(x):
    pass


def ok2(x):  # @UnusedVariable
    return True


def fail1(x):
    raise ValueError("message")


def fail2(x):  # @UnusedVariable
    return False


def invalid_callable1(x):  # @UnusedVariable
    return "ciao"


# generates a new name:
def cname():
    TestNewContract.counter += 1
    return "GeneratedContract%d" % TestNewContract.counter


class TestNewContract(unittest.TestCase):
    counter = 0

    # def setup(self):
    #    Storage.string2contract = {}

    def test_inverted_args(self):
        self.assertRaises(ValueError, new_contract, ok1, "list")

    def test_wrong_args(self):
        self.assertRaises(ValueError, new_contract, "my13", 2)

    def test_invalid_callable(self):
        self.assertRaises(ValueError, new_contract, "new", lambda: None)

    def test_parsing_error(self):
        self.assertRaises(ValueError, new_contract, "new", ">>")

    def test_parsing_error2(self):
        # parsing error (unknown spec)
        self.assertRaises(ValueError, new_contract, "new", "unknown")

    def test_invalid_names(self):
        # invalid names:
        alphabet = "A B C D E F G H I J K L M N O P Q R S T U W V X Y Z"
        for x in alphabet.split():
            self.assertRaises(ValueError, new_contract, x, "list")
            self.assertRaises(ValueError, new_contract, x.lower(), "list")
        self.assertRaises(ValueError, new_contract, "list", "list[N]")
        self.assertRaises(ValueError, new_contract, "2acdca", "list[N]")
        self.assertRaises(ValueError, new_contract, "_", "list[N]")

    def test_valid(self):
        c = new_contract("my_list", "list[2]")
        assert isinstance(c, Contract)
        check("tuple(my_list, my_list)", ([1, 2], [1, 2]))
        check_contracts_fail("tuple(my_list, my_list)", ([1, 2], [1, 2, 3]))

    def test_separate_context(self):
        new_contract("my_list2", "list[N]")
        check("tuple(my_list2, my_list2)", ([1, 2], [1, 2]))
        check("tuple(my_list2, my_list2)", ([1, 2], [1, 2, 3]))

    def test_renaming(self):
        self.assertNotEqual(ok1, ok2)
        new_contract("my7", ok1)
        self.assertRaises(ValueError, new_contract, "my7", ok2)

    def test_allow_renaming_if_equal1(self):
        new_contract("my8", ok1)
        new_contract("my8", ok1)

    def test_allow_renaming_if_equal2(self):
        new_contract("my8b", "list[3]")
        new_contract("my8b", "list[3]")

    def test_callable1(self):
        c = cname()
        new_contract(c, ok2)
        check("list(%s)" % c, [0])

    def test_callable2(self):
        c = cname()
        new_contract(c, ok2)
        check("list(%s)" % c, [0])

    def test_callable3(self):
        c = cname()
        new_contract(c, fail1)
        check_contracts_fail("list(%s)" % c, [0])

    def test_callable4(self):
        c = cname()
        new_contract(c, fail2)
        check_contracts_fail("list(%s)" % c, [0])

    def test_invalid_callable2(self):
        c = cname()
        new_contract(c, invalid_callable1)
        self.assertRaises(ValueError, check, "list(%s)" % c, [0])

    def test_other_pass(self):
        class Ex1(Exception):
            pass

        def invalid(x):
            raise Ex1()

        c = cname()
        new_contract(c, invalid)
        self.assertRaises(Ex1, check, "list(%s)" % c, [0])

    def test_callable(self):
        class MyTest_ok(object):
            def __call__(self, x):  # @UnusedVariable
                return True

        o = MyTest_ok()
        assert o("value") == True
        new_contract(cname(), o)

    def test_callable_old_style(self):
        class MyTest_ok:
            def __call__(self, x):  # @UnusedVariable
                return True

        o = MyTest_ok()
        assert o("value") == True
        new_contract(cname(), o)

    def test_class_as_contract1(self):
        # This should be interpreted as a type
        # init(x,y) so it is not mistaken for a valid callable
        class NewStyleClass(object):
            def __init__(self, x, y):  # @UnusedVariable
                pass

        new_contract(cname(), NewStyleClass)

    def test_class_as_contract2(self):
        # old sytle class
        class OldStyleClass:
            def __init__(self, x, y):  # @UnusedVariable
                pass

        new_contract(cname(), OldStyleClass)

    def test_class_as_contract3(self):
        class NewStyleClass(object):
            def __init__(self, x, y):  # @UnusedVariable
                pass

        @contract(x=NewStyleClass)
        def f(x):
            pass

    def test_class_as_contract4(self):
        class OldStyleClass:
            def __init__(self, x, y):  # @UnusedVariable
                pass

        @contract(x=OldStyleClass)
        def f(x):
            pass

    def test_callable_5(self):
        class MyTest_ok(object):
            def f(self, x):  # @UnusedVariable
                return True

        o = MyTest_ok()
        assert o.f("value") == True
        new_contract(cname(), o.f)

    # removed after checking in class methods
    #    def test_callable_invalid(self):
    #        class MyTest_fail(object):
    #            def __call__(self, x, y):  # @UnusedVariable
    #                return True
    #
    #        self.assertRaises(ValueError, new_contract, cname(), MyTest_fail())

    def test_lambda_2(self):
        new_contract(cname(), lambda x: True)  # @UnusedVariable
        new_contract(cname(), lambda x: None)  # @UnusedVariable

    def test_lambda_invalid(self):
        f = lambda: True  # @UnusedVariable
        self.assertRaises(ValueError, new_contract, cname(), f)

    def test_idioms(self):
        Storage.string2contract = {}  # TODO remove this hack
        color = new_contract("color", "list[3](number,>=0,<=1)")
        # Make sure we got it right
        color.check(
            [0, 0, 0],
        )
        color.check(
            [0, 0, 1],
        )
        color.fail([0, 0])
        color.fail([0, 0, 2])

        self.assertRaises(ValueError, color.fail, [0, 0, 1])

        # Now use ``color`` in other contracts.
        @contract
        def fill_area(inside, border):
            """Fill the area inside the current figure.

            :type border: color
            :type inside: color
            """
            pass

        @contract
        def fill_gradient(colors):
            """Use a gradient to fill the area.

            :type colors: list(color)
            """
            pass

    def test_as_decorator(self):
        @new_contract
        def even(x):
            return x % 2 == 0

        from Aspidites._vendor.contracts import parse

        p = parse("even")
        p.check(
            2,
        )
        p.check(
            4,
        )
        p.fail(3)
        p.check(
            2.0,
        )

    def test_as_decorator_multiple(self):
        @new_contract
        @contract(x="int")
        def even2(x):
            return x % 2 == 0

        from Aspidites._vendor.contracts import parse

        p = parse("even2")
        p.check(
            2,
        )
        p.fail(3)
        p.fail(2.0)  # now fails

    def test_types_as_contracts(self):
        c = cname()
        new_contract(c, str)
        check_contracts_ok(c, "")
        check_contracts_fail(c, 1)

    def test_types_as_contracts2(self):
        c = cname()
        new_contract(c, int)
        check_contracts_ok(c, 1)
        check_contracts_fail(c, "")

    def test_well_recognized(self):
        class OldStyleClass:
            def __init__(self, x, y):  # @UnusedVariable
                pass

        assert isinstance(OldStyleClass, type)

        class NewStyleClass:
            def __init__(self, x, y):  # @UnusedVariable
                pass

        assert isinstance(NewStyleClass, type)

    def test_as_decorator_with_args(self):
        from Aspidites._vendor.contracts import parse

        @new_contract
        def greater_than(value, thresh):
            return value > thresh

        p = parse("greater_than(5)")
        p.check(
            6,
        )
        p.fail(5)

    def test_as_decorator_with_kwargs(self):
        from Aspidites._vendor.contracts import parse

        @new_contract
        def less_than(value, thresh=1):
            return value < thresh

        p = parse("less_than(thresh=2)")
        p.check(
            1,
        )
        p.fail(2)

    def test_as_decorator_with_args_and_kwargs(self):
        from Aspidites._vendor.contracts import parse

        @new_contract
        def less_than_all(value, a, b, c=1, d=5):
            return value < min(a, b, c, d)

        p = parse("less_than_all(3, 4, c=5)")
        p.check(
            2,
        )
        p.fail(3)

    def test_capital_name1(self):
        # some problems with identifiers starting with capitals
        new_contract("SEn", "int")
        check_contracts_ok("SEn", 1)
        check_contracts_fail("SEn", 2.0)

    def test_capital_name2(self):
        new_contract("Sen", "int")
        check_contracts_ok("Sen", 1)
        check_contracts_fail("Sen", 2.0)


examples_valid = [
    "aa",
    "a_",
    "a2",
    "a_2",
    "list2",
    "dict2",
    "int2",
    "float2",
    "point2",
    "A2",
    "array2",
    "unit_length",
    "SE2",
    "SE3",
    "S1",
    "S2",
    "axis_angle",
]

for k in ParsingTmp.keywords:
    examples_valid.append("%s2" % k)
    examples_valid.append("%s_" % k)


def check_valid_identifier(e):
    check_valid_identifier.__dict__["description"] = "check_valid_identifier(%r)" % e

    identifier_expression.parseString(e, parseAll=True)  # @UndefinedVariable

    new_contract(e, "*")

    check(e, 42)


# def test_valid_identifiers():
#
#     for e in examples_valid:
#         yield check_valid_identifier, e
