# -*- coding: utf-8 -*-
# flake8: noqa
# disable flake check on this file because some constructs are strange
# or redundant on purpose and can't be disable on a line-by-line basis
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import ast
import inspect
import sys

import six

import _pytest._code
import pytest
from _pytest._code import Source

failsonjython = pytest.mark.xfail("sys.platform.startswith('java')")


def test_source_str_function():
    x = Source("3")
    assert str(x) == "3"

    x = Source("   3")
    assert str(x) == "3"

    x = Source(
        """
        3
        """
    )
    assert str(x) == "\n3"


def test_unicode():
    x = Source(u"4")
    assert str(x) == "4"
    co = _pytest._code.compile(u'u"å"', mode="eval")
    val = eval(co)
    assert isinstance(val, six.text_type)


def test_source_from_function():
    source = _pytest._code.Source(test_source_str_function)
    assert str(source).startswith("def test_source_str_function():")


def test_source_from_method():
    class TestClass(object):
        def test_method(self):
            pass

    source = _pytest._code.Source(TestClass().test_method)
    assert source.lines == ["def test_method(self):", "    pass"]


def test_source_from_lines():
    lines = ["a \n", "b\n", "c"]
    source = _pytest._code.Source(lines)
    assert source.lines == ["a ", "b", "c"]


def test_source_from_inner_function():
    def f():
        pass

    source = _pytest._code.Source(f, deindent=False)
    assert str(source).startswith("    def f():")
    source = _pytest._code.Source(f)
    assert str(source).startswith("def f():")


def test_source_putaround_simple():
    source = Source("raise ValueError")
    source = source.putaround(
        "try:",
        """\
        except ValueError:
            x = 42
        else:
            x = 23""",
    )
    assert (
        str(source)
        == """\
try:
    raise ValueError
except ValueError:
    x = 42
else:
    x = 23"""
    )


def test_source_putaround():
    source = Source()
    source = source.putaround(
        """
        if 1:
            x=1
    """
    )
    assert str(source).strip() == "if 1:\n    x=1"


def test_source_strips():
    source = Source("")
    assert source == Source()
    assert str(source) == ""
    assert source.strip() == source


def test_source_strip_multiline():
    source = Source()
    source.lines = ["", " hello", "  "]
    source2 = source.strip()
    assert source2.lines == [" hello"]


def test_syntaxerror_rerepresentation():
    ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz")
    assert ex.value.lineno == 1
    assert ex.value.offset in (4, 5, 7)  # XXX pypy/jython versus cpython?
    assert ex.value.text.strip(), "x x"


def test_isparseable():
    assert Source("hello").isparseable()
    assert Source("if 1:\n  pass").isparseable()
    assert Source(" \nif 1:\n  pass").isparseable()
    assert not Source("if 1:\n").isparseable()
    assert not Source(" \nif 1:\npass").isparseable()
    assert not Source(chr(0)).isparseable()


class TestAccesses(object):
    source = Source(
        """\
        def f(x):
            pass
        def g(x):
            pass
    """
    )

    def test_getrange(self):
        x = self.source[0:2]
        assert x.isparseable()
        assert len(x.lines) == 2
        assert str(x) == "def f(x):\n    pass"

    def test_getline(self):
        x = self.source[0]
        assert x == "def f(x):"

    def test_len(self):
        assert len(self.source) == 4

    def test_iter(self):
        values = [x for x in self.source]
        assert len(values) == 4


