"""Auto-matching for custom ACE-RFID filament tags (Issue #101).

Anycubic's ACE RFID system concatenates vendor + material + a truncated
serial into one `type` string for custom (third-party) tags, e.g.
"GEEETECH PLA Bas" (vendor "Geeetech", material "PLA", serial "Bas" for
"Basic"). Previously the bridge treated this whole string as an unknown
material and fell back to a neutral "Generic <type>" profile, even though
the user had already imported a matching OrcaSlicer profile via the ZIP
import feature (Issue #41) - requiring a manual per-slot reassignment every
time that spool was loaded.

_parse_combined_rfid_type() + _match_profile_by_vendor_family() resolve this
automatically against the merged system+user filament library.
"""
import argparse
import tempfile
from unittest.mock import MagicMock

from kobrax_moonraker_bridge import KobraXBridge

USER_PROFILES = [
    {"id": "OGFL99", "name": "Generic PLA", "vendor": "Generic", "type": "PLA", "color": ""},
    {"id": "GTPLA01", "name": "Geeetech PLA Basic", "vendor": "Geeetech", "type": "PLA", "color": "", "is_user": True},
]


def _bridge(profiles=USER_PROFILES):
    c = MagicMock(); c.callbacks = {}; c.connected = False
    args = argparse.Namespace(
        printer_ip="", mqtt_port=9883, username="", password="",
        mode_id="20030", device_id="", host="127.0.0.1", port=7125,
        data_dir=tempfile.mkdtemp(prefix="kxrfid-"),
    )
    b = KobraXBridge(c, args=args)
    b._orca_filaments_cache = profiles
    return b


def test_combined_rfid_type_parses_known_vendor_and_family():
    b = _bridge()
    vendor, family = b._parse_combined_rfid_type("GEEETECH PLA Bas")
    assert vendor == "Geeetech"
    assert family == "PLA"


def test_plain_type_string_is_unaffected():
    """Regression guard: a normal type="PLA" report (no vendor prefix) must
    not be mistaken for a combined RFID string."""
    b = _bridge()
    vendor, family = b._parse_combined_rfid_type("PLA")
    assert vendor == ""
    assert family == ""


def test_unknown_vendor_prefix_returns_no_match():
    b = _bridge()
    vendor, family = b._parse_combined_rfid_type("TOTALLYUNKNOWNBRAND PLA Bas")
    assert vendor == ""
    assert family == ""


def test_match_profile_by_vendor_family_finds_imported_profile():
    b = _bridge()
    profile = b._match_profile_by_vendor_family("Geeetech", "PLA")
    assert profile.get("name") == "Geeetech PLA Basic"


def test_match_profile_by_vendor_family_no_match_returns_empty():
    b = _bridge()
    profile = b._match_profile_by_vendor_family("Geeetech", "PETG")
    assert profile == {}


def test_match_profile_by_vendor_family_ambiguous_picks_first_without_crashing():
    profiles = USER_PROFILES + [
        {"id": "GTPLA02", "name": "Geeetech PLA Silk", "vendor": "Geeetech", "type": "PLA SILK", "color": "", "is_user": True},
    ]
    b = _bridge(profiles)
    profile = b._match_profile_by_vendor_family("Geeetech", "PLA")
    assert profile.get("name") in ("Geeetech PLA Basic", "Geeetech PLA Silk")


def test_build_lane_data_auto_resolves_combined_rfid_slot():
    """End-to-end: a slot reporting the combined RFID string should surface
    the imported Geeetech profile in lane_data instead of the Generic
    fallback."""
    b = _bridge()
    b._filament_profiles = {}  # no manual per-slot override - isolate the auto-match path
    b._filament_mode = "ace_hub"
    b._ams_slots = [
        {"global_index": 0, "box_id": 0, "status": 5, "type": "GEEETECH PLA Bas", "color": [238, 190, 152]},
    ]
    lane = b._build_lane_data()
    tray = lane["ams"][0]["tray"][0]
    assert tray["vendor_name"] == "Geeetech"
    assert tray["name"] == "Geeetech PLA Basic"


def test_build_lane_data_plain_type_still_uses_generic_fallback():
    """Regression guard: everyday type="PLA" slots must keep using the
    existing Generic-library fallback, unaffected by the new matching path."""
    b = _bridge()
    b._filament_profiles = {}  # no manual per-slot override - isolate the fallback path
    b._filament_mode = "ace_hub"
    b._ams_slots = [
        {"global_index": 0, "box_id": 0, "status": 5, "type": "PLA", "color": [255, 255, 255]},
    ]
    lane = b._build_lane_data()
    tray = lane["ams"][0]["tray"][0]
    assert tray["name"] == "Generic PLA"
    assert tray["vendor_name"] == "Generic"


# ─── Centralized matching via _effective_slot_profile() ────────────────────
#
# The bug reported in Issue #101 by @Blaim (nightly45 not working, despite
# _parse_combined_rfid_type()/_match_profile_by_vendor_family() existing):
# those two helpers were ONLY ever invoked inside _build_lane_data(), which
# is only reached when OrcaSlicer polls the Moonraker lane_data endpoint -
# never as part of the real MQTT receive path (_on_multicolor_box ->
# self._ams_slots -> _push_status_update -> dashboard / /kx/filament/slots).
# So the dashboard and the Happy-Hare gate data never saw a match, matching
# exactly what the user's screenshots showed. Fixed by moving the matching
# logic into _effective_slot_profile() itself, which all three consumers
# already call.

