import io
import json
import unittest
from pathlib import Path

from h8536.protocol_capture import analyze_capture_text, format_text_report, main, parse_capture_text


class ProtocolCaptureTest(unittest.TestCase):
    def test_parses_timestamped_capture_chunks(self):
        chunks = parse_capture_text("16:06:15.502  RX 006 bytes  00 00 15 80 00 CF\n")

        self.assertEqual(len(chunks), 1)
        self.assertEqual(chunks[0].timestamp_ms, 57975502)
        self.assertEqual(chunks[0].analyzer_direction, "rx")
        self.assertEqual(chunks[0].device_direction, "tx")
        self.assertEqual(chunks[0].bytes, (0x00, 0x00, 0x15, 0x80, 0x00, 0xCF))

    def test_parses_idle_frame_lines_without_direction_token(self):
        chunks = parse_capture_text("11:54:40.567  frame 006  00 00 00 00 80 DA\n")

        self.assertEqual(len(chunks), 1)
        self.assertEqual(chunks[0].timestamp_ms, 42880567)
        self.assertEqual(chunks[0].analyzer_direction, "rx")
        self.assertEqual(chunks[0].device_direction, "tx")
        self.assertEqual(chunks[0].bytes, (0x00, 0x00, 0x00, 0x00, 0x80, 0xDA))

    def test_recombines_user_split_rx_chunks_into_valid_call_frame(self):
        analysis = analyze_capture_text(
            "16:06:15.502  RX 003 bytes  00 00 15\n"
            "16:06:15.506  RX 003 bytes  80 00 CF\n"
        )

        self.assertEqual(analysis["frame_count"], 1)
        frame = analysis["frames"][0]
        self.assertEqual(frame["source_chunk_indexes"], [0, 1])
        self.assertEqual(frame["analyzer_direction"], "rx")
        self.assertEqual(frame["device_direction"], "tx")
        self.assertEqual(frame["report_candidate"]["index"], 0x15)
        self.assertEqual(frame["report_candidate"]["value"], 0x8000)
        self.assertEqual(
            frame["report_candidate"]["observed_candidate"]["name_candidate"],
            "call_button_candidate",
        )
        self.assertEqual(frame["report_candidate"]["observed_candidate"]["state_candidate"], "active")

    def test_recombines_split_chunks_with_multiple_frames_and_labels_known_reports(self):
        analysis = analyze_capture_text(
            "16:06:15.500  RX 004 bytes  00 00 00 00\n"
            "16:06:15.502  RX 005 bytes  80 DA 00 00 07\n"
            "16:06:15.504  RX 003 bytes  80 00 DD\n"
            "16:06:15.600  RX 006 bytes  00 00 00 00 80 DA\n"
        )

        self.assertEqual(analysis["frame_count"], 3)
        names = [
            frame["report_candidate"]["observed_candidate"]["name_candidate"]
            for frame in analysis["frames"]
        ]
        self.assertEqual(
            names,
            [
                "heartbeat_alive_candidate",
                "cam_power_button_candidate",
                "heartbeat_alive_candidate",
            ],
        )
        self.assertEqual(analysis["repeated_group_count"], 1)
        group = analysis["repeated_groups"][0]
        self.assertEqual(group["count"], 2)
        self.assertEqual(group["cadence_ms"]["samples"], [100])

    def test_text_report_mentions_split_label_and_cadence(self):
        report = format_text_report(
            analyze_capture_text(
                "16:06:15.502  RX 003 bytes  00 00 15\n"
                "16:06:15.506  RX 003 bytes  80 00 CF\n"
                "16:06:15.602  RX 006 bytes  00 00 15 80 00 CF\n"
            )
        )

        self.assertIn("call_button_candidate state=active", report)
        self.assertIn("checksum=ok split", report)
        self.assertIn("cadence=avg=100.0ms", report)

    def test_gate_session_hints_summarize_reports_transitions_and_heartbeat_interruptions(self):
        analysis = analyze_capture_text(
            "16:06:15.500  RX 006 bytes  00 00 00 00 80 DA\n"
            "16:06:15.550  RX 006 bytes  00 00 15 80 00 CF\n"
            "16:06:15.600  RX 006 bytes  00 00 00 00 80 DA\n"
            "16:06:15.650  RX 006 bytes  00 00 15 00 00 4F\n"
            "16:06:15.700  RX 006 bytes  00 00 00 00 80 DA\n"
        )

        hints = analysis["gate_session_hints"]
        self.assertEqual(
            hints["observed_autonomous_report_names"],
            ["call_button_candidate", "heartbeat_alive_candidate"],
        )
        reports = {item["name_candidate"]: item for item in hints["observed_reports"]}
        self.assertEqual(reports["heartbeat_alive_candidate"]["count"], 3)
        self.assertEqual(reports["heartbeat_alive_candidate"]["first_timestamp"], "16:06:15.500")
        self.assertEqual(reports["heartbeat_alive_candidate"]["last_timestamp"], "16:06:15.700")
        self.assertEqual(reports["call_button_candidate"]["states"], ["active", "inactive"])

        self.assertEqual(hints["heartbeat_cadence_ms"]["samples"], [100, 100])
        self.assertEqual(hints["heartbeat_cadence_ms"]["average"], 100.0)
        self.assertEqual(len(hints["heartbeat_interruptions"]), 2)
        self.assertEqual(
            hints["heartbeat_interruptions"][0]["interrupted_by"][0]["name_candidate"],
            "call_button_candidate",
        )

        self.assertEqual(len(hints["active_inactive_transitions"]), 1)
        transition = hints["active_inactive_transitions"][0]
        self.assertEqual(transition["index_hex"], "0x0015")
        self.assertEqual(transition["from_state"], "active")
        self.assertEqual(transition["to_state"], "inactive")
        self.assertEqual(hints["evidence_scope"], "capture_side_observation_only")
        self.assertIn("host/session gating", hints["caveat"])

    def test_text_report_mentions_gate_session_hints_and_caveat(self):
        report = format_text_report(
            analyze_capture_text(
                "16:06:15.500  RX 006 bytes  00 00 00 00 80 DA\n"
                "16:06:15.550  RX 006 bytes  00 00 15 80 00 CF\n"
                "16:06:15.600  RX 006 bytes  00 00 00 00 80 DA\n"
                "16:06:15.650  RX 006 bytes  00 00 15 00 00 4F\n"
            )
        )

        self.assertIn(
            "observed autonomous report candidates: call_button_candidate, heartbeat_alive_candidate",
            report,
        )
        self.assertIn("heartbeat cadence count=2 cadence=avg=100.0ms", report)
        self.assertIn("transition index=0x0015 active->inactive", report)
        self.assertIn("heartbeat gap 16:06:15.500..16:06:15.600", report)
        self.assertIn("caveat: Missing autonomous reports", report)

    def test_cli_json_output(self):
        output = io.StringIO()
        rc = main(
            ["--json", "-"],
            stdin=io.StringIO("16:06:15.502  RX 006 bytes  00 00 15 80 00 CF\n"),
            stdout=output,
        )

        self.assertEqual(rc, 0)
        payload = json.loads(output.getvalue())
        self.assertEqual(payload["frames"][0]["report_candidate"]["index"], 0x15)

    def test_idle_reference_capture_when_present(self):
        path = Path("ROM/rcp-txd-idle-only.txt")
        if not path.exists():
            self.skipTest("idle reference capture is not present")

        analysis = analyze_capture_text(path.read_text(encoding="utf-8"))

        self.assertGreaterEqual(analysis["frame_count"], 10)
        self.assertEqual(
            analysis["gate_session_hints"]["observed_autonomous_report_names"],
            ["heartbeat_alive_candidate"],
        )
        heartbeat = analysis["gate_session_hints"]["heartbeat_cadence_ms"]
        self.assertEqual(heartbeat["count"], analysis["frame_count"])
        self.assertGreater(heartbeat["average"], 600)
        self.assertLess(heartbeat["average"], 800)


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