# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
import itertools

import pytest

import tvm
from tvm.script.printer.doc import (
    AssertDoc,
    AssignDoc,
    CallDoc,
    ClassDoc,
    CommentDoc,
    DictDoc,
    DocStringDoc,
    ExprStmtDoc,
    ForDoc,
    FunctionDoc,
    IdDoc,
    IfDoc,
    LambdaDoc,
    ListDoc,
    LiteralDoc,
    OperationDoc,
    OperationKind,
    ReturnDoc,
    ScopeDoc,
    SliceDoc,
    StmtBlockDoc,
    TupleDoc,
    WhileDoc,
)
from tvm.script.printer.doc_printer import to_python_script


def format_script(s: str) -> str:
    """
    Remove leading and trailing blank lines, and make the minimum idention 0
    """
    s = s.strip("\n")

    non_empty_lines = [line for line in s.splitlines() if line and not line.isspace()]
    if not non_empty_lines:
        # no actual content
        return ""

    line_indents = [len(line) - len(line.lstrip(" ")) for line in non_empty_lines]
    spaces_to_remove = min(line_indents)

    cleaned_lines = "\n".join(line[spaces_to_remove:] for line in s.splitlines())
    if not cleaned_lines.endswith("\n"):
        cleaned_lines += "\n"
    return cleaned_lines.strip()


@pytest.mark.parametrize(
    "doc,expected",
    [
        (LiteralDoc(None), "None"),
        (LiteralDoc(True), "True"),
        (LiteralDoc(False), "False"),
        (LiteralDoc("test"), '"test"'),
        (LiteralDoc(""), '""'),
        (LiteralDoc('""'), r'"\"\""'),
        (LiteralDoc("\n\t\\test\r"), r'"\n\t\\test\r"'),
        # TODO: fix the roundatrippable problem caused by utf8
        pytest.param(LiteralDoc("\x88"), r'"\x88"', marks=pytest.mark.xfail),
        (LiteralDoc(0), "0"),
        (LiteralDoc(-1), "-1"),
        (LiteralDoc(3.25), "3.25"),
        (LiteralDoc(-0.5), "-0.5"),
    ],
    ids=itertools.count(),
)
def test_print_literal_doc(doc, expected):
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "name",
    [
        "test",
        "_test",
        "TestCase",
        "test_case",
        "test123",
    ],
    ids=itertools.count(),
)
def test_print_id_doc(name):
    doc = IdDoc(name)
    assert to_python_script(doc) == format_script(name)


@pytest.mark.parametrize(
    "attr",
    [
        "attr",
        "_attr",
        "Attr",
        "attr_1",
    ],
    ids=itertools.count(),
)
def test_print_attr_doc(attr):
    doc = IdDoc("x").attr(attr)
    assert to_python_script(doc) == format_script(f"x.{attr}")


@pytest.mark.parametrize(
    "indices, expected",
    [
        (
            (),
            "[()]",
        ),
        (
            (LiteralDoc(1),),
            "[1]",
        ),
        (
            (LiteralDoc(2), IdDoc("x")),
            "[2, x]",
        ),
        (
            (SliceDoc(LiteralDoc(1), LiteralDoc(2)),),
            "[1:2]",
        ),
        (
            (SliceDoc(LiteralDoc(1)), IdDoc("y")),
            "[1:, y]",
        ),
        (
            (SliceDoc(), IdDoc("y")),
            "[:, y]",
        ),
        (
            (IdDoc("x"), IdDoc("y"), IdDoc("z")),
            "[x, y, z]",
        ),
    ],
    ids=itertools.count(),
)
def test_print_index_doc(indices, expected):
    doc = IdDoc("x")[indices]
    assert to_python_script(doc) == format_script(f"x{expected}")


UNARY_OP_TOKENS = {
    OperationKind.USub: "-",
    OperationKind.Invert: "~",
    OperationKind.Not: "not ",
}


@pytest.mark.parametrize(
    "op_kind, expected_token",
    list(UNARY_OP_TOKENS.items()),
    ids=UNARY_OP_TOKENS.keys(),
)
def test_print_unary_operation_doc(op_kind, expected_token):
    doc = OperationDoc(op_kind, [IdDoc("x")])
    assert to_python_script(doc) == format_script(f"{expected_token}x")


