import unittest

from h8536.model import Instruction
from h8536.symbols import discover_symbols, instruction_accesses, symbol_for_address


def ins(address, mnemonic, operands="", references=None):
    return Instruction(
        address,
        b"\x00",
        mnemonic,
        operands,
        references=list(references or []),
    )


class SymbolDiscoveryTest(unittest.TestCase):
    def test_discovers_ram_symbol_counts_direction_and_widths(self):
        instructions = {
            0x1000: ins(0x1000, "MOV:G.B", "#H'12, @H'F680", [0xF680]),
            0x1004: ins(0x1004, "CMP:G.B", "#H'01, @H'F680", [0xF680]),
            0x1008: ins(0x1008, "ADD:Q.W", "#1, @H'F680", [0xF680]),
        }

        analysis = discover_symbols(instructions)
        symbols = analysis["symbols"]

        self.assertEqual(len(symbols), 1)
        symbol = symbols[0]
        self.assertEqual(symbol["address"], 0xF680)
        self.assertEqual(symbol["name"], "ram_F680")
        self.assertEqual(symbol["region"], "on_chip_ram")
        self.assertEqual(symbol["kind"], "ram")
        self.assertEqual(symbol["access_count"], 3)
        self.assertEqual(symbol["read_count"], 2)
        self.assertEqual(symbol["write_count"], 2)
        self.assertEqual(symbol["unknown_count"], 0)
        self.assertEqual(symbol["width_hints"], ["byte", "word"])
        self.assertEqual(symbol["width"], "mixed")
        self.assertEqual(symbol["first_access"], 0x1000)
        self.assertEqual(symbol["last_access"], 0x1008)
        self.assertEqual(symbol_for_address(analysis, 0xF680), "ram_F680")

    def test_names_program_or_external_memory_and_excludes_registers_by_default(self):
        instructions = [
            ins(0x2000, "MOV:G.W", "@H'1234, R1", [0x1234]),
            ins(0x2004, "MOV:G.B", "#H'80, @RAMCR", [0xFF11]),
        ]

        analysis = discover_symbols(instructions)

        self.assertEqual([symbol["name"] for symbol in analysis["symbols"]], ["mem_1234"])
        symbol = analysis["symbols"][0]
        self.assertEqual(symbol["region"], "program_or_external")
        self.assertEqual(symbol["kind"], "memory")
        self.assertEqual(symbol["read_count"], 1)
        self.assertIsNone(symbol_for_address(analysis, 0xFF11))

    def test_can_include_io_register_symbols_when_requested(self):
        instructions = [
            ins(0x2004, "MOV:G.B", "#H'80, @RAMCR", [0xFF11]),
        ]

        analysis = discover_symbols(instructions, include_registers=True)

        self.assertEqual(len(analysis["symbols"]), 1)
        symbol = analysis["symbols"][0]
        self.assertEqual(symbol["address"], 0xFF11)
        self.assertEqual(symbol["name"], "RAMCR")
        self.assertEqual(symbol["region"], "register_field")
        self.assertEqual(symbol["kind"], "register")
        self.assertEqual(symbol["write_count"], 1)

    def test_bit_and_clear_operations_use_conservative_directions(self):
        instructions = [
            ins(0x3000, "BSET.B", "#4, @H'F690", [0xF690]),
            ins(0x3002, "BCLR.B", "#4, @H'F690", [0xF690]),
            ins(0x3004, "TST.B", "@H'F690", [0xF690]),
            ins(0x3006, "CLR.B", "@H'F690", [0xF690]),
        ]

        analysis = discover_symbols(instructions)
        symbol = analysis["symbols"][0]

        self.assertEqual(symbol["read_count"], 3)
        self.assertEqual(symbol["write_count"], 3)
        self.assertEqual(
            [access["direction"] for access in symbol["accesses"]],
            ["read_write", "read_write", "read", "write"],
        )

    def test_optional_pointer_table_candidates_add_xrefs_without_io_pollution(self):
        instructions = [
            ins(0x4000, "MOV:G.B", "@H'F680, R0", [0xF680]),
        ]
        data_candidates = {
            "pointer_tables": [
                {
                    "address": 0x0200,
                    "targets": [0xF680, 0x1234, 0xFF11],
                },
            ],
        }

        analysis = discover_symbols(instructions, data_candidates=data_candidates)
        by_name = {symbol["name"]: symbol for symbol in analysis["symbols"]}

        self.assertEqual(by_name["ram_F680"]["xref_count"], 1)
        self.assertEqual(by_name["mem_1234"]["access_count"], 0)
        self.assertEqual(by_name["mem_1234"]["xref_count"], 1)
        self.assertNotIn("RAMCR", by_name)

    def test_instruction_accesses_handles_comma_inside_displacement_operand(self):
        access = instruction_accesses(
            ins(0x5000, "MOV:G.B", "@(H'0010,R1), @H'F682", [0xF682]),
        )

        self.assertEqual(access[0]["direction"], "write")
        self.assertEqual(access[0]["operand"], "@H'F682")


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