def test_effective_slot_profile_auto_resolves_raw_rfid_string_without_override():
    """The core Issue #101 regression: _effective_slot_profile() itself (not
    just _build_lane_data()) must resolve a combined RFID string when there is
    no manual per-slot override in config.ini."""
    b = _bridge()
    b._filament_profiles = {}
    profile = b._effective_slot_profile(0, "GEEETECH PLA Bas")
    assert profile.get("name") == "Geeetech PLA Basic"
    assert profile.get("vendor") == "Geeetech"


def test_effective_slot_profile_manual_override_still_wins():
    b = _bridge()
    b._filament_profiles = {0: {"id": "OGFL99", "name": "Generic PLA", "vendor": "Generic"}}
    profile = b._effective_slot_profile(0, "GEEETECH PLA Bas")
    assert profile.get("name") == "Generic PLA"


def test_effective_slot_profile_plain_type_no_override_returns_empty():
    """Regression guard: a plain type="PLA" slot with no override must still
    fall through to {} (the caller's own generic-name fallback), not be
    treated as an RFID string."""
    b = _bridge()
    b._filament_profiles = {}
    assert b._effective_slot_profile(0, "PLA") == {}


def _multicolor_box_report(raw_type: str, color=(238, 190, 152)) -> dict:
    """A realistic multiColorBox/report payload for one ACE box (id=0) with
    a toolhead (id=-1), matching the real Kobra X topology captured live
    during Issue #100/#101 investigation - drives _detect_filament_mode()
    to "ace_hub", same as on real hardware."""
    return {
        "state": "success",
        "data": {
            "head_tools_model": 1,
            "multi_color_box": [
                {
                    "id": -1, "loaded_slot": -1,
                    "slots": [{"index": 0, "status": 0, "type": "", "color": [0, 0, 0]}],
                },
                {
                    "id": 0, "loaded_slot": -1,
                    "slots": [
                        {"index": 0, "status": 0, "type": "", "color": [0, 0, 0]},
                        {"index": 1, "status": 0, "type": "", "color": [0, 0, 0]},
                        {
                            "index": 2, "status": 5, "type": raw_type,
                            "color": list(color), "sku": "",
                        },
                        {"index": 3, "status": 0, "type": "", "color": [0, 0, 0]},
                    ],
                },
            ],
        },
    }


def test_on_multicolor_box_end_to_end_resolves_rfid_slot_for_dashboard():
    """End-to-end regression test for Issue #101: feed a raw MQTT
    multiColorBox/report payload through the real receive path
    (_on_multicolor_box), then check that the dashboard-facing
    /kx/filament/slots data (handle_kx_filament_slots) - which is what
    populates the dashboard's window._slotProfileMap in the browser -
    actually reflects the matched profile, not the raw "GEEETECH PLA Bas"
    string. This is the exact path that was broken and untested before."""
    b = _bridge()
    b._filament_profiles = {}
    b._on_multicolor_box(_multicolor_box_report("GEEETECH PLA Bas"))

    assert b._filament_mode == "ace_hub"
    # global_index 6 = box_id 0 * 4 + local slot 2 in ace_hub mode's ACE block
    slot = next(s for s in b._ams_slots if s.get("type") == "GEEETECH PLA Bas")
    global_idx = slot["global_index"]

    profile = b._effective_slot_profile(global_idx, slot["type"])
    assert profile.get("name") == "Geeetech PLA Basic"
    assert profile.get("vendor") == "Geeetech"


def test_match_profile_by_vendor_family_disambiguates_via_variant_tokens():
    """Issue #101 follow-up (@Blaim): two profiles of the same vendor+family
    ("Geeetech PLA Basic" vs. "Geeetech PLA Matte") must resolve to the one
    matching the RFID string's truncated variant token ("bas" -> Basic),
    not just "whichever loads first"."""
    profiles = USER_PROFILES + [
        {"id": "GTPLA03", "name": "Geeetech PLA Matte", "vendor": "Geeetech", "type": "PLA", "color": "", "is_user": True},
    ]
    b = _bridge(profiles)

    basic = b._match_profile_by_vendor_family("Geeetech", "PLA", ["bas"])
    assert basic.get("name") == "Geeetech PLA Basic"

    matte = b._match_profile_by_vendor_family("Geeetech", "PLA", ["mat"])
    assert matte.get("name") == "Geeetech PLA Matte"


def test_match_profile_by_vendor_family_no_variant_tokens_falls_back_to_first():
    profiles = USER_PROFILES + [
        {"id": "GTPLA03", "name": "Geeetech PLA Matte", "vendor": "Geeetech", "type": "PLA", "color": "", "is_user": True},
    ]
    b = _bridge(profiles)
    profile = b._match_profile_by_vendor_family("Geeetech", "PLA")
    assert profile.get("name") == "Geeetech PLA Basic"


def test_rfid_variant_tokens_extracts_tokens_after_vendor_and_family():
    assert KobraXBridge._rfid_variant_tokens("GEEETECH PLA Bas") == ["bas"]
    assert KobraXBridge._rfid_variant_tokens("GEEETECH PLA") == []
    assert KobraXBridge._rfid_variant_tokens("PLA") == []