BINARY_OP_TOKENS = {
    OperationKind.Add: "+",
    OperationKind.Sub: "-",
    OperationKind.Mult: "*",
    OperationKind.Div: "/",
    OperationKind.FloorDiv: "//",
    OperationKind.Mod: "%",
    OperationKind.Pow: "**",
    OperationKind.LShift: "<<",
    OperationKind.RShift: ">>",
    OperationKind.BitAnd: "&",
    OperationKind.BitOr: "|",
    OperationKind.BitXor: "^",
    OperationKind.Lt: "<",
    OperationKind.LtE: "<=",
    OperationKind.Eq: "==",
    OperationKind.NotEq: "!=",
    OperationKind.Gt: ">",
    OperationKind.GtE: ">=",
    OperationKind.And: "and",
    OperationKind.Or: "or",
    OperationKind.MatMul: "@",
}


@pytest.mark.parametrize(
    "op_kind, expected_token",
    list(BINARY_OP_TOKENS.items()),
    ids=BINARY_OP_TOKENS.keys(),
)
def test_print_binary_operation_doc(op_kind, expected_token):
    doc = OperationDoc(op_kind, [IdDoc("x"), IdDoc("y")])
    assert to_python_script(doc) == format_script(f"x {expected_token} y")


SPECIAL_OP_CASES = [
    (
        OperationKind.IfThenElse,
        [LiteralDoc(True), LiteralDoc("true"), LiteralDoc("false")],
        '"true" if True else "false"',
    ),
    (
        OperationKind.IfThenElse,
        [IdDoc("x"), LiteralDoc(None), LiteralDoc(1)],
        "None if x else 1",
    ),
]


@pytest.mark.parametrize(
    "op_kind, operands, expected", SPECIAL_OP_CASES, ids=[kind for (kind, *_) in SPECIAL_OP_CASES]
)
def test_print_special_operation_doc(op_kind, operands, expected):
    doc = OperationDoc(op_kind, operands)
    assert to_python_script(doc) == format_script(expected)


def test_operation_doc_test_exhaustive():
    special_op_covered = {k for k, *_ in SPECIAL_OP_CASES}
    for op_kind in OperationKind:
        if OperationKind._UnaryStart < op_kind < OperationKind._UnaryEnd:
            assert op_kind in UNARY_OP_TOKENS, (
                f"{op_kind.name} not covered in test_print_unary_operation_doc. "
                f"Please add the expected token to UNARY_OP_TOKENS"
            )
        elif OperationKind._BinaryStart < op_kind < OperationKind._BinaryEnd:
            assert op_kind in BINARY_OP_TOKENS, (
                f"{op_kind.name} not covered in test_print_binary_operation_doc. "
                f"Please add the expected token to BINARY_OP_TOKENS"
            )
        elif not op_kind.name.startswith("_"):
            # Special Op
            assert op_kind in special_op_covered, (
                f"{op_kind.name} not covered in test_print_special_operation_doc. "
                f"Please add the test cases for it to SPECIAL_OP_CASES"
            )


@pytest.mark.parametrize(
    "args, kwargs, expected",
    [
        (
            (),
            {},
            "()",
        ),
        (
            (),
            {"key0": IdDoc("u")},
            "(key0=u)",
        ),
        (
            (),
            {"key0": IdDoc("u"), "key1": IdDoc("v")},
            "(key0=u, key1=v)",
        ),
        (
            (IdDoc("x"),),
            {},
            "(x)",
        ),
        (
            (IdDoc("x"),),
            {"key0": IdDoc("u")},
            "(x, key0=u)",
        ),
        (
            (IdDoc("x"),),
            {"key0": IdDoc("u"), "key1": IdDoc("v")},
            "(x, key0=u, key1=v)",
        ),
        (
            (IdDoc("x"), (IdDoc("y"))),
            {},
            "(x, y)",
        ),
        (
            (IdDoc("x"), (IdDoc("y"))),
            {"key0": IdDoc("u")},
            "(x, y, key0=u)",
        ),
        (
            (IdDoc("x"), (IdDoc("y"))),
            {"key0": IdDoc("u"), "key1": IdDoc("v")},
            "(x, y, key0=u, key1=v)",
        ),
    ],
    ids=itertools.count(),
)
def test_print_call_doc(args, kwargs, expected):
    doc = CallDoc(IdDoc("f"), *args, **kwargs)
    assert to_python_script(doc) == format_script(f"f{expected}")


