import unittest
from typing import List, Optional

from mypyc.analysis.ircheck import check_func_ir, FnError, can_coerce_to
from mypyc.ir.class_ir import ClassIR
from mypyc.ir.rtypes import (
    none_rprimitive, str_rprimitive, int32_rprimitive, int64_rprimitive,
    RType, RUnion, bytes_rprimitive, RInstance, object_rprimitive
)
from mypyc.ir.ops import (
    BasicBlock, Op, Return, Integer, Goto, Register, LoadLiteral, Assign
)
from mypyc.ir.func_ir import FuncIR, FuncDecl, FuncSignature
from mypyc.ir.pprint import format_func


def assert_has_error(fn: FuncIR, error: FnError) -> None:
    errors = check_func_ir(fn)
    assert errors == [error]


def assert_no_errors(fn: FuncIR) -> None:
    assert not check_func_ir(fn)


NONE_VALUE = Integer(0, rtype=none_rprimitive)


class TestIrcheck(unittest.TestCase):
    def setUp(self) -> None:
        self.label = 0

    def basic_block(self, ops: List[Op]) -> BasicBlock:
        self.label += 1
        block = BasicBlock(self.label)
        block.ops = ops
        return block

    def func_decl(self, name: str, ret_type: Optional[RType] = None) -> FuncDecl:
        if ret_type is None:
            ret_type = none_rprimitive
        return FuncDecl(
            name=name,
            class_name=None,
            module_name="module",
            sig=FuncSignature(
                args=[],
                ret_type=ret_type,
            ),
        )

    def test_valid_fn(self) -> None:
        assert_no_errors(
            FuncIR(
                decl=self.func_decl(name="func_1"),
                arg_regs=[],
                blocks=[
                    self.basic_block(
                        ops=[
                            Return(value=NONE_VALUE),
                        ]
                    )
                ],
            )
        )

    def test_block_not_terminated_empty_block(self) -> None:
        block = self.basic_block([])
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[],
            blocks=[block],
        )
        assert_has_error(fn, FnError(source=block, desc="Block not terminated"))

    def test_valid_goto(self) -> None:
        block_1 = self.basic_block([Return(value=NONE_VALUE)])
        block_2 = self.basic_block([Goto(label=block_1)])
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[],
            blocks=[block_1, block_2],
        )
        assert_no_errors(fn)

    def test_invalid_goto(self) -> None:
        block_1 = self.basic_block([Return(value=NONE_VALUE)])
        goto = Goto(label=block_1)
        block_2 = self.basic_block([goto])
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[],
            # block_1 omitted
            blocks=[block_2],
        )
        assert_has_error(
            fn, FnError(source=goto, desc="Invalid control operation target: 1")
        )

    def test_invalid_register_source(self) -> None:
        ret = Return(
            value=Register(
                type=none_rprimitive,
                name="r1",
            )
        )
        block = self.basic_block([ret])
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[],
            blocks=[block],
        )
        assert_has_error(
            fn, FnError(source=ret, desc="Invalid op reference to register r1")
        )

    def test_invalid_op_source(self) -> None:
        ret = Return(
            value=LoadLiteral(
                value="foo",
                rtype=str_rprimitive,
            )
        )
        block = self.basic_block([ret])
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[],
            blocks=[block],
        )
        assert_has_error(
            fn,
            FnError(source=ret, desc="Invalid op reference to op of type LoadLiteral"),
        )

    def test_invalid_return_type(self) -> None:
        ret = Return(value=Integer(value=5, rtype=int32_rprimitive))
        fn = FuncIR(
            decl=self.func_decl(name="func_1", ret_type=int64_rprimitive),
            arg_regs=[],
            blocks=[self.basic_block([ret])],
        )
        assert_has_error(
            fn,
            FnError(
                source=ret, desc="Cannot coerce source type int32 to dest type int64"
            ),
        )

    def test_invalid_assign(self) -> None:
        arg_reg = Register(type=int64_rprimitive, name="r1")
        assign = Assign(dest=arg_reg, src=Integer(value=5, rtype=int32_rprimitive))
        ret = Return(value=NONE_VALUE)
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[arg_reg],
            blocks=[self.basic_block([assign, ret])],
        )
        assert_has_error(
            fn,
            FnError(
                source=assign, desc="Cannot coerce source type int32 to dest type int64"
            ),
        )

    def test_can_coerce_to(self) -> None:
        cls = ClassIR(name="Cls", module_name="cls")
        valid_cases = [
            (int64_rprimitive, int64_rprimitive),
            (str_rprimitive, str_rprimitive),
            (str_rprimitive, object_rprimitive),
            (object_rprimitive, str_rprimitive),
            (RUnion([bytes_rprimitive, str_rprimitive]), str_rprimitive),
            (str_rprimitive, RUnion([bytes_rprimitive, str_rprimitive])),
            (RInstance(cls), object_rprimitive),
        ]

        invalid_cases = [
            (int64_rprimitive, int32_rprimitive),
            (RInstance(cls), str_rprimitive),
            (str_rprimitive, bytes_rprimitive),
        ]

        for src, dest in valid_cases:
            assert can_coerce_to(src, dest)
        for src, dest in invalid_cases:
            assert not can_coerce_to(src, dest)

    def test_duplicate_op(self) -> None:
        arg_reg = Register(type=int32_rprimitive, name="r1")
        assign = Assign(dest=arg_reg, src=Integer(value=5, rtype=int32_rprimitive))
        block = self.basic_block([assign, assign, Return(value=NONE_VALUE)])
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[],
            blocks=[block],
        )
        assert_has_error(fn, FnError(source=assign, desc="Func has a duplicate op"))

    def test_pprint(self) -> None:
        block_1 = self.basic_block([Return(value=NONE_VALUE)])
        goto = Goto(label=block_1)
        block_2 = self.basic_block([goto])
        fn = FuncIR(
            decl=self.func_decl(name="func_1"),
            arg_regs=[],
            # block_1 omitted
            blocks=[block_2],
        )
        errors = [(goto, "Invalid control operation target: 1")]
        formatted = format_func(fn, errors)
        assert formatted == [
            "def func_1():",
            "L0:",
            "    goto L1",
            "  ERR: Invalid control operation target: 1",
        ]
