import io
import json
import tempfile
import unittest
from pathlib import Path

from h8536.protocol_trace import (
    checksum_for,
    decode_trace,
    format_text_report,
    main,
    parse_byte_text,
)


def frame(prefix: list[int]) -> list[int]:
    return prefix + [checksum_for(prefix)]


class ProtocolTraceTest(unittest.TestCase):
    def test_decodes_six_byte_frame_fields_and_checksum(self):
        decoded = decode_trace(frame([0x08, 0x85, 0x34, 0x12, 0xAB]), direction="rx")
        only = decoded["frames"][0]

        self.assertTrue(only["checksum"]["valid"])
        self.assertEqual(only["direction"], "rx")
        self.assertEqual(only["command"]["value"], 0)
        self.assertEqual(only["index"]["byte1_low3"], 5)
        self.assertEqual(only["index"]["combined"], 0x0534)
        self.assertEqual(only["payload_value"]["word_be"], 0x12AB)
        self.assertEqual(only["payload_value"]["word_le"], 0xAB12)

    def test_reports_bad_checksum_and_trailing_bytes(self):
        decoded = decode_trace([0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x99])
        only = decoded["frames"][0]

        self.assertFalse(only["checksum"]["valid"])
        self.assertEqual(only["checksum"]["expected"], checksum_for([1, 2, 3, 4, 5]))
        self.assertEqual(decoded["trailing_bytes"], ["0x99"])

    def test_loads_semantic_command_names_from_decompiler_json(self):
        with tempfile.TemporaryDirectory() as tmp:
            path = Path(tmp) / "rom.json"
            path.write_text(
                json.dumps(
                    {
                        "serial_semantics": {
                            "protocol_semantics": [
                                {
                                    "command_effects": [
                                        {
                                            "command_value": 1,
                                            "name_candidate": "read_value",
                                        }
                                    ],
                                    "response_schema": [],
                                }
                            ]
                        }
                    }
                ),
                encoding="utf-8",
            )

            decoded = decode_trace(frame([0x09, 0, 1, 2, 3]), semantics_path=path)

        self.assertTrue(decoded["semantics"]["loaded"])
        self.assertEqual(decoded["frames"][0]["command"]["name_candidate"], "read_value")

    def test_marks_rx_cmd_7_as_retransmit_or_error_candidate_with_previous_frame(self):
        prior = frame([0x01, 0, 0, 0, 0])
        retry = frame([0x07, 0, 0, 0, 0])

        decoded = decode_trace(prior + retry, direction="rx")
        annotation = decoded["frames"][1]["stateful_annotations"][0]

        self.assertEqual(annotation["kind"], "retransmit_or_error_candidate")
        self.assertEqual(annotation["previous_valid_same_direction"]["frame_index"], 0)

    def test_decodes_observed_tx_frames_as_reports(self):
        samples = [
            ([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA], 0x0000, 0x0080, "heartbeat_alive_candidate", None),
            ([0x00, 0x00, 0x15, 0x80, 0x00, 0xCF], 0x0015, 0x8000, "call_button_candidate", "active"),
            ([0x00, 0x00, 0x15, 0x00, 0x00, 0x4F], 0x0015, 0x0000, "call_button_candidate", "inactive"),
            ([0x00, 0x00, 0x07, 0x80, 0x00, 0xDD], 0x0007, 0x8000, "cam_power_button_candidate", "active"),
        ]

        decoded = decode_trace([byte for sample, *_ in samples for byte in sample], direction="tx")

        for actual, (_, index, value, name, state) in zip(decoded["frames"], samples):
            self.assertTrue(actual["checksum"]["valid"])
            self.assertFalse(actual["command"]["applicable"])
            self.assertIsNone(actual["command"]["name_candidate"])
            self.assertEqual(actual["response_schema_candidates"], [])
            self.assertEqual(actual["stateful_annotations"], [])
            self.assertEqual(actual["report"]["index"], index)
            self.assertEqual(actual["report"]["value"], value)
            self.assertEqual(actual["report"]["observed_candidate"]["name_candidate"], name)
            self.assertEqual(actual["report"]["observed_candidate"].get("state_candidate"), state)

    def test_tx_text_report_does_not_render_cmd_or_set_value_acked(self):
        with tempfile.TemporaryDirectory() as tmp:
            path = Path(tmp) / "rom.json"
            path.write_text(
                json.dumps(
                    {
                        "serial_semantics": {
                            "command_effects": [
                                {
                                    "command_value": 0,
                                    "name_candidate": "set_value_acked",
                                }
                            ]
                        }
                    }
                ),
                encoding="utf-8",
            )

            decoded = decode_trace(
                [0x00, 0x00, 0x15, 0x80, 0x00, 0xCF],
                direction="tx",
                semantics_path=path,
            )

        text = format_text_report(decoded)

        self.assertIn("report_index=0x0015", text)
        self.assertIn("observed_candidate=call_button_candidate", text)
        self.assertNotIn("cmd=", text)
        self.assertNotIn("set_value_acked", text)

    def test_auto_direction_uses_rx_tx_prefixes(self):
        events = parse_byte_text(
            "rx: 01 00 00 00 00 5B\n"
            "tx: 04 00 00 00 00 5E\n"
        )

        decoded = decode_trace(events, direction="auto")

        self.assertEqual(decoded["frames"][0]["direction"], "rx")
        self.assertEqual(decoded["frames"][1]["direction"], "tx")

    def test_cli_json_output(self):
        output = io.StringIO()
        rc = main(["--json", "01", "00", "00", "00", "00", "5B"], stdout=output)

        self.assertEqual(rc, 0)
        payload = json.loads(output.getvalue())
        self.assertEqual(payload["frames"][0]["command"]["value"], 1)


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