import json
import unittest

from h8536.model import Instruction
from h8536.serial_reconstruction import (
    analyze_serial_reconstruction,
    serial_reconstruction_comment_for_instruction,
    serial_reconstruction_json_payload,
    serial_reconstruction_metadata_for_instruction,
)


def ins(
    address: int,
    mnemonic: str,
    operands: str = "",
    references: list[int] | None = None,
    kind: str = "normal",
    targets: list[int] | None = None,
) -> Instruction:
    return Instruction(
        address,
        b"",
        mnemonic,
        operands,
        kind=kind,
        targets=targets or [],
        references=references or [],
    )


class SerialReconstructionTest(unittest.TestCase):
    def test_candidate_sci1_tx_frame_length_and_checksum_pattern(self):
        instructions = {
            0x3000: ins(0x3000, "MOV:G.B", "#H'02, @H'F858", [0xF858]),
            0x3004: ins(0x3004, "MOV:G.B", "@H'F858, R0", [0xF858]),
            0x3008: ins(0x3008, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]),
            0x300C: ins(0x300C, "MOV:G.B", "#1, @H'F9C2", [0xF9C2]),
            0x301C: ins(0x301C, "MOV:E.B", "#H'5A, R1", []),
            0x3020: ins(0x3020, "XOR.B", "@H'F858, R1", [0xF858]),
            0x3024: ins(0x3024, "XOR.B", "@H'F859, R1", [0xF859]),
            0x3028: ins(0x3028, "XOR.B", "@H'F85A, R1", [0xF85A]),
            0x302C: ins(0x302C, "XOR.B", "@H'F85B, R1", [0xF85B]),
            0x3030: ins(0x3030, "XOR.B", "@H'F85C, R1", [0xF85C]),
            0x3034: ins(0x3034, "MOV:G.B", "R1, @H'F85D", [0xF85D]),
            0x3100: ins(0x3100, "MOV:G.B", "@H'F9C2, R2", [0xF9C2]),
            0x3104: ins(0x3104, "MOV:G.B", "@(-H'07A8,R2), R0", []),
            0x3108: ins(0x3108, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]),
            0x310C: ins(0x310C, "ADD:Q.B", "#1, @H'F9C2", [0xF9C2]),
            0x3110: ins(0x3110, "CMP:G.B", "#6, @H'F9C2", [0xF9C2]),
        }

        analysis = analyze_serial_reconstruction(instructions)

        self.assertEqual(len(analysis["candidates"]), 1)
        candidate = analysis["candidates"][0]
        self.assertEqual(candidate["kind"], "candidate_sci1_tx_frame")
        self.assertEqual(candidate["channel"], "SCI1")
        self.assertEqual(candidate["frame_length"], 6)
        self.assertEqual(candidate["buffer_start"], 0xF858)
        self.assertEqual(candidate["checksum_address"], 0xF85D)
        self.assertEqual(candidate["tx_index_address"], 0xF9C2)
        self.assertEqual(candidate["checksum_seed"], 0x5A)
        self.assertEqual(
            [role["name"] for role in candidate["roles"]],
            ["tx_frame", "tx_checksum", "tx_index"],
        )
        self.assertEqual(candidate["roles"][0]["address"], 0xF858)
        self.assertEqual(candidate["roles"][1]["address"], 0xF85D)
        self.assertEqual(candidate["roles"][2]["address"], 0xF9C2)
        self.assertEqual(candidate["tx_path"]["kind"], "interrupt_driven_txi")
        self.assertEqual(candidate["tx_path"]["initial_tdr_write_address"], 0x3008)
        self.assertEqual(candidate["tx_path"]["txi_indexed_tdr_write_address"], 0x3108)
        self.assertIn("TDRE", candidate["tx_path"]["summary"])
        self.assertEqual(candidate["confidence"], "high")
        self.assertEqual(candidate["confidence_score"], 0.95)
        self.assertEqual(candidate["missing_evidence"], [])

        evidence_addresses = candidate["evidence_addresses"]
        self.assertIn(0x3034, evidence_addresses["checksum_byte"])
        self.assertIn(0x3034, evidence_addresses["xor_checksum_chain"])
        self.assertIn(0x3108, evidence_addresses["tx_isr_indexed_send"])
        self.assertEqual(evidence_addresses["tx_index_compare_frame_length"], [0x3110])
        self.assertEqual(evidence_addresses["tx_checksum_seed"], [0x301C])

        checksum_comment = serial_reconstruction_comment_for_instruction(analysis, 0x3034)
        self.assertIn("candidate/evidence-supported SCI1 6-byte TX frame", checksum_comment)
        self.assertIn("checksum H'F85D", checksum_comment)
        self.assertIn("confidence high", checksum_comment)

        metadata = serial_reconstruction_metadata_for_instruction(analysis, 0x3108)
        self.assertEqual(metadata[0]["candidate_id"], "sci1_tx_frame_f858_len6_candidate")
        self.assertEqual(metadata[0]["evidence"], "tx_isr_indexed_send")
        self.assertIn(0x3108, metadata[0]["evidence_addresses"])

        payload = serial_reconstruction_json_payload(analysis)
        self.assertEqual(payload["candidates"][0]["frame_length"], 6)
        self.assertEqual(payload["candidates"][0]["roles"][1]["name"], "tx_checksum")
        json.dumps(payload)

    def test_candidate_timer_ram_roles_from_frt1_ocia_tick(self):
        instructions = {
            0xBEEA: ins(0xBEEA, "BCLR.B", "#5, @FRT1_TCSR", [0xFE91]),
            0xBEEE: ins(0xBEEE, "TST.B", "@H'F9C0", [0xF9C0]),
            0xBEF4: ins(0xBEF4, "ADD:Q.B", "#-1, @H'F9C0", [0xF9C0]),
            0xBEF8: ins(0xBEF8, "TST.B", "@H'F9C1", [0xF9C1]),
            0xBEFE: ins(0xBEFE, "ADD:Q.B", "#-1, @H'F9C1", [0xF9C1]),
            0xBF02: ins(0xBF02, "TST.W", "@H'F9C6", [0xF9C6]),
            0xBF08: ins(0xBF08, "ADD:Q.W", "#-1, @H'F9C6", [0xF9C6]),
        }

        analysis = analyze_serial_reconstruction(instructions)

        self.assertEqual(analysis["candidates"], [])
        roles = {role["name"]: role for role in analysis["ram_roles"]}
        self.assertEqual(set(roles), {"post_tx_report_delay", "secondary_tx_report_delay", "periodic_report_countdown"})
        self.assertEqual(roles["post_tx_report_delay"]["address"], 0xF9C0)
        self.assertEqual(roles["secondary_tx_report_delay"]["address"], 0xF9C1)
        self.assertEqual(roles["periodic_report_countdown"]["address"], 0xF9C6)
        self.assertEqual(roles["periodic_report_countdown"]["width_bits"], 16)
        self.assertIn("candidate/evidence-supported", roles["post_tx_report_delay"]["confidence"])
        self.assertIn("emulator-guided timer behavior", roles["post_tx_report_delay"]["caveat"])

        payload = serial_reconstruction_json_payload(analysis)
        self.assertEqual(payload["ram_roles"][0]["name"], "post_tx_report_delay")
        json.dumps(payload)

        decrement_comment = serial_reconstruction_comment_for_instruction(analysis, 0xBEF4)
        self.assertIn("RAM role post_tx_report_delay", decrement_comment)
        self.assertIn("FRT1 OCIA periodic tick ISR", decrement_comment)
        metadata = serial_reconstruction_metadata_for_instruction(analysis, 0xBF08)
        self.assertEqual(metadata[0]["action"], "serial_reconstruction_ram_role")
        self.assertEqual(metadata[0]["role_name"], "periodic_report_countdown")

    def test_candidate_sci1_rx_frame_length_and_checksum_validation_pattern(self):
        instructions = {
            0x4FF0: ins(0x4FF0, "BSET.B", "#7, @H'FAA4", [0xFAA4]),
            0x4FF4: ins(0x4FF4, "BCLR.B", "#5, @SCI1_SSR", [0xFEDC]),
            0x4FF8: ins(0x4FF8, "BCLR.B", "#4, @SCI1_SSR", [0xFEDC]),
            0x4FFC: ins(0x4FFC, "BCLR.B", "#3, @SCI1_SSR", [0xFEDC]),
            0x4FFE: ins(0x4FFE, "BCLR.B", "#6, @SCI1_SSR", [0xFEDC]),
            0x5000: ins(0x5000, "MOV:G.B", "@SCI1_RDR, R0", [0xFEDD]),
            0x5004: ins(0x5004, "MOV:G.B", "R0, @(-H'0798,R1)", []),
            0x5008: ins(0x5008, "ADD:Q.B", "#1, R1", []),
            0x500C: ins(0x500C, "MOV:G.B", "R1, @H'F9C3", [0xF9C3]),
            0x5010: ins(0x5010, "CMP:E", "#H'06, R1", []),
            0x5014: ins(0x5014, "MOV:G.B", "#H'14, @H'F9C5", [0xF9C5]),
            0x5100: ins(0x5100, "CMP:G.B", "#H'06, @H'F9C3", [0xF9C3]),
            0x5104: ins(0x5104, "MOV:G.W", "@H'F868, R0", [0xF868]),
            0x5108: ins(0x5108, "MOV:G.W", "R0, @H'F860", [0xF860]),
            0x510C: ins(0x510C, "MOV:G.W", "@H'F86A, R0", [0xF86A]),
            0x5110: ins(0x5110, "MOV:G.W", "R0, @H'F862", [0xF862]),
            0x5114: ins(0x5114, "MOV:G.W", "@H'F86C, R0", [0xF86C]),
            0x5118: ins(0x5118, "MOV:G.W", "R0, @H'F864", [0xF864]),
            0x5120: ins(0x5120, "MOV:E.B", "#H'5A, R0", []),
            0x5124: ins(0x5124, "XOR.B", "@H'F860, R0", [0xF860]),
            0x5128: ins(0x5128, "XOR.B", "@H'F861, R0", [0xF861]),
            0x512C: ins(0x512C, "XOR.B", "@H'F862, R0", [0xF862]),
            0x5130: ins(0x5130, "XOR.B", "@H'F863, R0", [0xF863]),
            0x5134: ins(0x5134, "XOR.B", "@H'F864, R0", [0xF864]),
            0x5138: ins(0x5138, "CMP:G.B", "@H'F865, R0", [0xF865]),
        }

        analysis = analyze_serial_reconstruction(instructions)

        candidate = next(item for item in analysis["candidates"] if item["kind"] == "candidate_sci1_rx_frame")
        self.assertEqual(candidate["frame_length"], 6)
        self.assertEqual(candidate["capture_buffer_start"], 0xF868)
        self.assertEqual(candidate["validation_buffer_start"], 0xF860)
        self.assertEqual(candidate["checksum_address"], 0xF865)
        self.assertEqual(candidate["checksum_seed"], 0x5A)
        self.assertIn("no explicit header", candidate["confidence_reason"])
        self.assertIn("rx_rdrf_clear_before_rdr_read", candidate["evidence_addresses"])
        self.assertIn("rx_eri_falls_through_to_rxi", candidate["evidence_addresses"])
        self.assertTrue(candidate["rx_error_handling"]["fallthrough_to_rx_byte_path"])
        self.assertTrue(candidate["rx_error_handling"]["rdrf_clear_before_rdr_read"])
        self.assertEqual(candidate["rx_error_handling"]["error_latch_address"], 0xFAA4)

        comment = serial_reconstruction_comment_for_instruction(analysis, 0x5138)
        self.assertIn("candidate/evidence-supported SCI1 6-byte RX frame", comment)
        self.assertIn("checksum H'F865", comment)
        self.assertIn("confidence high", comment)
        self.assertIn(
            "ROM clears SCI1 SSR.RDRF before reading SCI1_RDR",
            serial_reconstruction_comment_for_instruction(analysis, 0x4FFE),
        )
        self.assertIn(
            "SCI1 ERI latches FAA4.bit7",
            serial_reconstruction_comment_for_instruction(analysis, 0x4FF0),
        )

    def test_lone_tdr_write_does_not_emit_reconstruction(self):
        instructions = {
            0x4000: ins(0x4000, "MOV:G.B", "R0, @SCI1_TDR", [0xFEDB]),
        }

        analysis = analyze_serial_reconstruction(instructions)

        self.assertEqual(analysis["candidates"], [])
        self.assertEqual(serial_reconstruction_comment_for_instruction(analysis, 0x4000), "")
        self.assertEqual(serial_reconstruction_metadata_for_instruction(analysis, 0x4000), [])
        self.assertEqual(serial_reconstruction_json_payload(analysis)["candidates"], [])


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