@pytest.mark.parametrize(
    "args, expected",
    [
        (
            (),
            "lambda : 0",
        ),
        (
            (IdDoc("x"),),
            "lambda x: 0",
        ),
        (
            (IdDoc("x"), IdDoc("y")),
            "lambda x, y: 0",
        ),
        (
            (IdDoc("x"), IdDoc("y"), IdDoc("z")),
            "lambda x, y, z: 0",
        ),
    ],
    ids=itertools.count(),
)
def test_print_lambda_doc(args, expected):
    doc = LambdaDoc(args, body=LiteralDoc(0))
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "elements, expected",
    [
        (
            (),
            "[]",
        ),
        (
            [IdDoc("x")],
            "[x]",
        ),
        (
            [IdDoc("x"), IdDoc("y")],
            "[x, y]",
        ),
        (
            [IdDoc("x"), IdDoc("y"), IdDoc("z")],
            "[x, y, z]",
        ),
    ],
    ids=itertools.count(),
)
def test_print_list_doc(elements, expected):
    doc = ListDoc(elements)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "elements, expected",
    [
        (
            (),
            "()",
        ),
        (
            [IdDoc("x")],
            "(x,)",
        ),
        (
            [IdDoc("x"), IdDoc("y")],
            "(x, y)",
        ),
        (
            [IdDoc("x"), IdDoc("y"), IdDoc("z")],
            "(x, y, z)",
        ),
    ],
    ids=itertools.count(),
)
def test_print_tuple_doc(elements, expected):
    doc = TupleDoc(elements)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "content, expected",
    [
        (
            {},
            "{}",
        ),
        (
            {LiteralDoc("key_x"): IdDoc("x")},
            '{"key_x": x}',
        ),
        (
            {LiteralDoc("key_x"): IdDoc("x"), LiteralDoc("key_y"): IdDoc("y")},
            '{"key_x": x, "key_y": y}',
        ),
        (
            {
                LiteralDoc("key_x"): IdDoc("x"),
                LiteralDoc("key_y"): IdDoc("y"),
                LiteralDoc("key_z"): IdDoc("z"),
            },
            '{"key_x": x, "key_y": y, "key_z": z}',
        ),
    ],
    ids=itertools.count(),
)
def test_print_dict_doc(content, expected):
    doc = DictDoc(content)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "slice_doc, expected",
    [
        (
            SliceDoc(),
            ":",
        ),
        (
            SliceDoc(LiteralDoc(1)),
            "1:",
        ),
        (
            SliceDoc(None, LiteralDoc(2)),
            ":2",
        ),
        (
            SliceDoc(LiteralDoc(1), LiteralDoc(2)),
            "1:2",
        ),
        (
            SliceDoc(None, None, LiteralDoc(3)),
            "::3",
        ),
        (
            SliceDoc(LiteralDoc(1), None, LiteralDoc(3)),
            "1::3",
        ),
        (
            SliceDoc(None, LiteralDoc(2), LiteralDoc(3)),
            ":2:3",
        ),
        (
            SliceDoc(LiteralDoc(1), LiteralDoc(2), LiteralDoc(3)),
            "1:2:3",
        ),
    ],
    ids=itertools.count(),
)
def test_print_slice_doc(slice_doc, expected):
    doc = IdDoc("x")[slice_doc]
    assert to_python_script(doc) == format_script(f"x[{expected}]")


