import json
import tempfile
import unittest
from pathlib import Path

from h8536.table_xrefs import analyze_table_xrefs, generate_table_xref_report, write_table_xrefs


def reference(address: int) -> dict:
    return {"address": address}


def instruction(
    address: int,
    mnemonic: str,
    operands: str = "",
    references: list[int] | None = None,
    text: str | None = None,
) -> dict:
    return {
        "address": address,
        "mnemonic": mnemonic,
        "operands": operands,
        "text": text or f"{mnemonic} {operands}".strip(),
        "references": [reference(item) for item in (references or [])],
        "targets": [],
    }


def payload() -> dict:
    return {
        "call_graph": {
            "nodes": [
                {"start": 0xC000, "end": 0xC0FF, "label": "loc_C000"},
                {"start": 0xD000, "end": 0xD0FF, "label": "loc_D000"},
            ],
        },
        "serial_semantics": {
            "table_map_candidates": [
                {
                    "kind": "logical_table_map_candidate",
                    "name_candidate": "primary_value_table_candidate",
                    "confidence": "candidate-medium",
                    "accesses": [{"instruction_address": 0xC004}],
                }
            ],
        },
        "lcd_text": {
            "strings": [
                {
                    "address": 0x77F4,
                    "text": "COMM LINK ITEM-1",
                    "trimmed": "COMM LINK ITEM-1",
                    "confidence": "high",
                    "xrefs": [
                        {
                            "address": 0x7804,
                            "following_bsr": {"address": 0x7807, "target": 0x5A91},
                        }
                    ],
                    "xref_count": 1,
                }
            ],
        },
        "lcd_driver": {
            "routines": [
                {
                    "start": 0x3F40,
                    "end": 0x3F74,
                    "role_hint": "lcd_wait_and_transfer",
                    "roles": ["lcd_data_write"],
                }
            ],
        },
        "instructions": [
            instruction(0xC000, "MOV:G.W", "#H'0006, R3"),
            instruction(0xC004, "MOV:G.W", "@(-H'2000,R3), R0"),
            instruction(0xC008, "MOV:G.W", "R1, @(-H'1800,R4)"),
            instruction(0xD000, "MOV:G.W", "@H'F900, R2", [0xF900]),
            instruction(0xD004, "BSET.B", "#7, @(-H'1400,R5)"),
            instruction(0xD008, "CMP:G.W", "@H'E124, R0", [0xE124]),
            instruction(0xD00C, "MOV:G.W", "R3, @H'F922", [0xF922]),
        ],
    }


class TableXrefsTest(unittest.TestCase):
    def test_reports_logical_direct_static_and_dynamic_accesses(self):
        analysis = analyze_table_xrefs(payload())
        tables = {table["name"]: table for table in analysis["tables"]}

        primary = tables["primary_value_table_candidate"]
        self.assertEqual(primary["access_count"], 3)
        self.assertEqual(primary["read_count"], 3)
        self.assertEqual(primary["static_offsets"], [0, 6, 0x124])
        static_access = primary["accesses"][0]
        self.assertEqual(static_access["index"], 6)
        self.assertEqual(static_access["logical_address"], 0xE006)
        self.assertEqual(static_access["function_label"], "loc_C000")
        self.assertEqual(static_access["semantic_candidates"][0]["confidence"], "candidate-medium")
        self.assertEqual(primary["accesses"][2]["kind"], "direct_logical_address_access")
        self.assertEqual(primary["accesses"][2]["logical_address"], 0xE124)

        current = tables["current_value_table_candidate"]
        self.assertEqual(current["access_count"], 2)
        self.assertEqual(current["dynamic_index_count"], 1)
        self.assertEqual(current["accesses"][0]["index"], "dynamic")
        self.assertEqual(current["accesses"][0]["index_register"], "R4")
        self.assertEqual(current["accesses"][1]["direct_address"], 0xF922)
        self.assertEqual(current["accesses"][1]["offset"], 2)
        self.assertEqual(current["write_count"], 2)

        flags = tables["flag_table_candidate"]
        self.assertEqual(flags["write_count"], 1)
        self.assertEqual(flags["dynamic_index_count"], 1)

        lcd_terms = {
            item["term"]: item
            for item in analysis["lcd_correlation"]["term_hits"]
        }
        self.assertEqual(lcd_terms["CONNECT"]["hit_count"], 0)
        self.assertEqual(lcd_terms["COMM LINK"]["hit_count"], 1)
        self.assertEqual(
            analysis["lcd_correlation"]["display_builder_targets"][0]["target"],
            0x5A91,
        )

    def test_text_report_names_dynamic_registers_and_functions(self):
        text = generate_table_xref_report(payload(), source_name="sample.json")

        self.assertIn("Table/Index Cross-Reference Report for sample.json", text)
        self.assertIn("primary_value_table_candidate H'E000", text)
        self.assertIn("offset H'0006 -> H'E006", text)
        self.assertIn("offset H'0124 -> H'E124", text)
        self.assertIn("offset H'0002 at H'F922", text)
        self.assertIn("index dynamic via R4", text)
        self.assertIn("term 'CONNECT': no LCD/text candidate hits", text)
        self.assertIn("term 'COMM LINK': 1 candidate", text)
        self.assertIn("display builder xrefs: H'5A91:1", text)
        self.assertIn("loc_C000", text)

    def test_write_json_output(self):
        with tempfile.TemporaryDirectory() as tmp:
            input_path = Path(tmp) / "rom.json"
            output_path = Path(tmp) / "xrefs.json"
            input_path.write_text(json.dumps(payload()), encoding="utf-8")

            write_table_xrefs(input_path, output_path, as_json=True)

            written = json.loads(output_path.read_text(encoding="utf-8"))
            self.assertEqual(written["kind"], "table_xrefs")
            self.assertEqual(written["summary"]["access_count"], 6)
            self.assertEqual(written["source"], str(input_path))


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