class TestSourceParsingAndCompiling(object):
    source = Source(
        """\
        def f(x):
            assert (x ==
                    3 +
                    4)
    """
    ).strip()

    def test_compile(self):
        co = _pytest._code.compile("x=3")
        d = {}
        exec(co, d)
        assert d["x"] == 3

    def test_compile_and_getsource_simple(self):
        co = _pytest._code.compile("x=3")
        exec(co)
        source = _pytest._code.Source(co)
        assert str(source) == "x=3"

    def test_compile_and_getsource_through_same_function(self):
        def gensource(source):
            return _pytest._code.compile(source)

        co1 = gensource(
            """
            def f():
                raise KeyError()
        """
        )
        co2 = gensource(
            """
            def f():
                raise ValueError()
        """
        )
        source1 = inspect.getsource(co1)
        assert "KeyError" in source1
        source2 = inspect.getsource(co2)
        assert "ValueError" in source2

    def test_getstatement(self):
        # print str(self.source)
        ass = str(self.source[1:])
        for i in range(1, 4):
            # print "trying start in line %r" % self.source[i]
            s = self.source.getstatement(i)
            # x = s.deindent()
            assert str(s) == ass

    def test_getstatementrange_triple_quoted(self):
        # print str(self.source)
        source = Source(
            """hello('''
        ''')"""
        )
        s = source.getstatement(0)
        assert s == str(source)
        s = source.getstatement(1)
        assert s == str(source)

    def test_getstatementrange_within_constructs(self):
        source = Source(
            """\
            try:
                try:
                    raise ValueError
                except SomeThing:
                    pass
            finally:
                42
        """
        )
        assert len(source) == 7
        # check all lineno's that could occur in a traceback
        # assert source.getstatementrange(0) == (0, 7)
        # assert source.getstatementrange(1) == (1, 5)
        assert source.getstatementrange(2) == (2, 3)
        assert source.getstatementrange(3) == (3, 4)
        assert source.getstatementrange(4) == (4, 5)
        # assert source.getstatementrange(5) == (0, 7)
        assert source.getstatementrange(6) == (6, 7)

    def test_getstatementrange_bug(self):
        source = Source(
            """\
            try:
                x = (
                   y +
                   z)
            except:
                pass
        """
        )
        assert len(source) == 6
        assert source.getstatementrange(2) == (1, 4)

    def test_getstatementrange_bug2(self):
        source = Source(
            """\
            assert (
                33
                ==
                [
                  X(3,
                      b=1, c=2
                   ),
                ]
              )
        """
        )
        assert len(source) == 9
        assert source.getstatementrange(5) == (0, 9)

    def test_getstatementrange_ast_issue58(self):
        source = Source(
            """\

            def test_some():
                for a in [a for a in
                    CAUSE_ERROR]: pass

            x = 3
        """
        )
        assert getstatement(2, source).lines == source.lines[2:3]
        assert getstatement(3, source).lines == source.lines[3:4]

    def test_getstatementrange_out_of_bounds_py3(self):
        source = Source("if xxx:\n   from .collections import something")
        r = source.getstatementrange(1)
        assert r == (1, 2)

    def test_getstatementrange_with_syntaxerror_issue7(self):
        source = Source(":")
        pytest.raises(SyntaxError, lambda: source.getstatementrange(0))

    def test_compile_to_ast(self):
        source = Source("x = 4")
        mod = source.compile(flag=ast.PyCF_ONLY_AST)
        assert isinstance(mod, ast.Module)
        compile(mod, "<filename>", "exec")

    def test_compile_and_getsource(self):
        co = self.source.compile()
        exec(co, globals())
        f(7)
        excinfo = pytest.raises(AssertionError, f, 6)
        frame = excinfo.traceback[-1].frame
        stmt = frame.code.fullsource.getstatement(frame.lineno)
        assert str(stmt).strip().startswith("assert")

    @pytest.mark.parametrize("name", ["", None, "my"])
    def test_compilefuncs_and_path_sanity(self, name):
        def check(comp, name):
            co = comp(self.source, name)
            if not name:
                expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2)
            else:
                expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2)
            fn = co.co_filename
            assert fn.endswith(expected)

        mycode = _pytest._code.Code(self.test_compilefuncs_and_path_sanity)
        mylineno = mycode.firstlineno
        mypath = mycode.path

        for comp in _pytest._code.compile, _pytest._code.Source.compile:
            check(comp, name)

    def test_offsetless_synerr(self):
        pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval")


def test_getstartingblock_singleline():
    class A(object):
        def __init__(self, *args):
            frame = sys._getframe(1)
            self.source = _pytest._code.Frame(frame).statement

    x = A("x", "y")

    values = [i for i in x.source.lines if i.strip()]
    assert len(values) == 1


