"""Tests for the token-based transformers in IPython.core.inputtransformer2

Line-based transformers are the simpler ones; token-based transformers are
more complex. See test_inputtransformer2_line for tests for line-based
transformations.
"""

import platform
import string
import sys
from textwrap import dedent

import pytest

from IPython.core import inputtransformer2 as ipt2
from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line

MULTILINE_MAGIC = (
    """\
a = f()
%foo \\
bar
g()
""".splitlines(
        keepends=True
    ),
    (2, 0),
    """\
a = f()
get_ipython().run_line_magic('foo', ' bar')
g()
""".splitlines(
        keepends=True
    ),
)

INDENTED_MAGIC = (
    """\
for a in range(5):
    %ls
""".splitlines(
        keepends=True
    ),
    (2, 4),
    """\
for a in range(5):
    get_ipython().run_line_magic('ls', '')
""".splitlines(
        keepends=True
    ),
)

CRLF_MAGIC = (
    ["a = f()\n", "%ls\r\n", "g()\n"],
    (2, 0),
    ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"],
)

MULTILINE_MAGIC_ASSIGN = (
    """\
a = f()
b = %foo \\
  bar
g()
""".splitlines(
        keepends=True
    ),
    (2, 4),
    """\
a = f()
b = get_ipython().run_line_magic('foo', '   bar')
g()
""".splitlines(
        keepends=True
    ),
)

MULTILINE_SYSTEM_ASSIGN = (
    """\
a = f()
b = !foo \\
  bar
g()
""".splitlines(
        keepends=True
    ),
    (2, 4),
    """\
a = f()
b = get_ipython().getoutput('foo    bar')
g()
""".splitlines(
        keepends=True
    ),
)

#####

MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = (
    """\
def test():
  for i in range(1):
    print(i)
  res =! ls
""".splitlines(
        keepends=True
    ),
    (4, 7),
    """\
def test():
  for i in range(1):
    print(i)
  res =get_ipython().getoutput(\' ls\')
""".splitlines(
        keepends=True
    ),
)

######

AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n'])

AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n'])

AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"])

SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"])

DETAILED_HELP = (
    ["foo??\n"],
    (1, 0),
    ["get_ipython().run_line_magic('pinfo2', 'foo')\n"],
)

MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"])

HELP_IN_EXPR = (
    ["a = b + c?\n"],
    (1, 0),
    ["get_ipython().run_line_magic('pinfo', 'c')\n"],
)

HELP_CONTINUED_LINE = (
    """\
a = \\
zip?
""".splitlines(
        keepends=True
    ),
    (1, 0),
    [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
)

HELP_MULTILINE = (
    """\
(a,
b) = zip?
""".splitlines(
        keepends=True
    ),
    (1, 0),
    [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
)

HELP_UNICODE = (
    ["π.foo?\n"],
    (1, 0),
    ["get_ipython().run_line_magic('pinfo', 'π.foo')\n"],
)


def null_cleanup_transformer(lines):
    """
    A cleanup transform that returns an empty list.
    """
    return []


def test_check_make_token_by_line_never_ends_empty():
    """
    Check that not sequence of single or double characters ends up leading to en empty list of tokens
    """
    from string import printable

    # Most of the two-character combinations below are missing a line
    # ending, which `make_tokens_by_line` deliberately warns about; assert
    # on that warning instead of letting it leak into the test output.
    with pytest.warns(UserWarning, match="do not have lineending markers"):
        for c in printable:
            assert make_tokens_by_line(c)[-1] != []
            for k in printable:
                assert make_tokens_by_line(c + k)[-1] != []


def check_find(transformer, case, match=True):
    sample, expected_start, _ = case
    tbl = make_tokens_by_line(sample)
    res = transformer.find(tbl)
    if match:
        # start_line is stored 0-indexed, expected values are 1-indexed
        assert (res.start_line + 1, res.start_col) == expected_start
        return res
    else:
        assert res is None


def check_transform(transformer_cls, case):
    lines, start, expected = case
    transformer = transformer_cls(start)
    assert transformer.transform(lines) == expected


def test_continued_line():
    lines = MULTILINE_MAGIC_ASSIGN[0]
    assert ipt2.find_end_of_continued_line(lines, 1) == 2

    assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo    bar"


def test_find_assign_magic():
    check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
    check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
    check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)


def test_transform_assign_magic():
    check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)


def test_find_assign_system():
    check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
    check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
    check_find(ipt2.SystemAssign, (["a =  !ls\n"], (1, 5), None))
    check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
    check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)


def test_transform_assign_system():
    check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
    check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)


def test_find_magic_escape():
    check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
    check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
    check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)


def test_transform_magic_escape():
    check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
    check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
    check_transform(ipt2.EscapedCommand, CRLF_MAGIC)


@pytest.mark.parametrize("case", [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN])
def test_find_autocalls(case):
    check_find(ipt2.EscapedCommand, case)


@pytest.mark.parametrize("case", [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN])
def test_transform_autocall(case):
    check_transform(ipt2.EscapedCommand, case)


