from __future__ import annotations

from pathlib import Path

import pytest

from pymulator.specimport.core import ConversationCandidate
from pymulator.specimport.core import build_ecu_spec
from pymulator.specimport.exporter import summarize_for_ui
from pymulator.specimport.exporter import export_selected_candidates
from pymulator.specimport.parser import analyze_log_file, parse_selection


EXAMPLES_ROOT = Path("/Users/mickey/Documents/late/CANcorder/examples")


pytestmark = pytest.mark.skipif(not EXAMPLES_ROOT.exists(), reason="CANcorder examples not available")


def test_filters_random_candump_traffic() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "candump" / "candump-2025-12-18_131436.log"))
    assert analysis.source_type == "candump"
    assert len(analysis.messages) > 0
    assert analysis.candidates == []


def test_detects_cl1000_diagnostic_conversations() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "cl1000" / "0000240.TXT"))
    assert analysis.source_type == "cl1000"
    assert len(analysis.messages) > 0
    assert len(analysis.candidates) >= 1

    top = analysis.candidates[0]
    assert top.request_id in {0x7E0, 0x7E1}
    assert top.response_id in {0x7E8, 0x7E9}
    assert top.matched_pairs > 0


def test_detects_cl1000_0000352_pairs_and_vin() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "cl1000" / "0000352.TXT"))
    assert analysis.source_type == "cl1000"
    assert analysis.metadata.get("isotpAddressing") == "normal"
    assert len(analysis.candidates) >= 2

    pairs = {(candidate.request_id, candidate.response_id): candidate for candidate in analysis.candidates}
    assert (0x7E0, 0x7E8) in pairs
    assert (0x7E1, 0x7E9) in pairs
    assert pairs[(0x7E1, 0x7E9)].vin == "W1K2130891A884216"
    assert 0x7DF in pairs[(0x7E0, 0x7E8)].auxiliary_request_ids
    assert 0x7DF in pairs[(0x7E1, 0x7E9)].auxiliary_request_ids

    ecu_spec_7e0 = build_ecu_spec(pairs[(0x7E0, 0x7E8)], "ECU_7E0", "UNKNOWN00000000000")
    ecu_spec_7e1 = build_ecu_spec(pairs[(0x7E1, 0x7E9)], "ECU_7E1", "UNKNOWN00000000000")
    assert "0x7DF" in ecu_spec_7e0["uds"]["arbitration"]["auxiliary"]
    assert "0x7DF" in ecu_spec_7e1["uds"]["arbitration"]["auxiliary"]
    identifiers_7e0 = {item["id"]: item for item in ecu_spec_7e0["uds"]["identifiers"]}
    assert "0xF121" in identifiers_7e0
    assert "0xF15A" in identifiers_7e0
    assert identifiers_7e0["0xF15A"]["bytes"] == ""
    assert identifiers_7e0["0xF15A"]["writeLevels"] == [5]
    assert len(ecu_spec_7e1["uds"]["identifiers"]) >= 1
    assert ecu_spec_7e0["uds"]["p2ServerMax"] == 0.05
    assert ecu_spec_7e0["uds"]["p2extServerMax"] == 2.0

    security_levels = ecu_spec_7e0["uds"]["securityLevels"]
    assert len(security_levels) == 1
    assert security_levels[0]["level"] == "0x05"
    assert security_levels[0]["seedKeyPairs"][0]["seed"] == "4E A7 D3 69"
    assert security_levels[0]["seedKeyPairs"][0]["key"] == "CD 55 A0 A2"

    custom_requests = {item["request"] for item in ecu_spec_7e0["uds"]["customServices"]}
    assert "31 01 FF 00" not in custom_requests
    assert "2E F1 5A 00 00 03 0D 09 06 00 00 00 00" not in custom_requests
    assert "34 00 44 80 14 00 00 00 0C 00 00" not in custom_requests
    assert any(req.startswith("36 ") for req in custom_requests)

    routines_7e0 = {item["id"]: item for item in ecu_spec_7e0["uds"]["routines"]}
    assert "0xFF00" in routines_7e0
    assert routines_7e0["0xFF00"]["ignoreOptionRecord"] is True
    assert routines_7e0["0xFF00"]["responses"][0]["startResponse"] == "00"

    memory_blocks = ecu_spec_7e0["uds"]["memoryLayout"]
    assert len(memory_blocks) == 1
    assert memory_blocks[0]["address"] == "0x80140000"
    assert memory_blocks[0]["size"] == "0x000C0000"
    assert memory_blocks[0]["mtu"] == "0xFFF"

    responses_31 = pairs[(0x7E0, 0x7E8)].request_response_map.get("31 01 FF 00", [])
    assert any(response[:4] == [0x71, 0x01, 0xFF, 0x00] for response in responses_31)

    summary = summarize_for_ui(
        analysis.candidates,
        metadata_vin=analysis.metadata.get("vin"),
        messages=analysis.messages,
    )
    assert summary["header"]["vin"] == "W1K2130891A884216"
    assert summary["header"]["protocol"] == "uds"
    assert summary["header"]["tamperSuspected"] is False
    assert any(row["pair"] == "0x7E0 -> 0x7E8" for row in summary["conversations"])
    assert any(row["pair"] == "0x7E1 -> 0x7E9" for row in summary["conversations"])


