import re
import unittest

from mako import exceptions
from mako.codegen import _FOR_LOOP
from mako.lookup import TemplateLookup
from mako.runtime import LoopContext
from mako.runtime import LoopStack
from mako.template import Template
from mako.testing.assertions import assert_raises_message
from mako.testing.fixtures import TemplateTest
from mako.testing.helpers import flatten_result


class TestLoop(unittest.TestCase):
    def test__FOR_LOOP(self):
        for statement, target_list, expression_list in (
            ("for x in y:", "x", "y"),
            ("for x, y in z:", "x, y", "z"),
            ("for (x,y) in z:", "(x,y)", "z"),
            ("for ( x, y, z) in a:", "( x, y, z)", "a"),
            ("for x in [1, 2, 3]:", "x", "[1, 2, 3]"),
            ('for x in "spam":', "x", '"spam"'),
            (
                "for k,v in dict(a=1,b=2).items():",
                "k,v",
                "dict(a=1,b=2).items()",
            ),
            (
                "for x in [y+1 for y in [1, 2, 3]]:",
                "x",
                "[y+1 for y in [1, 2, 3]]",
            ),
            (
                "for ((key1, val1), (key2, val2)) in pairwise(dict.items()):",
                "((key1, val1), (key2, val2))",
                "pairwise(dict.items())",
            ),
            (
                "for (key1, val1), (key2, val2) in pairwise(dict.items()):",
                "(key1, val1), (key2, val2)",
                "pairwise(dict.items())",
            ),
        ):
            match = _FOR_LOOP.match(statement)
            assert match and match.groups() == (target_list, expression_list)

    def test_no_loop(self):
        template = Template(
            """% for x in 'spam':
${x}
% endfor"""
        )
        code = template.code
        assert not re.match(r"loop = __M_loop._enter\(:", code), (
            "No need to "
            "generate a loop context if the loop variable wasn't accessed"
        )
        print(template.render())

    def test_loop_demo(self):
        template = Template(
            """x|index|reverse_index|first|last|cycle|even|odd
% for x in 'ham':
${x}|${loop.index}|${loop.reverse_index}|${loop.first}|"""
            """${loop.last}|${loop.cycle('even', 'odd')}|"""
            """${loop.even}|${loop.odd}
% endfor"""
        )
        expected = [
            "x|index|reverse_index|first|last|cycle|even|odd",
            "h|0|2|True|False|even|True|False",
            "a|1|1|False|False|odd|False|True",
            "m|2|0|False|True|even|True|False",
        ]
        code = template.code
        assert "loop = __M_loop._enter(" in code, (
            "Generated a loop context since " "the loop variable was accessed"
        )
        rendered = template.render()
        print(rendered)
        for line in expected:
            assert line in rendered, (
                "Loop variables give information about "
                "the progress of the loop"
            )

    def test_nested_loops(self):
        template = Template(
            """% for x in 'ab':
${x} ${loop.index} <- start in outer loop
% for y in [0, 1]:
${y} ${loop.index} <- go to inner loop
% endfor
${x} ${loop.index} <- back to outer loop
% endfor"""
        )
        rendered = template.render()
        expected = [
            "a 0 <- start in outer loop",
            "0 0 <- go to inner loop",
            "1 1 <- go to inner loop",
            "a 0 <- back to outer loop",
            "b 1 <- start in outer loop",
            "0 0 <- go to inner loop",
            "1 1 <- go to inner loop",
            "b 1 <- back to outer loop",
        ]
        for line in expected:
            assert line in rendered, (
                "The LoopStack allows you to take "
                "advantage of the loop variable even in embedded loops"
            )

    def test_parent_loops(self):
        template = Template(
            """% for x in 'ab':
${x} ${loop.index} <- outer loop
% for y in [0, 1]:
${y} ${loop.index} <- inner loop
${x} ${loop.parent.index} <- parent loop
% endfor
${x} ${loop.index} <- outer loop
% endfor"""
        )
        code = template.code
        rendered = template.render()
        expected = [
            "a 0 <- outer loop",
            "a 0 <- parent loop",
            "b 1 <- outer loop",
            "b 1 <- parent loop",
        ]
        for line in expected:
            print(code)
            assert line in rendered, (
                "The parent attribute of a loop gives "
                "you the previous loop context in the stack"
            )

    def test_out_of_context_access(self):
        template = Template("""${loop.index}""")
        assert_raises_message(
            exceptions.RuntimeException,
            "No loop context is established",
            template.render,
        )