@pytest.mark.parametrize(
    "stmts, expected",
    [
        (
            [],
            "",
        ),
        (
            [ExprStmtDoc(IdDoc("x"))],
            "x",
        ),
        (
            [ExprStmtDoc(IdDoc("x")), ExprStmtDoc(IdDoc("y"))],
            """
            x
            y
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_stmt_block_doc(stmts, expected):
    doc = StmtBlockDoc(stmts)
    assert to_python_script(doc).strip() == format_script(expected).strip()


@pytest.mark.parametrize(
    "doc, expected",
    [
        (
            AssignDoc(IdDoc("x"), IdDoc("y"), None),
            "x = y",
        ),
        (
            AssignDoc(IdDoc("x"), IdDoc("y"), IdDoc("int")),
            "x: int = y",
        ),
        (
            AssignDoc(IdDoc("x"), None, IdDoc("int")),
            "x: int",
        ),
        (
            AssignDoc(TupleDoc([IdDoc("x"), IdDoc("y")]), IdDoc("z"), None),
            "x, y = z",
        ),
        (
            AssignDoc(TupleDoc([IdDoc("x"), TupleDoc([IdDoc("y"), IdDoc("z")])]), IdDoc("z"), None),
            "x, (y, z) = z",
        ),
    ],
    ids=itertools.count(),
)
def test_print_assign_doc(doc, expected):
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "then_branch, else_branch, expected",
    [
        (
            [ExprStmtDoc(IdDoc("x"))],
            [],
            """
            if pred:
                x
            """,
        ),
        (
            [],
            [ExprStmtDoc(IdDoc("y"))],
            """
            if pred:
                pass
            else:
                y
            """,
        ),
        (
            [ExprStmtDoc(IdDoc("x"))],
            [ExprStmtDoc(IdDoc("y"))],
            """
            if pred:
                x
            else:
                y
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_if_doc(then_branch, else_branch, expected):
    doc = IfDoc(IdDoc("pred"), then_branch, else_branch)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "body, expected",
    [
        (
            [ExprStmtDoc(IdDoc("x"))],
            """
            while pred:
                x
            """,
        ),
        (
            [],
            """
            while pred:
                pass
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_while_doc(body, expected):
    doc = WhileDoc(IdDoc("pred"), body)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "body, expected",
    [
        (
            [ExprStmtDoc(IdDoc("x"))],
            """
            for x in y:
                x
            """,
        ),
        (
            [],
            """
            for x in y:
                pass
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_for_doc(body, expected):
    doc = ForDoc(IdDoc("x"), IdDoc("y"), body)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "lhs, body, expected",
    [
        (
            IdDoc("c"),
            [ExprStmtDoc(IdDoc("x"))],
            """
            with context() as c:
                x
            """,
        ),
        (
            IdDoc("c"),
            [],
            """
            with context() as c:
                pass
            """,
        ),
        (
            None,
            [],
            """
            with context():
                pass
            """,
        ),
        (
            None,
            [ExprStmtDoc(IdDoc("x"))],
            """
            with context():
                x
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_scope_doc(lhs, body, expected):
    doc = ScopeDoc(lhs, CallDoc(IdDoc("context")), body)
    assert to_python_script(doc) == format_script(expected)


def test_print_expr_stmt_doc():
    doc = ExprStmtDoc(CallDoc(IdDoc("f"), IdDoc("x")))
    assert to_python_script(doc) == format_script("f(x)")


@pytest.mark.parametrize(
    "msg, expected",
    [
        (
            None,
            """
            assert True
            """,
        ),
        (
            LiteralDoc("test message"),
            """
            assert True, "test message"
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_assert_doc(msg, expected):
    test = LiteralDoc(True)

    doc = AssertDoc(test, msg)

    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "value, expected",
    [
        (
            LiteralDoc(None),
            """
            return None
            """,
        ),
        (
            IdDoc("x"),
            """
            return x
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_return_doc(value, expected):
    doc = ReturnDoc(value)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "args, decorators, return_type, body, expected",
    [
        (
            [],
            [],
            None,
            [],
            """
            def func():
                pass
            """,
        ),
        (
            [AssignDoc(IdDoc("x"), rhs=None, annotation=IdDoc("int"))],
            [],
            IdDoc("int"),
            [],
            """
            def func(x: int) -> int:
                pass
            """,
        ),
        (
            [AssignDoc(IdDoc("x"), rhs=LiteralDoc(1), annotation=IdDoc("int"))],
            [],
            LiteralDoc(None),
            [],
            """
            def func(x: int = 1) -> None:
                pass
            """,
        ),
        (
            [],
            [IdDoc("wrap")],
            LiteralDoc(None),
            [],
            """
            @wrap
            def func() -> None:
                pass
            """,
        ),
        (
            [],
            [IdDoc("wrap_outter"), IdDoc("wrap_inner")],
            LiteralDoc(None),
            [],
            """
            @wrap_outter
            @wrap_inner
            def func() -> None:
                pass
            """,
        ),
        (
            [
                AssignDoc(IdDoc("x"), rhs=None, annotation=IdDoc("int")),
                AssignDoc(IdDoc("y"), rhs=LiteralDoc(1), annotation=IdDoc("int")),
            ],
            [IdDoc("wrap")],
            LiteralDoc(None),
            [],
            """
            @wrap
            def func(x: int, y: int = 1) -> None:
                pass
            """,
        ),
        (
            [
                AssignDoc(IdDoc("x"), rhs=None, annotation=IdDoc("int")),
                AssignDoc(IdDoc("y"), rhs=LiteralDoc(1), annotation=IdDoc("int")),
            ],
            [IdDoc("wrap")],
            LiteralDoc(None),
            [
                AssignDoc(IdDoc("y"), OperationDoc(OperationKind.Add, [IdDoc("x"), LiteralDoc(1)])),
                AssignDoc(IdDoc("y"), OperationDoc(OperationKind.Sub, [IdDoc("y"), LiteralDoc(1)])),
            ],
            """
            @wrap
            def func(x: int, y: int = 1) -> None:
                y = x + 1
                y = y - 1
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_function_doc(args, decorators, body, return_type, expected):
    doc = FunctionDoc(IdDoc("func"), args, decorators, return_type, body)
    assert to_python_script(doc) == format_script(expected)  # test


def get_func_doc_for_class(name):
    args = [
        AssignDoc(IdDoc("x"), rhs=None, annotation=IdDoc("int")),
        AssignDoc(IdDoc("y"), rhs=LiteralDoc(1), annotation=IdDoc("int")),
    ]
    body = [
        AssignDoc(IdDoc("y"), OperationDoc(OperationKind.Add, [IdDoc("x"), LiteralDoc(1)])),
        AssignDoc(IdDoc("y"), OperationDoc(OperationKind.Sub, [IdDoc("y"), LiteralDoc(1)])),
    ]
    return FunctionDoc(
        name=IdDoc(name),
        args=args,
        decorators=[IdDoc("wrap")],
        return_type=LiteralDoc(None),
        body=body,
    )


@pytest.mark.parametrize(
    "decorators, body, expected",
    [
        (
            [],
            [],
            """
            class TestClass:
                pass
            """,
        ),
        (
            [IdDoc("wrap")],
            [],
            """
            @wrap
            class TestClass:
                pass
            """,
        ),
        (
            [IdDoc("wrap_outter"), IdDoc("wrap_inner")],
            [],
            """
            @wrap_outter
            @wrap_inner
            class TestClass:
                pass
            """,
        ),
        (
            [IdDoc("wrap")],
            [get_func_doc_for_class("f1")],
            """
            @wrap
            class TestClass:
                @wrap
                def f1(x: int, y: int = 1) -> None:
                    y = x + 1
                    y = y - 1

            """,
        ),
        (
            [IdDoc("wrap")],
            [get_func_doc_for_class("f1"), get_func_doc_for_class("f2")],
            """
            @wrap
            class TestClass:
                @wrap
                def f1(x: int, y: int = 1) -> None:
                    y = x + 1
                    y = y - 1

                @wrap
                def f2(x: int, y: int = 1) -> None:
                    y = x + 1
                    y = y - 1

            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_class_doc(decorators, body, expected):
    doc = ClassDoc(IdDoc("TestClass"), decorators, body)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "comment, expected",
    [
        ("", ""),
        ("test comment 1", "# test comment 1"),
        (
            "test comment 1\ntest comment 2",
            """
            # test comment 1
            # test comment 2
            """,
        ),
    ],
    ids=itertools.count(),
)
def test_print_comment_doc(comment, expected):
    doc = CommentDoc(comment)
    assert to_python_script(doc) == format_script(expected)


def test_print_comment_doc_preserves_trailing_tab():
    assert to_python_script(CommentDoc("test comment\t")) == "# test comment\t"


@pytest.mark.parametrize(
    "comment, expected",
    [
        (
            "",
            "",
        ),
        (
            "test comment 1",
            '''
            """
            test comment 1
            """
            ''',
        ),
        (
            "test comment 1\ntest comment 2",
            '''
            """
            test comment 1
            test comment 2
            """
            ''',
        ),
    ],
    ids=itertools.count(),
)
def test_print_doc_string_doc(comment, expected):
    doc = DocStringDoc(comment)
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "doc, comment, expected",
    [
        (
            AssignDoc(IdDoc("x"), IdDoc("y"), IdDoc("int")),
            "comment",
            """
            x: int = y  # comment
            """,
        ),
        (
            IfDoc(IdDoc("x"), [ExprStmtDoc(IdDoc("y"))], [ExprStmtDoc(IdDoc("z"))]),
            "comment",
            """
            # comment
            if x:
                y
            else:
                z
            """,
        ),
        (
            IfDoc(IdDoc("x"), [ExprStmtDoc(IdDoc("y"))], [ExprStmtDoc(IdDoc("z"))]),
            "comment line 1\ncomment line 2",
            """
            # comment line 1
            # comment line 2
            if x:
                y
            else:
                z
            """,
        ),
        (
            WhileDoc(
                LiteralDoc(True),
                [
                    AssignDoc(IdDoc("x"), IdDoc("y")),
                ],
            ),
            "comment",
            """
            # comment
            while True:
                x = y
            """,
        ),
        (
            ForDoc(IdDoc("x"), IdDoc("y"), []),
            "comment",
            """
            # comment
            for x in y:
                pass
            """,
        ),
        (
            ScopeDoc(IdDoc("x"), IdDoc("y"), []),
            "comment",
            """
            # comment
            with y as x:
                pass
            """,
        ),
        (
            ExprStmtDoc(IdDoc("x")),
            "comment",
            """
            x  # comment
            """,
        ),
        (
            AssertDoc(LiteralDoc(True)),
            "comment",
            """
            assert True  # comment
            """,
        ),
        (
            ReturnDoc(LiteralDoc(1)),
            "comment",
            """
            return 1  # comment
            """,
        ),
        (
            get_func_doc_for_class("f"),
            "comment",
            '''
            @wrap
            def f(x: int, y: int = 1) -> None:
                """
                comment
                """
                y = x + 1
                y = y - 1
            ''',
        ),
        (
            get_func_doc_for_class("f"),
            "comment line 1\n\ncomment line 3",
            '''
            @wrap
            def f(x: int, y: int = 1) -> None:
                """
                comment line 1

                comment line 3
                """
                y = x + 1
                y = y - 1
            ''',
        ),
        (
            ClassDoc(IdDoc("TestClass"), decorators=[IdDoc("wrap")], body=[]),
            "comment",
            '''
            @wrap
            class TestClass:
                """
                comment
                """
                pass
            ''',
        ),
        (
            ClassDoc(IdDoc("TestClass"), decorators=[IdDoc("wrap")], body=[]),
            "comment line 1\n\ncomment line 3",
            '''
            @wrap
            class TestClass:
                """
                comment line 1

                comment line 3
                """
                pass
            ''',
        ),
    ],
    ids=itertools.count(),
)
def test_print_doc_comment(doc, comment, expected):
    doc.comment = comment
    assert to_python_script(doc) == format_script(expected)


@pytest.mark.parametrize(
    "doc",
    [
        AssignDoc(IdDoc("x"), IdDoc("y"), IdDoc("int")),
        ExprStmtDoc(IdDoc("x")),
        AssertDoc(IdDoc("x")),
        ReturnDoc(IdDoc("x")),
    ],
)
def test_print_invalid_multiline_doc_comment(doc):
    doc.comment = "1\n2"
    with pytest.raises(ValueError) as e:
        to_python_script(doc)
    assert "cannot have newline" in str(e.value)


def generate_expr_precedence_test_cases():
    x = IdDoc("x")
    y = IdDoc("y")
    z = IdDoc("z")

    def negative(a):
        return OperationDoc(OperationKind.USub, [a])

    def invert(a):
        return OperationDoc(OperationKind.Invert, [a])

    def not_(a):
        return OperationDoc(OperationKind.Not, [a])

    def add(a, b):
        return OperationDoc(OperationKind.Add, [a, b])

    def sub(a, b):
        return OperationDoc(OperationKind.Sub, [a, b])

    def mult(a, b):
        return OperationDoc(OperationKind.Mult, [a, b])

    def div(a, b):
        return OperationDoc(OperationKind.Div, [a, b])

    def mod(a, b):
        return OperationDoc(OperationKind.Mod, [a, b])

    def pow(a, b):
        return OperationDoc(OperationKind.Pow, [a, b])

    def lshift(a, b):
        return OperationDoc(OperationKind.LShift, [a, b])

    def bit_and(a, b):
        return OperationDoc(OperationKind.BitAnd, [a, b])

    def bit_or(a, b):
        return OperationDoc(OperationKind.BitOr, [a, b])

    def bit_xor(a, b):
        return OperationDoc(OperationKind.BitXor, [a, b])

    def lt(a, b):
        return OperationDoc(OperationKind.Lt, [a, b])

    def eq(a, b):
        return OperationDoc(OperationKind.Eq, [a, b])

    def not_eq(a, b):
        return OperationDoc(OperationKind.NotEq, [a, b])

    def and_(a, b):
        return OperationDoc(OperationKind.And, [a, b])

    def or_(a, b):
        return OperationDoc(OperationKind.Or, [a, b])

    def if_then_else(a, b, c):
        return OperationDoc(OperationKind.IfThenElse, [a, b, c])

    test_cases = {
        "attr-call-index": [
            (
                add(x, y).attr("test"),
                "(x + y).test",
            ),
            (
                add(x, y.attr("test")),
                "x + y.test",
            ),
            (
                x[z].call(y),
                "x[z](y)",
            ),
            (
                x.call(y)[z],
                "x(y)[z]",
            ),
            (
                x.call(y).call(z),
                "x(y)(z)",
            ),
            (
                x.call(y).attr("test"),
                "x(y).test",
            ),
            (
                x.attr("test").call(y),
                "x.test(y)",
            ),
            (
                x.attr("test").attr("test2"),
                "x.test.test2",
            ),
            (
                LambdaDoc([x], x).call(y),
                "(lambda x: x)(y)",
            ),
            (
                add(x, y)[z][add(z, z)].attr("name"),
                "(x + y)[z][z + z].name",
            ),
        ],
        "power": [
            (
                pow(pow(x, y), z),
                "(x ** y) ** z",
            ),
            (
                pow(x, pow(y, z)),
                "x ** y ** z",
            ),
            (
                pow(negative(x), negative(y)),
                "(-x) ** -y",
            ),
            (
                pow(add(x, y), add(y, z)),
                "(x + y) ** (y + z)",
            ),
        ],
        "unary": [
            (
                invert(negative(y)),
                "~-y",
            ),
            (
                negative(y).attr("test"),
                "(-y).test",
            ),
            (
                negative(y.attr("test")),
                "-y.test",
            ),
            (
                mult(negative(x), negative(y)),
                "-x * -y",
            ),
            (
                negative(add(invert(x), negative(y))),
                "-(~x + -y)",
            ),
        ],
        "add-mult": [
            (
                mult(x, mult(y, z)),
                "x * (y * z)",
            ),
            (
                mult(mult(x, y), z),
                "x * y * z",
            ),
            (
                mult(x, add(y, z)),
                "x * (y + z)",
            ),
            (
                mult(add(y, z), x),
                "(y + z) * x",
            ),
            (
                add(x, mod(y, z)),
                "x + y % z",
            ),
            (
                add(mult(y, z), x),
                "y * z + x",
            ),
            (
                add(add(x, y), add(y, z)),
                "x + y + (y + z)",
            ),
            (
                div(add(x, y), add(y, z)),
                "(x + y) / (y + z)",
            ),
        ],
        "shift": [
            (
                div(x, lshift(y, z)),
                "x / (y << z)",
            ),
            (
                mult(lshift(y, z), x),
                "(y << z) * x",
            ),
            (
                lshift(x, mult(y, z)),
                "x << y * z",
            ),
            (
                lshift(mult(x, y), z),
                "x * y << z",
            ),
            (
                lshift(mult(x, y), z),
                "x * y << z",
            ),
            (
                lshift(lshift(x, y), z),
                "x << y << z",
            ),
            (
                lshift(x, lshift(y, z)),
                "x << (y << z)",
            ),
        ],
        "bitwise": [
            (
                add(bit_or(x, y), bit_or(y, z)),
                "(x | y) + (y | z)",
            ),
            (
                bit_and(bit_or(x, y), bit_or(y, z)),
                "(x | y) & (y | z)",
            ),
            (
                bit_or(bit_and(x, y), bit_and(y, z)),
                "x & y | y & z",
            ),
            (
                bit_and(bit_xor(x, bit_or(y, z)), z),
                "(x ^ (y | z)) & z",
            ),
        ],
        "comparison": [
            (
                not_eq(add(x, y), z),
                "x + y != z",
            ),
            (
                eq(pow(x, y), z),
                "x ** y == z",
            ),
            (
                lt(x, div(y, z)),
                "x < y / z",
            ),
            (
                lt(x, if_then_else(y, y, y)),
                "x < (y if y else y)",
            ),
        ],
        "boolean": [
            (
                not_(and_(x, y)),
                "not (x and y)",
            ),
            (
                and_(not_(x), y),
                "not x and y",
            ),
            (
                and_(or_(x, y), z),
                "(x or y) and z",
            ),
            (
                or_(x, or_(y, z)),
                "x or (y or z)",
            ),
            (
                or_(or_(x, y), z),
                "x or y or z",
            ),
            (
                or_(and_(x, y), z),
                # Maybe we should consider adding parentheses here
                # for readability, even though it's not necessary.
                "x and y or z",
            ),
            (
                and_(or_(not_(x), y), z),
                "(not x or y) and z",
            ),
            (
                and_(lt(x, y), lt(y, z)),
                "x < y and y < z",
            ),
            (
                or_(not_(eq(x, y)), lt(y, z)),
                # Same as the previous one, the code here is not
                # readable without parentheses.
                "not x == y or y < z",
            ),
            (
                and_(if_then_else(x, y, z), x),
                "(y if x else z) and x",
            ),
            (
                not_(if_then_else(x, y, z)),
                "not (y if x else z)",
            ),
        ],
        "if-then-else": [
            (
                if_then_else(x, if_then_else(y, y, y), z),
                "y if y else y if x else z",
            ),
            (
                if_then_else(if_then_else(x, x, x), y, z),
                "y if (x if x else x) else z",
            ),
            (
                if_then_else(x, y, if_then_else(z, z, z)),
                "y if x else (z if z else z)",
            ),
            (
                if_then_else(lt(x, x), add(y, y), mult(z, z)),
                "y + y if x < x else z * z",
            ),
            (
                if_then_else(LambdaDoc([x], x), LambdaDoc([y], y), LambdaDoc([z], z)),
                "(lambda y: y) if (lambda x: x) else (lambda z: z)",
            ),
        ],
        "lambda": [
            (
                LambdaDoc([x, y], add(z, z)),
                "lambda x, y: z + z",
            ),
            (
                add(LambdaDoc([x, y], z), z),
                "(lambda x, y: z) + z",
            ),
            (
                LambdaDoc([x, y], add(z, z)).call(x, y),
                "(lambda x, y: z + z)(x, y)",
            ),
            (
                LambdaDoc([x], LambdaDoc([y], z)),
                "lambda x: lambda y: z",
            ),
        ],
    }

    return [
        pytest.param(*args, id=f"{group_name}-{i}")
        for group_name, cases in test_cases.items()
        for i, args in enumerate(cases)
    ]


@pytest.mark.parametrize("doc, expected", generate_expr_precedence_test_cases())
def test_expr_precedence(doc, expected):
    assert to_python_script(doc) == format_script(expected)


if __name__ == "__main__":
    tvm.testing.main()