def test_getline_finally():
    def c():
        pass

    with pytest.raises(TypeError) as excinfo:
        teardown = None
        try:
            c(1)
        finally:
            if teardown:
                teardown()
    source = excinfo.traceback[-1].statement
    assert str(source).strip() == "c(1)"


def test_getfuncsource_dynamic():
    source = """
        def f():
            raise ValueError

        def g(): pass
    """
    co = _pytest._code.compile(source)
    exec(co, globals())
    assert str(_pytest._code.Source(f)).strip() == "def f():\n    raise ValueError"
    assert str(_pytest._code.Source(g)).strip() == "def g(): pass"


def test_getfuncsource_with_multine_string():
    def f():
        c = """while True:
    pass
"""

    expected = '''\
    def f():
        c = """while True:
    pass
"""
'''
    assert str(_pytest._code.Source(f)) == expected.rstrip()


def test_deindent():
    from _pytest._code.source import deindent as deindent

    assert deindent(["\tfoo", "\tbar"]) == ["foo", "bar"]

    source = """\
        def f():
            def g():
                pass
    """
    lines = deindent(source.splitlines())
    assert lines == ["def f():", "    def g():", "        pass"]


def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot):
    # this test fails because the implicit inspect.getsource(A) below
    # does not return the "x = 1" last line.
    source = _pytest._code.Source(
        """
        class A(object):
            def method(self):
                x = 1
    """
    )
    path = tmpdir.join("a.py")
    path.write(source)
    s2 = _pytest._code.Source(tmpdir.join("a.py").pyimport().A)
    assert str(source).strip() == str(s2).strip()


if True:

    def x():
        pass


def test_getsource_fallback():
    from _pytest._code.source import getsource

    expected = """def x():
    pass"""
    src = getsource(x)
    assert src == expected


def test_idem_compile_and_getsource():
    from _pytest._code.source import getsource

    expected = "def x(): pass"
    co = _pytest._code.compile(expected)
    src = getsource(co)
    assert src == expected


def test_findsource_fallback():
    from _pytest._code.source import findsource

    src, lineno = findsource(x)
    assert "test_findsource_simple" in str(src)
    assert src[lineno] == "    def x():"


def test_findsource():
    from _pytest._code.source import findsource

    co = _pytest._code.compile(
        """if 1:
    def x():
        pass
"""
    )

    src, lineno = findsource(co)
    assert "if 1:" in str(src)

    d = {}
    eval(co, d)
    src, lineno = findsource(d["x"])
    assert "if 1:" in str(src)
    assert src[lineno] == "    def x():"


def test_getfslineno():
    from _pytest._code import getfslineno

    def f(x):
        pass

    fspath, lineno = getfslineno(f)

    assert fspath.basename == "test_source.py"
    assert lineno == _pytest._code.getrawcode(f).co_firstlineno - 1  # see findsource

    class A(object):
        pass

    fspath, lineno = getfslineno(A)

    _, A_lineno = inspect.findsource(A)
    assert fspath.basename == "test_source.py"
    assert lineno == A_lineno

    assert getfslineno(3) == ("", -1)

    class B(object):
        pass

    B.__name__ = B.__qualname__ = "B2"
    assert getfslineno(B)[1] == -1


def test_code_of_object_instance_with_call():
    class A(object):
        pass

    pytest.raises(TypeError, lambda: _pytest._code.Source(A()))

    class WithCall(object):
        def __call__(self):
            pass

    code = _pytest._code.Code(WithCall())
    assert "pass" in str(code.source())

    class Hello(object):
        def __call__(self):
            pass

    pytest.raises(TypeError, lambda: _pytest._code.Code(Hello))


def getstatement(lineno, source):
    from _pytest._code.source import getstatementrange_ast

    source = _pytest._code.Source(source, deindent=False)
    ast, start, end = getstatementrange_ast(lineno, source)
    return source[start:end]


def test_oneline():
    source = getstatement(0, "raise ValueError")
    assert str(source) == "raise ValueError"


def test_comment_and_no_newline_at_end():
    from _pytest._code.source import getstatementrange_ast

    source = Source(
        [
            "def test_basic_complex():",
            "    assert 1 == 2",
            "# vim: filetype=pyopencl:fdm=marker",
        ]
    )
    ast, start, end = getstatementrange_ast(1, source)
    assert end == 2


