"""
Test all things related to the ``jedi.api`` module.
"""

import os
from textwrap import dedent

import pytest
from pytest import raises
from parso import cache

from jedi import preload_module
from jedi.inference.gradual import typeshed
from test.helpers import test_dir, get_example_dir


def test_preload_modules():
    def check_loaded(*module_names):
        for grammar_cache in cache.parser_cache.values():
            if None in grammar_cache:
                break
        # Filter the typeshed parser cache.
        typeshed_cache_count = sum(
            1 for path in grammar_cache
            if path is not None and str(path).startswith(str(typeshed.TYPESHED_PATH))
        )
        # +1 for None module (currently used)
        assert len(grammar_cache) - typeshed_cache_count == len(module_names) + 1
        for i in module_names:
            assert [i in str(k) for k in grammar_cache.keys() if k is not None]

    old_cache = cache.parser_cache.copy()
    cache.parser_cache.clear()

    try:
        preload_module('sys')
        check_loaded()  # compiled (c_builtin) modules shouldn't be in the cache.
        preload_module('types', 'token')
        check_loaded('types', 'token')
    finally:
        cache.parser_cache.update(old_cache)


def test_empty_script(Script):
    assert Script('')


def test_line_number_errors(Script):
    """
    Script should raise a ValueError if line/column numbers are not in a
    valid range.
    """
    s = 'hello'
    # lines
    with raises(ValueError):
        Script(s).complete(2, 0)
    with raises(ValueError):
        Script(s).complete(0, 0)

    # columns
    with raises(ValueError):
        Script(s).infer(1, len(s) + 1)
    with raises(ValueError):
        Script(s).goto(1, -1)

    # ok
    Script(s).get_signatures(1, 0)
    Script(s).get_references(1, len(s))


def _check_number(Script, source, result='float'):
    completions = Script(source).complete()
    assert completions[0].parent().name == result


def test_completion_on_number_literals(Script):
    # No completions on an int literal (is a float).
    assert [c.name for c in Script('1. ').complete()] \
        == ['and', 'if', 'in', 'is', 'not', 'or']

    # Multiple points after an int literal basically mean that there's a float
    # and a call after that.
    _check_number(Script, '1..')
    _check_number(Script, '1.0.')

    # power notation
    _check_number(Script, '1.e14.')
    _check_number(Script, '1.e-3.')
    _check_number(Script, '9e3.')
    assert Script('1.e3..').complete() == []
    assert Script('1.e-13..').complete() == []


def test_completion_on_hex_literals(Script):
    assert Script('0x1..').complete() == []
    _check_number(Script, '0x1.', 'int')  # hexdecimal
    # Completing binary literals doesn't work if they are not actually binary
    # (invalid statements).
    assert Script('0b2.b').complete() == []
    _check_number(Script, '0b1.', 'int')  # binary

    _check_number(Script, '0x2e.', 'int')
    _check_number(Script, '0xE7.', 'int')
    _check_number(Script, '0xEa.', 'int')
    # theoretically, but people can just check for syntax errors:
    assert Script('0x.').complete() == []


def test_completion_on_complex_literals(Script):
    assert Script('1j..').complete() == []
    _check_number(Script, '1j.', 'complex')
    _check_number(Script, '44.j.', 'complex')
    _check_number(Script, '4.0j.', 'complex')
    # No dot no completion - I thought, but 4j is actually a literal after
    # which a keyword like or is allowed. Good times, haha!
    # However this has been disabled again, because it apparently annoyed
    # users. So no completion after j without a space :)
    assert not Script('4j').complete()
    assert ({c.name for c in Script('4j ').complete()}
            == {'if', 'and', 'in', 'is', 'not', 'or'})


def test_goto_non_name(Script, environment):
    assert Script('for').goto() == []

    assert Script('assert').goto() == []
    assert Script('True').goto() == []


def test_infer_on_non_name(Script):
    assert Script('import x').infer(column=0) == []


def test_infer_on_generator(Script, environment):
    script = Script('def x(): yield 1\ny=x()\ny')
    def_, = script.infer()
    assert def_.name == 'Generator'
    def_, = script.infer(only_stubs=True)
    assert def_.name == 'Generator'


