import pytest
from textwrap import dedent

from parso import load_grammar, ParserSyntaxError
from parso.python.tokenize import tokenize


@pytest.fixture
def grammar():
    return load_grammar(version='3.8')


@pytest.mark.parametrize(
    'code', [
        # simple cases
        'f"{1}"',
        'f"""{1}"""',
        'f"{foo} {bar}"',

        # empty string
        'f""',
        'f""""""',

        # empty format specifier is okay
        'f"{1:}"',

        # use of conversion options
        'f"{1!a}"',
        'f"{1!a:1}"',

        # format specifiers
        'f"{1:1}"',
        'f"{1:1.{32}}"',
        'f"{1::>4}"',
        'f"{x:{y}}"',
        'f"{x:{y:}}"',
        'f"{x:{y:1}}"',

        # Escapes
        'f"{{}}"',
        'f"{{{1}}}"',
        'f"{{{1}"',
        'f"1{{2{{3"',
        'f"}}"',

        # New Python 3.8 syntax f'{a=}'
        'f"{a=}"',
        'f"{a()=}"',

        # multiline f-string
        'f"""abc\ndef"""',
        'f"""abc{\n123}def"""',

        # a line continuation inside of an fstring_string
        'f"abc\\\ndef"',
        'f"\\\n{123}\\\n"',

        # a line continuation inside of an fstring_expr
        'f"{\\\n123}"',

        # a line continuation inside of an format spec
        'f"{123:.2\\\nf}"',

        # some unparenthesized syntactic structures
        'f"{*x,}"',
        'f"{*x, *y}"',
        'f"{x, *y}"',
        'f"{*x, y}"',
        'f"{x for x in [1]}"',

        # named unicode characters
        'f"\\N{BULLET}"',
        'f"\\N{FLEUR-DE-LIS}"',
        'f"\\N{NO ENTRY}"',
        'f"Combo {expr} and \\N{NO ENTRY}"',
        'f"\\N{NO ENTRY} and {expr}"',
        'f"\\N{no entry}"',
        'f"\\N{SOYOMBO LETTER -A}"',
        'f"\\N{DOMINO TILE HORIZONTAL-00-00}"',
        'f"""\\N{NO ENTRY}"""',
    ]
)
def test_valid(code, grammar):
    module = grammar.parse(code, error_recovery=False)
    fstring = module.children[0]
    assert fstring.type == 'fstring'
    assert fstring.get_code() == code


@pytest.mark.parametrize(
    'code', [
        # an f-string can't contain unmatched curly braces
        'f"}"',
        'f"{"',
        'f"""}"""',
        'f"""{"""',

        # invalid conversion characters
        'f"{1!{a}}"',
        'f"{1=!{a}}"',
        'f"{!{a}}"',

        # The curly braces must contain an expression
        'f"{}"',
        'f"{:}"',
        'f"{:}}}"',
        'f"{:1}"',
        'f"{!:}"',
        'f"{!}"',
        'f"{!a}"',

        # invalid (empty) format specifiers
        'f"{1:{}}"',
        'f"{1:{:}}"',

        # a newline without a line continuation inside a single-line string
        'f"abc\ndef"',

        # various named unicode escapes that aren't name-shaped
        'f"\\N{ BULLET }"',
        'f"\\N{NO   ENTRY}"',
        'f"""\\N{NO\nENTRY}"""',
    ]
)
def test_invalid(code, grammar):
    with pytest.raises(ParserSyntaxError):
        grammar.parse(code, error_recovery=False)

    # It should work with error recovery.
    grammar.parse(code, error_recovery=True)


@pytest.mark.parametrize(
    ('code', 'positions'), [
        # 2 times 2, 5 because python expr and endmarker.
        ('f"}{"', [(1, 0), (1, 2), (1, 3), (1, 4), (1, 5)]),
        ('f" :{ 1 : } "', [(1, 0), (1, 2), (1, 4), (1, 6), (1, 8), (1, 9),
                           (1, 10), (1, 11), (1, 12), (1, 13)]),
        ('f"""\n {\nfoo\n }"""', [(1, 0), (1, 4), (2, 1), (3, 0), (4, 1),
                                  (4, 2), (4, 5)]),
        ('f"\\N{NO ENTRY} and {expr}"', [(1, 0), (1, 2), (1, 19), (1, 20),
                                         (1, 24), (1, 25), (1, 26)]),
    ]
)
def test_tokenize_start_pos(code, positions):
    tokens = list(tokenize(code, version_info=(3, 6)))
    assert positions == [p.start_pos for p in tokens]


@pytest.mark.parametrize(
    'code', [
        dedent("""\
            f'''s{
               str.uppe
            '''
            """),
        'f"foo',
        'f"""foo',
        'f"abc\ndef"',
    ]
)
def test_roundtrip(grammar, code):
    tree = grammar.parse(code)
    assert tree.get_code() == code