# Licensed to the .NET Foundation under one or more agreements.
# The .NET Foundation licenses this file to you under the Apache 2.0 License.
# See the LICENSE file in the project root for more information.

import sys
import unittest

from iptest import IronPythonTestCase, run_test, is_cli, is_cpython

class SyntaxTests(IronPythonTestCase):

    def check_compile_error(self, code, msg, lineno):
        with self.assertRaises(SyntaxError) as cm:
            compile(code, "<testcase>", "exec")
        self.assertEqual(cm.exception.msg, msg)
        self.assertEqual(cm.exception.lineno, lineno)

    def test_no_binding_func(self):
        source = """if True:
            def foo():
                nonlocal x
                return x
            foo()
            """
        self.check_compile_error(source, "no binding for nonlocal 'x' found", 3)

    def test_no_binding_func_global(self):
        source = """if True:
            x = 1
            def foo():
                nonlocal x
                return x
            foo()
            """
        self.check_compile_error(source, "no binding for nonlocal 'x' found", 4)

    def test_no_binding_class(self):
        source = """if True:
            class Foo():
                x = 1
                class Bar():
                    nonlocal x
                    def f(self):
                        return x
            """
        self.check_compile_error(source, "no binding for nonlocal 'x' found", 5)

    def test_global_nonlocal(self):
        source = """if True:
            def foo():
                global x    # CPython's error location
                nonlocal x  # IronPython's error location
                return x
            foo()
            """
        self.check_compile_error(source, "name 'x' is nonlocal and global", 4 if is_cli else 3)

    def test_local_global_nonlocal(self):
        source = """if True:
            def foo():
                x = 2 # x local in foo
                def bar():
                    def gek():
                        nonlocal x # sees global x in bar before x from foo
                        return x
                    global x # masks local x in outer scope foo
                    x = 3 # global x
                    return x, gek()
                return x, bar()
            foo()
            """
        self.check_compile_error(source, "no binding for nonlocal 'x' found", 6)

    def test_nonlocal_global(self):
        source = """if True:
            def foo():
                nonlocal x  # CPython's error location
                global x    # IronPython's error location
                return x
            foo()
            """
        self.check_compile_error(source, "name 'x' is nonlocal and global", 4 if is_cli else 3)

    def test_missing_nonlocal(self):
        source = """if True:
            def foo(x):
                def bar():
                    nonlocal y
                    return x
                return bar()
            foo(1)
            """
        self.check_compile_error(source, "no binding for nonlocal 'y' found", 4)

    @unittest.skipIf(is_cpython and sys.version_info <= (3,6), "CPython 3.4, 3.5 issues SyntaxWarning for this case")
    def test_prior_assignment(self):
        source = """if True:
            def foo(x):
                def bar():
                    x = 0
                    nonlocal x
                    return x
                return bar()
            foo(1)
            """
        self.check_compile_error(source, "name 'x' is assigned to before nonlocal declaration", 5)

    @unittest.skipIf(is_cpython and sys.version_info <= (3,6), "CPython 3.4, 3.5 issues SyntaxWarning for this case")
    def test_prior_use(self):
        source = """if True:
            def foo(x):
                def bar():
                    y = x
                    nonlocal x
                    return x
                return bar()
            foo(1)
            """
        self.check_compile_error(source, "name 'x' is used prior to nonlocal declaration", 5)

    @unittest.skipIf(is_cpython and sys.version_info <= (3,6), "CPython 3.4, 3.5 issues SyntaxWarning for this case")
    def test_prior_assignment_and_use(self):
        source = """if True:
            def foo(x):
                def bar():
                    class x: pass  # assignment
                    x = 0          # assignment
                    x += 1         # use
                    nonlocal x
                    return x
                return bar()
            foo(1)
            """
        self.check_compile_error(source, "name 'x' is assigned to before nonlocal declaration", 7)

    @unittest.skipIf(is_cpython and sys.version_info <= (3,6), "CPython 3.4, 3.5 issues SyntaxWarning for this case")
    def test_prior_use_and_assignment(self):
        source = """if True:
            def foo(x):
                def bar():
                    x += 1         # use
                    class x: pass  # assignment
                    x = 0          # assignment
                    nonlocal x
                    return x
                return bar()
            foo(1)
            """
        self.check_compile_error(source, "name 'x' is assigned to before nonlocal declaration", 7)

    @unittest.skipIf(is_cpython and sys.version_info <= (3,6), "CPython 3.4, 3.5 issues SyntaxWarning for this case")
    def test_prior_call_and_assignment(self):
        source = """if True:
            def foo(x):
                def bar():
                    x()            # use
                    class x: pass  # assignment
                    x = 0          # assignment
                    nonlocal x
                    return x
                return bar()
            foo(int)
            """
        if is_cli:
            self.check_compile_error(source, "name 'x' is assigned to before nonlocal declaration", 7)
        else:
            self.check_compile_error(source, "name 'x' is used prior to nonlocal declaration", 7)

