import copy
import itertools
import pickle

import pytest

from jinja2 import ChainableUndefined
from jinja2 import DebugUndefined
from jinja2 import StrictUndefined
from jinja2 import Template
from jinja2 import TemplateRuntimeError
from jinja2 import Undefined
from jinja2.runtime import LoopContext

TEST_IDX_TEMPLATE_STR_1 = (
    "[{% for i in lst|reverse %}(len={{ loop.length }},"
    " revindex={{ loop.revindex }}, index={{ loop.index }}, val={{ i }}){% endfor %}]"
)
TEST_IDX0_TEMPLATE_STR_1 = (
    "[{% for i in lst|reverse %}(len={{ loop.length }},"
    " revindex0={{ loop.revindex0 }}, index0={{ loop.index0 }}, val={{ i }})"
    "{% endfor %}]"
)


def test_loop_idx():
    t = Template(TEST_IDX_TEMPLATE_STR_1)
    lst = [10]
    excepted_render = "[(len=1, revindex=1, index=1, val=10)]"
    assert excepted_render == t.render(lst=lst)


def test_loop_idx0():
    t = Template(TEST_IDX0_TEMPLATE_STR_1)
    lst = [10]
    excepted_render = "[(len=1, revindex0=0, index0=0, val=10)]"
    assert excepted_render == t.render(lst=lst)


def test_loopcontext0():
    in_lst = []
    lc = LoopContext(reversed(in_lst), None)
    assert lc.length == len(in_lst)


def test_loopcontext1():
    in_lst = [10]
    lc = LoopContext(reversed(in_lst), None)
    assert lc.length == len(in_lst)


def test_loopcontext2():
    in_lst = [10, 11]
    lc = LoopContext(reversed(in_lst), None)
    assert lc.length == len(in_lst)


def test_iterator_not_advanced_early():
    t = Template("{% for _, g in gs %}{{ loop.index }} {{ g|list }}\n{% endfor %}")
    out = t.render(
        gs=itertools.groupby([(1, "a"), (1, "b"), (2, "c"), (3, "d")], lambda x: x[0])
    )
    # groupby groups depend on the current position of the iterator. If
    # it was advanced early, the lists would appear empty.
    assert out == "1 [(1, 'a'), (1, 'b')]\n2 [(2, 'c')]\n3 [(3, 'd')]\n"


def test_mock_not_pass_arg_marker():
    """If a callable class has a ``__getattr__`` that returns True-like
    values for arbitrary attrs, it should not be incorrectly identified
    as a ``pass_context`` function.
    """

    class Calc:
        def __getattr__(self, item):
            return object()

        def __call__(self, *args, **kwargs):
            return len(args) + len(kwargs)

    t = Template("{{ calc() }}")
    out = t.render(calc=Calc())
    # Would be "1" if context argument was passed.
    assert out == "0"


_undefined_types = (Undefined, ChainableUndefined, DebugUndefined, StrictUndefined)


@pytest.mark.parametrize("undefined_type", _undefined_types)
def test_undefined_copy(undefined_type):
    undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
    copied = copy.copy(undef)

    assert copied is not undef
    assert copied._undefined_hint is undef._undefined_hint
    assert copied._undefined_obj is undef._undefined_obj
    assert copied._undefined_name is undef._undefined_name
    assert copied._undefined_exception is undef._undefined_exception


@pytest.mark.parametrize("undefined_type", _undefined_types)
def test_undefined_deepcopy(undefined_type):
    undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
    copied = copy.deepcopy(undef)

    assert copied._undefined_hint is undef._undefined_hint
    assert copied._undefined_obj is not undef._undefined_obj
    assert copied._undefined_obj == undef._undefined_obj
    assert copied._undefined_name is undef._undefined_name
    assert copied._undefined_exception is undef._undefined_exception


@pytest.mark.parametrize("undefined_type", _undefined_types)
def test_undefined_pickle(undefined_type):
    undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
    copied = pickle.loads(pickle.dumps(undef))

    assert copied._undefined_hint is not undef._undefined_hint
    assert copied._undefined_hint == undef._undefined_hint
    assert copied._undefined_obj is not undef._undefined_obj
    assert copied._undefined_obj == undef._undefined_obj
    assert copied._undefined_name is not undef._undefined_name
    assert copied._undefined_name == undef._undefined_name
    assert copied._undefined_exception is undef._undefined_exception