def test_detects_cl1000_0000006_dtcs_for_7e0_7e8() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "cl1000" / "0000006.TXT"))
    assert analysis.source_type == "cl1000"
    pairs = {(candidate.request_id, candidate.response_id): candidate for candidate in analysis.candidates}
    assert (0x7E0, 0x7E8) in pairs

    ecu_spec = build_ecu_spec(pairs[(0x7E0, 0x7E8)], "ECU_7E0", "UNKNOWN00000000000")
    dtcs = ecu_spec["uds"]["dtcs"]
    assert len(dtcs) >= 10

    dtc_map = {tuple(item["bytes"]): set(item.get("status", [])) for item in dtcs}
    assert (0x00, 0x9E, 0x15) in dtc_map
    assert (0x00, 0x42, 0x60) in dtc_map
    assert "warningIndicatorRequested" in dtc_map[(0x00, 0x9E, 0x15)]


def test_detects_ecuconnect_conversation() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "ecuconnect" / "vag-identify.log"))
    assert analysis.source_type == "ecuconnect"
    assert len(analysis.messages) > 0
    assert len(analysis.candidates) >= 1

    pairs = {(candidate.request_id, candidate.response_id) for candidate in analysis.candidates}
    assert (0x7E0, 0x7E8) in pairs


def test_detects_pcapng_hsfz_conversation() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "pcapng" / "VehiCAL_ID.pcapng"))
    assert analysis.source_type == "pcapng"
    assert len(analysis.messages) > 0
    assert len(analysis.candidates) >= 1


def test_detects_pcapng_hsfz_broadcast_responder_candidates() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "pcapng" / "error.pcapng"))
    assert analysis.source_type == "pcapng"
    assert analysis.metadata.get("protocol") == "hsfz"
    assert len(analysis.messages) > 0
    pairs = {(candidate.request_id, candidate.response_id) for candidate in analysis.candidates}
    responders = {response for request, response in pairs if request == 0xF4}
    assert len(responders) >= 15
    assert {0x01, 0x06, 0x29, 0x44, 0x72, 0x78}.issubset(responders)


def test_detects_pcapng_doip_tester_present_broadcast_conversations() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "pcapng" / "doip-tester-present-broadcast.pcapng"))
    assert analysis.source_type == "pcapng"
    assert analysis.metadata.get("protocol") == "doip"
    assert len(analysis.messages) > 0
    assert len(analysis.candidates) >= 5
    pairs = {(candidate.request_id, candidate.response_id): candidate for candidate in analysis.candidates}
    assert any(request == 0xE80 for request, _response in pairs.keys())
    assert any(response == 0x402A for _request, response in pairs.keys())


def test_detects_pcapng_doip_identification_prefers_plausible_vin_candidate() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "pcapng" / "mercedes_vin_id.pcapng"))
    assert analysis.source_type == "pcapng"
    assert analysis.metadata.get("vin") == "W1T96302710779652"