class FunctionalTests(IronPythonTestCase):

    def test_local_scope_nonlocal(self):
        # Test that nonlocal declarations are limited to a local scope (do not propagate to inner scopes)
        def foo():
            x = 1 # local in foo
            def bar():
                nonlocal x # from foo
                x = 2
                self.assertEqual(x, 2)
                def gek():
                    # x is a readable reference to foo.<locals>.x
                    self.assertEqual(x, 2)
                    def lee():
                        nonlocal x # from foo
                        x = 3 # modifies foo.<locals>.x
                        self.assertEqual(x, 3)
                        def kao():
                            x = 4 # local in kao
                            self.assertEqual(x, 4)
                        kao()
                        self.assertEqual(x, 3) # not changed by kao
                    lee()
                    self.assertEqual(x, 3) # changed by lee
                gek()
                self.assertEqual(x, 3) # changed by lee
            bar()
            self.assertEqual(x, 3) # changed by lee
        foo()

    def test_nonlocal_del(self):
        # Test that a nonlocal does not rebind to an unshadowed variable after del
        def foo():
            x1, x2 = 'foo:x1', 'foo:x2' # local in foo
            x3 = 'foo:x3'
            def bar():
                with self.assertRaises(UnboundLocalError) as cm:
                    del x3 # x3 becomes local in bar but unassigned
                self.assertEqual(cm.exception.args[0], "local variable 'x3' referenced before assignment")

                with self.assertRaises(NameError) as cm:
                    dummy = x4 # x4 is local in foo but unassigned
                self.assertEqual(cm.exception.args[0], "free variable 'x4' referenced before assignment in enclosing scope")

                x1, x2 = 'bar:x1', 'bar:x2' # local in bar, shadowing foo

                def gek():
                    nonlocal x1, x2, x3 # from bar
                    self.assertEqual(x1, 'bar:x1')
                    self.assertEqual(x2, 'bar:x2')

                    x1, x2 = 'gek:x1', 'gek:x2' # reassigned locals in bar
                    del x1 # deletes a local in bar
                    with self.assertRaises(NameError) as cm:
                        del x1 # x1 in bar is already deleted
                    self.assertEqual(cm.exception.args[0], "free variable 'x1' referenced before assignment in enclosing scope")

                    del x2 # deletes a local in bar
                    x2 = 'gek:x2+' # reassigns a variable in bar, bringing it back to life

                    with self.assertRaises(NameError) as cm:
                        dummy = x3 # x3 in bar is not yet assigned
                    self.assertEqual(cm.exception.args[0], "free variable 'x3' referenced before assignment in enclosing scope")
                gek()

                x3 = 'bar:x3' # finally x3 is assigned and declared local in bar

                with self.assertRaises(UnboundLocalError) as cm:
                    dummy = x1 # x1 is already deleted by gek
                self.assertEqual(cm.exception.args[0], "local variable 'x1' referenced before assignment")
                with self.assertRaises(UnboundLocalError) as cm:
                    del x1 # x1 is already deleted by gek
                self.assertEqual(cm.exception.args[0], "local variable 'x1' referenced before assignment")

                self.assertEqual(x2, 'gek:x2+') # killed and resurrected by gek
            bar()

            self.assertEqual(x1, 'foo:x1') # unchanged
            self.assertEqual(x2, 'foo:x2') # unchanged
            self.assertEqual(x3, 'foo:x3') # unchanged
            x4 = 'foo:x4' # made local in foo
        foo()

    def test_class_scope(self):
        x = 'func'
        class Foo():
            x = 'class'
            class Bar():
                nonlocal x
                def f(self):
                    return x
            def get_bar(self):
                return self.Bar()

        self.assertEqual(Foo.Bar().f(), 'func')
        self.assertEqual(Foo().get_bar().f(), 'func')

    def test_nonlocal_class_del(self):
        def foo():
            class Bar():
                def del_bar(self):
                    nonlocal Bar
                    del Bar
                def get_bar(self):
                    return Bar()
            bar = Bar()
            return bar.get_bar, bar.del_bar

        get_bar, del_bar = foo()
        # str(get_bar()) produces something like
        # <__main__.FunctionalTests.test_nonlocal_class_del.<locals>.foo.<locals>.Bar object at 0x000002426EEEE908>
        self.assertEqual(str(get_bar())[1:].split()[0].split('.')[-1], 'Bar') # get_bar() works
        self.assertEqual(del_bar(), None) # delete class Bar
        with self.assertRaises(NameError) as cm:
            get_bar() # cannot instantiate a nonexistent class
        self.assertEqual(cm.exception.args[0], "free variable 'Bar' referenced before assignment in enclosing scope")

    @unittest.skipIf(is_cli, "https://github.com/IronLanguages/ironpython3/issues/30")
    def test_nonlocal_names(self):
        def foo():
            x = 'foo:x' # local in foo
            def bar():
                nonlocal x # from foo
                class x(): # reassigns foo.<locals>.x to bar.<locals>.x
                    def f(self):
                        return x()
                self.assertRegex(str(x().f()), 
                    r"^<%s\.FunctionalTests\.test_nonlocal_names\.<locals>\.foo.<locals>.bar.<locals>.x object at 0x[0-9A-F]+>$" % __name__)
            bar()
            self.assertEqual(str(x), 
                "<class '%s.FunctionalTests.test_nonlocal_names.<locals>.foo.<locals>.bar.<locals>.x'>" % __name__)
            bar_x = x
            def gek():
                nonlocal x, x # from foo
                x = 'gek:x' # reassigns foo.<locals>.x to a local string
            gek()
            self.assertEqual(str(x), 'gek:x') # reasigned by gek
            self.assertEqual(str(bar_x), # maintains bar.<locals>.x
                "<class '%s.FunctionalTests.test_nonlocal_names.<locals>.foo.<locals>.bar.<locals>.x'>" % __name__)
            # bar_x.f sees x from foo, not class x from bar
            self.assertIsInstance(bar_x(), object)
            self.assertRaises(TypeError, bar_x().f)
        foo()

    def test_nonlocal_import(self):
        maxunicode = 0
        some_number = 0
        def foo():
            nonlocal maxunicode, some_number
            from sys import maxunicode
            from sys import maxsize as some_number
        foo()
        self.assertEqual(maxunicode, 0xFFFF if is_cli else 0x10FFFF)
        self.assertGreaterEqual(some_number, 0x7FFFFFFF)

run_test(__name__)