import unittest

from h8536.pseudocode import PseudocodeOptions, generate_pseudocode


def _instruction(
    address,
    mnemonic,
    operands="",
    *,
    kind="normal",
    targets=None,
    text=None,
):
    return {
        "address": address,
        "text": text or f"{mnemonic} {operands}".strip(),
        "mnemonic": mnemonic,
        "operands": operands,
        "kind": kind,
        "targets": list(targets or []),
        "references": [],
        "comment": "",
    }


def _payload(instructions):
    start = min(ins["address"] for ins in instructions)
    end = max(ins["address"] for ins in instructions)
    return {
        "vectors": [],
        "call_graph": {
            "nodes": [
                {
                    "start": start,
                    "end": end,
                    "label": f"loc_{start:04X}",
                    "sources": [],
                    "instruction_count": len(instructions),
                    "calls": [],
                }
            ],
            "edges": [],
        },
        "instructions": instructions,
    }


def _options(**overrides):
    values = {
        "include_asm": False,
        "include_addresses": False,
        "emit_declarations": False,
    }
    values.update(overrides)
    return PseudocodeOptions(**values)


class PseudocodeStructuringTest(unittest.TestCase):
    def test_backward_conditional_branch_becomes_do_while(self):
        payload = _payload(
            [
                _instruction(0x0100, "MOV.B", "#H'00, R0"),
                _instruction(0x0102, "ADD.B", "#H'01, R0"),
                _instruction(0x0104, "CMP.B", "#H'03, R0"),
                _instruction(0x0106, "BNE", "loc_0102", kind="branch", targets=[0x0102]),
                _instruction(0x0108, "RTS", kind="return"),
            ]
        )

        text = generate_pseudocode(payload, options=_options())

        self.assertIn("do {", text)
        self.assertIn("} while (!Z);", text)
        self.assertNotIn("goto loc_0102;", text)
        self.assertNotIn("loc_0102:", text)

    def test_forward_conditional_branch_over_small_span_becomes_if(self):
        payload = _payload(
            [
                _instruction(0x0100, "CMP.B", "#H'00, R0"),
                _instruction(0x0102, "BEQ", "loc_0108", kind="branch", targets=[0x0108]),
                _instruction(0x0104, "MOV.B", "#H'01, R1"),
                _instruction(0x0106, "ADD.B", "#H'02, R1"),
                _instruction(0x0108, "RTS", kind="return"),
            ]
        )

        text = generate_pseudocode(payload, options=_options())

        self.assertIn("if (!Z) {", text)
        self.assertIn("R1 = (uint8_t)(0x01);", text)
        self.assertIn("R1 += (uint8_t)(0x02);", text)
        self.assertNotIn("goto loc_0108;", text)
        self.assertNotIn("loc_0108:", text)

    def test_structuring_can_be_disabled(self):
        payload = _payload(
            [
                _instruction(0x0100, "CMP.B", "#H'00, R0"),
                _instruction(0x0102, "BEQ", "loc_0108", kind="branch", targets=[0x0108]),
                _instruction(0x0104, "MOV.B", "#H'01, R1"),
                _instruction(0x0108, "RTS", kind="return"),
            ]
        )

        text = generate_pseudocode(payload, options=_options(structured=False))

        self.assertIn("if (Z) goto loc_0108;", text)
        self.assertIn("loc_0108:", text)
        self.assertNotIn("if (!Z) {", text)

    def test_ambiguous_forward_branch_keeps_goto_fallback(self):
        payload = _payload(
            [
                _instruction(0x0100, "BEQ", "loc_0108", kind="branch", targets=[0x0108]),
                _instruction(0x0102, "MOV.B", "#H'01, R1"),
                _instruction(0x0104, "BRA", "loc_0108", kind="jump", targets=[0x0108]),
                _instruction(0x0108, "RTS", kind="return"),
            ]
        )

        text = generate_pseudocode(payload, options=_options())

        self.assertIn("if (Z) goto loc_0108;", text)
        self.assertIn("goto loc_0108;", text)
        self.assertIn("loc_0108:", text)
        self.assertNotIn("if (!Z) {", text)


if __name__ == "__main__":
    unittest.main()