def test_goto_definition_not_multiple(Script):
    """
    There should be only one result if it leads back to the same
    origin (e.g. instance method)
    """

    s = dedent('''\
            import random
            class A():
                def __init__(self, a):
                    self.a = 3

                def foo(self):
                    pass

            if random.randint(0, 1):
                a = A(2)
            else:
                a = A(1)
            a''')
    assert len(Script(s).infer()) == 1


def test_reference_description(Script):
    descs = [u.description for u in Script("foo = ''; foo").get_references()]
    assert set(descs) == {"foo = ''", 'foo'}


def test_get_line_code(Script):
    def get_line_code(source, line=None, **kwargs):
        # On Windows replace \r
        return Script(source).complete(line=line)[0].get_line_code(**kwargs).replace('\r', '')

    # On builtin
    assert get_line_code('abs') == 'def abs(__x: SupportsAbs[_T]) -> _T: ...\n'

    # On custom code
    first_line = 'def foo():\n'
    line = '    foo'
    code = first_line + line
    assert get_line_code(code) == first_line

    # With before/after
    code = code + '\nother_line'
    assert get_line_code(code, line=2) == first_line
    assert get_line_code(code, line=2, after=1) == first_line + line + '\n'
    assert get_line_code(code, line=2, after=2, before=1) == code
    # Should just be the whole thing, since there are no more lines on both
    # sides.
    assert get_line_code(code, line=2, after=3, before=3) == code


def test_get_line_code_on_builtin(Script, disable_typeshed):
    abs_ = Script('abs').complete()[0]
    assert abs_.name == 'abs'
    assert abs_.get_line_code() == ''
    assert abs_.line is None


def test_goto_follow_imports(Script):
    code = dedent("""
    import inspect
    inspect.isfunction""")
    definition, = Script(code).goto(column=0, follow_imports=True)
    assert definition.module_path.name == 'inspect.py'
    assert (definition.line, definition.column) == (1, 0)

    definition, = Script(code).goto(follow_imports=True)
    assert definition.module_path.name == 'inspect.py'
    assert (definition.line, definition.column) > (1, 0)

    code = '''def param(p): pass\nparam(1)'''
    start_pos = 1, len('def param(')

    script = Script(code)
    definition, = script.goto(*start_pos, follow_imports=True)
    assert (definition.line, definition.column) == start_pos
    assert definition.name == 'p'
    result, = definition.goto()
    assert result.name == 'p'
    result, = definition.infer()
    assert result.name == 'int'
    result, = result.infer()
    assert result.name == 'int'

    definition, = script.goto(*start_pos)
    assert (definition.line, definition.column) == start_pos

    d, = Script('a = 1\na').goto(follow_imports=True)
    assert d.name == 'a'


def test_goto_module(Script):
    def check(line, expected, follow_imports=False):
        script = Script(path=path)
        module, = script.goto(line=line, follow_imports=follow_imports)
        assert module.module_path == expected

    base_path = get_example_dir('simple_import')
    path = base_path.joinpath('__init__.py')

    check(1, base_path.joinpath('module.py'))
    check(1, base_path.joinpath('module.py'), follow_imports=True)
    check(5, base_path.joinpath('module2.py'))


def test_goto_definition_cursor(Script):

    s = ("class A():\n"
         "    def _something(self):\n"
         "        return\n"
         "    def different_line(self,\n"
         "                   b):\n"
         "        return\n"
         "A._something\n"
         "A.different_line"
         )

    in_name = 2, 9
    under_score = 2, 8
    cls = 2, 7
    should1 = 7, 10
    diff_line = 4, 10
    should2 = 8, 10

    def get_def(pos):
        return [d.description for d in Script(s).infer(*pos)]

    in_name = get_def(in_name)
    under_score = get_def(under_score)
    should1 = get_def(should1)
    should2 = get_def(should2)

    diff_line = get_def(diff_line)

    assert should1 == in_name
    assert should1 == under_score

    assert should2 == diff_line

    assert get_def(cls) == []