def test_find_help():
    for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
        check_find(ipt2.HelpEnd, case)

    tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
    assert tf.q_line == 1
    assert tf.q_col == 3

    tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
    assert tf.q_line == 1
    assert tf.q_col == 8

    # ? in a comment does not trigger help
    check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
    # Nor in a string
    check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)


def test_transform_help():
    tf = ipt2.HelpEnd((1, 0), (1, 9))
    assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2]

    tf = ipt2.HelpEnd((1, 0), (2, 3))
    assert tf.transform(HELP_CONTINUED_LINE[0]) == HELP_CONTINUED_LINE[2]

    tf = ipt2.HelpEnd((1, 0), (2, 8))
    assert tf.transform(HELP_MULTILINE[0]) == HELP_MULTILINE[2]

    tf = ipt2.HelpEnd((1, 0), (1, 0))
    assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2]


def test_find_assign_op_dedent():
    """
    be careful that empty token like dedent are not counted as parens
    """

    class Tk:
        def __init__(self, s):
            self.string = s

    assert _find_assign_op([Tk(s) for s in ("", "a", "=", "b")]) == 2
    assert (
        _find_assign_op([Tk(s) for s in ("", "(", "a", "=", "b", ")", "=", "5")]) == 6
    )


extra_closing_paren_param = (
    pytest.param("(\n))", "invalid", None)
    if sys.version_info >= (3, 12)
    else pytest.param("(\n))", "incomplete", 0)
)
examples = [
    pytest.param("a = 1", "complete", None),
    pytest.param("for a in range(5):", "incomplete", 4),
    pytest.param("for a in range(5):\n    if a > 0:", "incomplete", 8),
    pytest.param("raise = 2", "invalid", None),
    pytest.param("a = [1,\n2,", "incomplete", 0),
    extra_closing_paren_param,
    pytest.param("\\\r\n", "incomplete", 0),
    pytest.param("a = '''\n   hi", "incomplete", 3),
    pytest.param("def a():\n x=1\n global x", "invalid", None),
    pytest.param("a \\ ", "invalid", None),  # Nothing allowed after backslash,
    pytest.param("1\\\n+2", "complete", None),
]


@pytest.mark.parametrize("code, expected, number", examples)
def test_check_complete_param(code, expected, number):
    cc = ipt2.TransformerManager().check_complete
    assert cc(code) == (expected, number)


@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
def test_check_complete():
    cc = ipt2.TransformerManager().check_complete

    example = dedent(
        """
        if True:
            a=1"""
    )

    assert cc(example) == ("incomplete", 4)
    assert cc(example + "\n") == ("complete", None)
    assert cc(example + "\n    ") == ("complete", None)

    # no need to loop on all the letters/numbers.
    short = "12abAB" + string.printable[62:]
    for c in short:
        # test does not raise:
        cc(c)
        for k in short:
            cc(c + k)

    assert cc("def f():\n  x=0\n  \\\n  ") == ("incomplete", 2)


@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
@pytest.mark.parametrize(
    "value, expected",
    [
        ('''def foo():\n    """''', ("incomplete", 4)),
        ("""async with example:\n    pass""", ("incomplete", 4)),
        ("""async with example:\n    pass\n    """, ("complete", None)),
    ],
)
def test_check_complete_II(value, expected):
    """
    Test that multiple line strings are properly handled.

    Separate test function for convenience

    """
    cc = ipt2.TransformerManager().check_complete
    assert cc(value) == expected


@pytest.mark.parametrize(
    "value, expected",
    [
        (")", ("invalid", None)),
        ("]", ("invalid", None)),
        ("}", ("invalid", None)),
        (")(", ("invalid", None)),
        ("][", ("invalid", None)),
        ("}{", ("invalid", None)),
        ("]()(", ("invalid", None)),
        ("())(", ("invalid", None)),
        (")[](", ("invalid", None)),
        ("()](", ("invalid", None)),
    ],
)
def test_check_complete_invalidates_sunken_brackets(value, expected):
    """
    Test that a single line with more closing brackets than the opening ones is
    interpreted as invalid
    """
    cc = ipt2.TransformerManager().check_complete
    assert cc(value) == expected


def test_null_cleanup_transformer():
    manager = ipt2.TransformerManager()
    manager.cleanup_transforms.insert(0, null_cleanup_transformer)
    assert manager.transform_cell("") == ""


def test_side_effects_I():
    count = 0

    def counter(lines):
        nonlocal count
        count += 1
        return lines

    counter.has_side_effects = True

    manager = ipt2.TransformerManager()
    manager.cleanup_transforms.insert(0, counter)
    assert manager.check_complete("a=1\n") == ("complete", None)
    assert count == 0


def test_side_effects_II():
    count = 0

    def counter(lines):
        nonlocal count
        count += 1
        return lines

    counter.has_side_effects = True

    manager = ipt2.TransformerManager()
    manager.line_transforms.insert(0, counter)
    assert manager.check_complete("b=1\n") == ("complete", None)
    assert count == 0