def test_oneline_and_comment():
    source = getstatement(0, "raise ValueError\n#hello")
    assert str(source) == "raise ValueError"


def test_comments():
    source = '''def test():
    "comment 1"
    x = 1
      # comment 2
    # comment 3

    assert False

"""
comment 4
"""
'''
    for line in range(2, 6):
        assert str(getstatement(line, source)) == "    x = 1"
    if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"):
        tqs_start = 8
    else:
        tqs_start = 10
        assert str(getstatement(10, source)) == '"""'
    for line in range(6, tqs_start):
        assert str(getstatement(line, source)) == "    assert False"
    for line in range(tqs_start, 10):
        assert str(getstatement(line, source)) == '"""\ncomment 4\n"""'


def test_comment_in_statement():
    source = """test(foo=1,
    # comment 1
    bar=2)
"""
    for line in range(1, 3):
        assert (
            str(getstatement(line, source))
            == "test(foo=1,\n    # comment 1\n    bar=2)"
        )


def test_single_line_else():
    source = getstatement(1, "if False: 2\nelse: 3")
    assert str(source) == "else: 3"


def test_single_line_finally():
    source = getstatement(1, "try: 1\nfinally: 3")
    assert str(source) == "finally: 3"


def test_issue55():
    source = (
        "def round_trip(dinp):\n  assert 1 == dinp\n"
        'def test_rt():\n  round_trip("""\n""")\n'
    )
    s = getstatement(3, source)
    assert str(s) == '  round_trip("""\n""")'


def test_multiline():
    source = getstatement(
        0,
        """\
raise ValueError(
    23
)
x = 3
""",
    )
    assert str(source) == "raise ValueError(\n    23\n)"


class TestTry(object):
    source = """\
try:
    raise ValueError
except Something:
    raise IndexError(1)
else:
    raise KeyError()
"""

    def test_body(self):
        source = getstatement(1, self.source)
        assert str(source) == "    raise ValueError"

    def test_except_line(self):
        source = getstatement(2, self.source)
        assert str(source) == "except Something:"

    def test_except_body(self):
        source = getstatement(3, self.source)
        assert str(source) == "    raise IndexError(1)"

    def test_else(self):
        source = getstatement(5, self.source)
        assert str(source) == "    raise KeyError()"


class TestTryFinally(object):
    source = """\
try:
    raise ValueError
finally:
    raise IndexError(1)
"""

    def test_body(self):
        source = getstatement(1, self.source)
        assert str(source) == "    raise ValueError"

    def test_finally(self):
        source = getstatement(3, self.source)
        assert str(source) == "    raise IndexError(1)"


class TestIf(object):
    source = """\
if 1:
    y = 3
elif False:
    y = 5
else:
    y = 7
"""

    def test_body(self):
        source = getstatement(1, self.source)
        assert str(source) == "    y = 3"

    def test_elif_clause(self):
        source = getstatement(2, self.source)
        assert str(source) == "elif False:"

    def test_elif(self):
        source = getstatement(3, self.source)
        assert str(source) == "    y = 5"

    def test_else(self):
        source = getstatement(5, self.source)
        assert str(source) == "    y = 7"


def test_semicolon():
    s = """\
hello ; pytest.skip()
"""
    source = getstatement(0, s)
    assert str(source) == s.strip()


def test_def_online():
    s = """\
def func(): raise ValueError(42)

def something():
    pass
"""
    source = getstatement(0, s)
    assert str(source) == "def func(): raise ValueError(42)"


def XXX_test_expression_multiline():
    source = """\
something
'''
'''"""
    result = getstatement(1, source)
    assert str(result) == "'''\n'''"


def test_getstartingblock_multiline():
    class A(object):
        def __init__(self, *args):
            frame = sys._getframe(1)
            self.source = _pytest._code.Frame(frame).statement

    # fmt: off
    x = A('x',
          'y'
          ,
          'z')
    # fmt: on
    values = [i for i in x.source.lines if i.strip()]
    assert len(values) == 4