class TestLoopStack(unittest.TestCase):
    def setUp(self):
        self.stack = LoopStack()
        self.bottom = "spam"
        self.stack.stack = [self.bottom]

    def test_enter(self):
        iterable = "ham"
        s = self.stack._enter(iterable)
        assert s is self.stack.stack[-1], (
            "Calling the stack with an iterable returns " "the stack"
        )
        assert iterable == self.stack.stack[-1]._iterable, (
            "and pushes the " "iterable on the top of the stack"
        )

    def test__top(self):
        assert self.bottom == self.stack._top, (
            "_top returns the last item " "on the stack"
        )

    def test__pop(self):
        assert len(self.stack.stack) == 1
        top = self.stack._pop()
        assert top == self.bottom
        assert len(self.stack.stack) == 0

    def test__push(self):
        assert len(self.stack.stack) == 1
        iterable = "ham"
        self.stack._push(iterable)
        assert len(self.stack.stack) == 2
        assert iterable is self.stack._top._iterable

    def test_exit(self):
        iterable = "ham"
        self.stack._enter(iterable)
        before = len(self.stack.stack)
        self.stack._exit()
        after = len(self.stack.stack)
        assert before == (after + 1), "Exiting a context pops the stack"


class TestLoopContext(unittest.TestCase):
    def setUp(self):
        self.iterable = [1, 2, 3]
        self.ctx = LoopContext(self.iterable)

    def test___len__(self):
        assert len(self.iterable) == len(self.ctx), (
            "The LoopContext is the " "same length as the iterable"
        )

    def test_index(self):
        expected = tuple(range(len(self.iterable)))
        actual = tuple(self.ctx.index for i in self.ctx)
        assert expected == actual, (
            "The index is consistent with the current " "iteration count"
        )

    def test_reverse_index(self):
        length = len(self.iterable)
        expected = tuple(length - i - 1 for i in range(length))
        actual = tuple(self.ctx.reverse_index for i in self.ctx)
        print(expected, actual)
        assert expected == actual, (
            "The reverse_index is the number of " "iterations until the end"
        )

    def test_first(self):
        expected = (True, False, False)
        actual = tuple(self.ctx.first for i in self.ctx)
        assert expected == actual, "first is only true on the first iteration"

    def test_last(self):
        expected = (False, False, True)
        actual = tuple(self.ctx.last for i in self.ctx)
        assert expected == actual, "last is only true on the last iteration"

    def test_even(self):
        expected = (True, False, True)
        actual = tuple(self.ctx.even for i in self.ctx)
        assert expected == actual, "even is true on even iterations"

    def test_odd(self):
        expected = (False, True, False)
        actual = tuple(self.ctx.odd for i in self.ctx)
        assert expected == actual, "odd is true on odd iterations"

    def test_cycle(self):
        expected = ("a", "b", "a")
        actual = tuple(self.ctx.cycle("a", "b") for i in self.ctx)
        assert expected == actual, "cycle endlessly cycles through the values"


class TestLoopFlags(TemplateTest):
    def test_loop_disabled_template(self):
        self._do_memory_test(
            """
            the loop: ${loop}
        """,
            "the loop: hi",
            template_args=dict(loop="hi"),
            filters=flatten_result,
            enable_loop=False,
        )

    def test_loop_disabled_lookup(self):
        l = TemplateLookup(enable_loop=False)
        l.put_string(
            "x",
            """
            the loop: ${loop}
        """,
        )

        self._do_test(
            l.get_template("x"),
            "the loop: hi",
            template_args=dict(loop="hi"),
            filters=flatten_result,
        )

    def test_loop_disabled_override_template(self):
        self._do_memory_test(
            """
            <%page enable_loop="True" />
            % for i in (1, 2, 3):
                ${i} ${loop.index}
            % endfor
        """,
            "1 0 2 1 3 2",
            template_args=dict(loop="hi"),
            filters=flatten_result,
            enable_loop=False,
        )

    def test_loop_disabled_override_lookup(self):
        l = TemplateLookup(enable_loop=False)
        l.put_string(
            "x",
            """
            <%page enable_loop="True" />
            % for i in (1, 2, 3):
                ${i} ${loop.index}
            % endfor
        """,
        )

        self._do_test(
            l.get_template("x"),
            "1 0 2 1 3 2",
            template_args=dict(loop="hi"),
            filters=flatten_result,
        )

    def test_loop_enabled_override_template(self):
        self._do_memory_test(
            """
            <%page enable_loop="True" />
            % for i in (1, 2, 3):
                ${i} ${loop.index}
            % endfor
        """,
            "1 0 2 1 3 2",
            template_args=dict(),
            filters=flatten_result,
        )

    def test_loop_enabled_override_lookup(self):
        l = TemplateLookup()
        l.put_string(
            "x",
            """
            <%page enable_loop="True" />
            % for i in (1, 2, 3):
                ${i} ${loop.index}
            % endfor
        """,
        )

        self._do_test(
            l.get_template("x"),
            "1 0 2 1 3 2",
            template_args=dict(),
            filters=flatten_result,
        )