def test_no_statement_parent(Script):
    source = dedent("""
    def f():
        pass

    class C:
        pass

    variable = f if random.choice([0, 1]) else C""")
    defs = Script(source).infer(column=3)
    defs = sorted(defs, key=lambda d: d.line)
    assert [d.description for d in defs] == ['def f', 'class C']


def test_backslash_continuation_and_bracket(Script):
    code = dedent(r"""
    x = 0
    a = \
      [1, 2, 3, (x)]""")

    lines = code.splitlines()
    column = lines[-1].index('(')
    def_, = Script(code).infer(line=len(lines), column=column)
    assert def_.name == 'int'


def test_goto_follow_builtin_imports(Script):
    s = Script('import sys; sys')
    d, = s.goto(follow_imports=True)
    assert d.in_builtin_module() is True
    d, = s.goto(follow_imports=True, follow_builtin_imports=True)
    assert d.in_builtin_module() is True


def test_docstrings_for_completions(Script):
    for c in Script('').complete():
        assert isinstance(c.docstring(), str)


def test_completions_order_most_resemblance_on_top(Script):
    """Test that the completion which resembles the in-typing the most will come first."""
    code = "from pathlib import Path\npath = Path('hello.txt')\n\npat"
    script = Script(code)
    # User is typing "pat" and "path" is closer to it than "Path".
    assert ['path', 'Path'] == [comp.name for comp in script.complete()]


def test_fuzzy_completion(Script):
    script = Script('string =  "hello"\nstring.upper')
    # 'isupper' is included because it is fuzzily matched.
    assert ['upper',
            'isupper'] == [comp.name for comp in script.complete(fuzzy=True)]


def test_math_fuzzy_completion(Script, environment):
    script = Script('import math\nmath.og')
    expected = ['copysign', 'log', 'log10', 'log1p', 'log2']
    completions = script.complete(fuzzy=True)
    assert expected == [comp.name for comp in completions]
    for c in completions:
        assert c.complete is None


def test_file_fuzzy_completion(Script):
    path = os.path.join(test_dir, 'completion')
    script = Script('"{}/ep08_i'.format(path))
    expected = [
        'pep0484_basic.py"',
        'pep0484_generic_mismatches.py"',
        'pep0484_generic_parameters.py"',
        'pep0484_generic_passthroughs.py"',
        'pep0484_typing.py"',
    ]
    assert expected == [comp.name for comp in script.complete(fuzzy=True)]


@pytest.mark.parametrize(
    'code, column', [
        ('"foo"', 0),
        ('"foo"', 3),
        ('"foo"', None),
        ('"""foo"""', 5),
        ('"""foo"""', 1),
        ('"""foo"""', 2),
    ]
)
def test_goto_on_string(Script, code, column):
    script = Script(code)
    assert not script.infer(column=column)
    assert not script.goto(column=column)


def test_multi_goto(Script):
    script = Script('x = 1\ny = 1.0\nx\ny')
    x, = script.goto(line=3)
    y, = script.goto(line=4)
    assert x.line == 1
    assert y.line == 2


@pytest.mark.parametrize(
    'code, column, expected', [
        ('str() ', 3, 'str'),
        ('str() ', 4, 'str'),
        ('str() ', 5, 'str'),
        ('str() ', 6, None),
        ('str(    ) ', 6, None),
        ('   1', 1, None),
        ('str(1) ', 3, 'str'),
        ('str(1) ', 4, 'int'),
        ('str(1) ', 5, 'int'),
        ('str(1) ', 6, 'str'),
        ('str(1) ', 7, None),
        ('str( 1) ', 4, 'str'),
        ('str( 1) ', 5, 'int'),
        ('str(+1) ', 4, 'str'),
        ('str(+1) ', 5, 'int'),
        ('str(1, 1.) ', 3, 'str'),
        ('str(1, 1.) ', 4, 'int'),
        ('str(1, 1.) ', 5, 'int'),
        ('str(1, 1.) ', 6, None),
        ('str(1, 1.) ', 7, 'float'),
    ]
)
def test_infer_after_parentheses(Script, code, column, expected):
    completions = Script(code).infer(column=column)
    if expected is None:
        assert completions == []
    else:
        assert [c.name for c in completions] == [expected]