def test_exports_vehicle_and_selected_ecu_specs(tmp_path: Path) -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "cl1000" / "0000240.TXT"))
    assert len(analysis.candidates) >= 2

    result = export_selected_candidates(
        candidates=analysis.candidates,
        output_dir=str(tmp_path),
        vehicle_name="Example Import",
        base_vin=analysis.metadata.get("vin"),
        selected_indices=[0, 1],
        overwrite=True,
    )

    vehicle_path = Path(result.vehicle_path)
    assert vehicle_path.exists()
    assert vehicle_path.suffix == ".vehicle"
    assert len(result.ecus) == 2
    for ecu in result.ecus:
        ecu_path = Path(ecu.path)
        assert ecu_path.exists()
        assert ecu_path.suffix == ".json"


def test_parse_selection_ranges_and_single_values() -> None:
    assert parse_selection("all", 4) == [0, 1, 2, 3]
    assert parse_selection("1,3", 4) == [0, 2]
    assert parse_selection("2-4", 5) == [1, 2, 3]


def test_ui_summary_flags_tamper_when_vin_or_protocol_differs() -> None:
    candidates = [
        ConversationCandidate(
            key="A",
            request_id=0x7E0,
            response_id=0x7E8,
            extended_addr=None,
            message_count=10,
            request_count=5,
            response_count=5,
            matched_pairs=5,
            unmatched_requests=0,
            unique_requests=5,
            score=100,
            confidence="high",
            protocol_hint="uds",
            vin="WVWZZZAUZLW018419",
            request_response_map={},
        ),
        ConversationCandidate(
            key="B",
            request_id=0x700,
            response_id=0x708,
            extended_addr=None,
            message_count=10,
            request_count=5,
            response_count=5,
            matched_pairs=5,
            unmatched_requests=0,
            unique_requests=5,
            score=90,
            confidence="high",
            protocol_hint="kwp",
            vin="WVWZZZAUZLW018410",
            request_response_map={},
        ),
    ]

    summary = summarize_for_ui(candidates)
    assert summary["header"]["tamperSuspected"] is True
    assert summary["header"]["vinConsistent"] is False
    assert summary["header"]["protocolConsistent"] is False
    assert len(summary["conversations"]) == 2


def test_ui_summary_marks_broadcast_candidates_not_recommended() -> None:
    analysis = analyze_log_file(str(EXAMPLES_ROOT / "cl1000" / "0000014_OEM_erfolgreich.TXT"))
    pairs = {(candidate.request_id, candidate.response_id): candidate for candidate in analysis.candidates}
    assert (0x710, 0x77A) in pairs
    assert pairs[(0x710, 0x77A)].confidence == "low"
    assert (0x7E1, 0x77A) not in pairs

    summary = summarize_for_ui(
        analysis.candidates,
        metadata_vin=analysis.metadata.get("vin"),
        messages=analysis.messages,
    )
    conversations = summary["conversations"]
    assert len(conversations) >= 1

    broadcast_rows = [row for row in conversations if row.get("isBroadcast")]
    assert len(broadcast_rows) >= 1
    assert all(row.get("isRecommended") is False for row in broadcast_rows)
    assert all((row.get("responderCount") or 0) >= 2 for row in broadcast_rows)
    assert max(row.get("responderCount") or 0 for row in broadcast_rows) >= 20
    broadcast_700 = [row for row in broadcast_rows if row["requestIdHex"] == "0x700"]
    assert len(broadcast_700) >= 20
    responders_700 = {row["responseIdHex"] for row in broadcast_700}
    assert {"0x77E", "0x776", "0x79B", "0x774", "0x79C", "0x7DD", "0x17FE00A9", "0x17FE0096"}.issubset(
        responders_700
    )
    assert any(row["pair"] == "0x710 -> 0x77A" for row in conversations)
    assert not any(row["pair"] == "0x7E1 -> 0x77A" for row in conversations)

    primary_row = next((row for row in conversations if row["pair"] == "0x7E1 -> 0x7E9"), None)
    assert primary_row is not None
    assert primary_row.get("isBroadcast") is False
    assert primary_row.get("isRecommended") is True
