"""
Mixed-format integration tests for the dashboard payload parser.

Tests cover:
    - Legacy top-level payload is rejected (v2-only mode)
  - v2 grouped payload: periodic capture
  - v2 grouped payload: event_start capture
  - v2 grouped payload: event_stop capture
    - Classification into v2_success / parse_failures
  - Soft required-field validation (v2 only, never drops message)
  - Edge cases: missing image, missing status, non-dict payload
"""

import sys
import os
import logging
import importlib.util

# listener/ has no __init__.py — load the module directly from its file path
os.environ.setdefault("DB_CONN", "sqlite:///:memory:")  # prevent DB engine errors on import
_LISTENER_PATH = os.path.join(os.path.dirname(__file__), "listener.py")
_spec = importlib.util.spec_from_file_location("listener_module", _LISTENER_PATH)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)

_extract_dashboard_payload_fields = _mod._extract_dashboard_payload_fields
_classify_dashboard_payload       = _mod._classify_dashboard_payload
_validate_v2_required_fields      = _mod._validate_v2_required_fields
_normalize_screenshot_type        = _mod._normalize_screenshot_type
DASHBOARD_PARSE_METRICS           = _mod.DASHBOARD_PARSE_METRICS

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

IMAGE_B64 = "aGVsbG8="  # base64("hello")

LEGACY_PAYLOAD = {
    "client_id": "uuid-legacy",
    "status": "alive",
    "screenshot": {
        "data": IMAGE_B64,
        "timestamp": "2026-03-30T10:00:00+00:00",
    },
    "screenshot_type": "periodic",
    "process_health": {
        "current_process": "impressive",
        "process_pid": 1234,
        "process_status": "running",
        "event_id": 42,
    },
}

def _make_v2(capture_type):
    return {
        "message": {
            "client_id": "uuid-v2",
            "status": "alive",
        },
        "content": {
            "screenshot": {
                "filename": "latest.jpg",
                "data": IMAGE_B64,
                "timestamp": "2026-03-30T10:15:41.123456+00:00",
                "size": 6,
            }
        },
        "runtime": {
            "system_info": {
                "hostname": "pi-display-01",
                "ip": "192.168.1.42",
                "uptime": 12345.0,
            },
            "process_health": {
                "event_id": "evt-7",
                "event_type": "presentation",
                "current_process": "impressive",
                "process_pid": 4123,
                "process_status": "running",
                "restart_count": 0,
            },
        },
        "metadata": {
            "schema_version": "2.0",
            "producer": "simclient",
            "published_at": "2026-03-30T10:15:42.004321+00:00",
            "capture": {
                "type": capture_type,
                "captured_at": "2026-03-30T10:15:41.123456+00:00",
                "age_s": 0.9,
                "triggered": capture_type != "periodic",
                "send_immediately": capture_type != "periodic",
            },
            "transport": {"qos": 0, "publisher": "simclient"},
        },
    }

V2_PERIODIC   = _make_v2("periodic")
V2_EVT_START  = _make_v2("event_start")
V2_EVT_STOP   = _make_v2("event_stop")

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def assert_eq(label, actual, expected):
    assert actual == expected, f"FAIL [{label}]: expected {expected!r}, got {actual!r}"

def assert_not_none(label, actual):
    assert actual is not None, f"FAIL [{label}]: expected non-None, got None"

def assert_none(label, actual):
    assert actual is None, f"FAIL [{label}]: expected None, got {actual!r}"

def assert_warns(label, fn, substring):
    """Assert that fn() emits a logging.WARNING containing substring."""
    records = []
    handler = logging.handlers_collector(records)
    logger = logging.getLogger()
    logger.addHandler(handler)
    try:
        fn()
    finally:
        logger.removeHandler(handler)
    warnings = [r.getMessage() for r in records if r.levelno == logging.WARNING]
    assert any(substring in w for w in warnings), (
        f"FAIL [{label}]: no WARNING containing {substring!r} found in {warnings}"
    )


class _CapturingHandler(logging.Handler):
    def __init__(self, records):
        super().__init__()
        self._records = records

    def emit(self, record):
        self._records.append(record)


def capture_warnings(fn):
    """Run fn(), return list of WARNING message strings."""
    records = []
    handler = _CapturingHandler(records)
    logger = logging.getLogger()
    logger.addHandler(handler)
    try:
        fn()
    finally:
        logger.removeHandler(handler)
    return [r.getMessage() for r in records if r.levelno == logging.WARNING]


# ---------------------------------------------------------------------------
# Tests: _normalize_screenshot_type
# ---------------------------------------------------------------------------

def test_normalize_known_types():
    for t in ("periodic", "event_start", "event_stop"):
        assert_eq(f"normalize_{t}", _normalize_screenshot_type(t), t)
        assert_eq(f"normalize_{t}_upper", _normalize_screenshot_type(t.upper()), t)

def test_normalize_unknown_returns_none():
    assert_none("normalize_unknown", _normalize_screenshot_type("unknown"))
    assert_none("normalize_none", _normalize_screenshot_type(None))
    assert_none("normalize_empty", _normalize_screenshot_type(""))

# ---------------------------------------------------------------------------
# Tests: _classify_dashboard_payload
# ---------------------------------------------------------------------------

def test_classify_legacy():
    mode, ver = _classify_dashboard_payload(LEGACY_PAYLOAD)
    assert_eq("classify_legacy_mode", mode, "parse_failures")
    assert_none("classify_legacy_version", ver)

def test_classify_v2_periodic():
    mode, ver = _classify_dashboard_payload(V2_PERIODIC)
    assert_eq("classify_v2_periodic_mode", mode, "v2_success")
    assert_eq("classify_v2_periodic_version", ver, "2.0")

def test_classify_v2_event_start():
    mode, ver = _classify_dashboard_payload(V2_EVT_START)
    assert_eq("classify_v2_event_start_mode", mode, "v2_success")

def test_classify_v2_event_stop():
    mode, ver = _classify_dashboard_payload(V2_EVT_STOP)
    assert_eq("classify_v2_event_stop_mode", mode, "v2_success")

def test_classify_non_dict():
    mode, ver = _classify_dashboard_payload("not a dict")
    assert_eq("classify_non_dict", mode, "parse_failures")

def test_classify_empty_dict():
    mode, ver = _classify_dashboard_payload({})
    assert_eq("classify_empty_dict", mode, "parse_failures")

# ---------------------------------------------------------------------------
# Tests: _extract_dashboard_payload_fields — legacy payload rejected in v2-only mode
# ---------------------------------------------------------------------------

def test_legacy_image_not_extracted():
    r = _extract_dashboard_payload_fields(LEGACY_PAYLOAD)
    assert_none("legacy_image", r["image"])

def test_legacy_screenshot_type_missing():
    r = _extract_dashboard_payload_fields(LEGACY_PAYLOAD)
    assert_none("legacy_screenshot_type", r["screenshot_type"])

def test_legacy_status_missing():
    r = _extract_dashboard_payload_fields(LEGACY_PAYLOAD)
    assert_none("legacy_status", r["status"])

def test_legacy_process_health_empty():
    r = _extract_dashboard_payload_fields(LEGACY_PAYLOAD)
    assert_eq("legacy_process_health", r["process_health"], {})

def test_legacy_timestamp_missing():
    r = _extract_dashboard_payload_fields(LEGACY_PAYLOAD)
    assert_none("legacy_timestamp", r["timestamp"])

# ---------------------------------------------------------------------------
# Tests: _extract_dashboard_payload_fields — v2 periodic
# ---------------------------------------------------------------------------

def test_v2_periodic_image():
    r = _extract_dashboard_payload_fields(V2_PERIODIC)
    assert_eq("v2_periodic_image", r["image"], IMAGE_B64)

def test_v2_periodic_screenshot_type():
    r = _extract_dashboard_payload_fields(V2_PERIODIC)
    assert_eq("v2_periodic_type", r["screenshot_type"], "periodic")

def test_v2_periodic_status():
    r = _extract_dashboard_payload_fields(V2_PERIODIC)
    assert_eq("v2_periodic_status", r["status"], "alive")

def test_v2_periodic_process_health():
    r = _extract_dashboard_payload_fields(V2_PERIODIC)
    assert_eq("v2_periodic_pid", r["process_health"]["process_pid"], 4123)
    assert_eq("v2_periodic_process", r["process_health"]["current_process"], "impressive")

def test_v2_periodic_timestamp_prefers_screenshot():
    r = _extract_dashboard_payload_fields(V2_PERIODIC)
    # screenshot.timestamp must take precedence over capture.captured_at / published_at
    assert_eq("v2_periodic_ts", r["timestamp"], "2026-03-30T10:15:41.123456+00:00")

# ---------------------------------------------------------------------------
# Tests: _extract_dashboard_payload_fields — v2 event_start
# ---------------------------------------------------------------------------

def test_v2_event_start_type():
    r = _extract_dashboard_payload_fields(V2_EVT_START)
    assert_eq("v2_event_start_type", r["screenshot_type"], "event_start")

def test_v2_event_start_image():
    r = _extract_dashboard_payload_fields(V2_EVT_START)
    assert_eq("v2_event_start_image", r["image"], IMAGE_B64)

# ---------------------------------------------------------------------------
# Tests: _extract_dashboard_payload_fields — v2 event_stop
# ---------------------------------------------------------------------------

def test_v2_event_stop_type():
    r = _extract_dashboard_payload_fields(V2_EVT_STOP)
    assert_eq("v2_event_stop_type", r["screenshot_type"], "event_stop")

def test_v2_event_stop_image():
    r = _extract_dashboard_payload_fields(V2_EVT_STOP)
    assert_eq("v2_event_stop_image", r["image"], IMAGE_B64)

# ---------------------------------------------------------------------------
# Tests: _extract_dashboard_payload_fields — edge cases
# ---------------------------------------------------------------------------

def test_non_dict_returns_nulls():
    r = _extract_dashboard_payload_fields("bad")
    assert_none("non_dict_image",   r["image"])
    assert_none("non_dict_type",    r["screenshot_type"])
    assert_none("non_dict_status",  r["status"])

def test_missing_image_returns_none():
    payload = {**V2_PERIODIC, "content": {"screenshot": {"timestamp": "2026-03-30T10:00:00+00:00"}}}
    r = _extract_dashboard_payload_fields(payload)
    assert_none("missing_image", r["image"])

def test_missing_process_health_returns_empty_dict():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["runtime"]["process_health"]
    r = _extract_dashboard_payload_fields(payload)
    assert_eq("missing_ph", r["process_health"], {})

def test_timestamp_fallback_to_captured_at_when_no_screenshot_ts():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["content"]["screenshot"]["timestamp"]
    r = _extract_dashboard_payload_fields(payload)
    assert_eq("ts_fallback_captured_at", r["timestamp"], "2026-03-30T10:15:41.123456+00:00")

def test_timestamp_fallback_to_published_at_when_no_capture_ts():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["content"]["screenshot"]["timestamp"]
    del payload["metadata"]["capture"]["captured_at"]
    r = _extract_dashboard_payload_fields(payload)
    assert_eq("ts_fallback_published_at", r["timestamp"], "2026-03-30T10:15:42.004321+00:00")

# ---------------------------------------------------------------------------
# Tests: _validate_v2_required_fields (soft — never raises)
# ---------------------------------------------------------------------------

def test_v2_valid_payload_no_warnings():
    warnings = capture_warnings(lambda: _validate_v2_required_fields(V2_PERIODIC, "uuid-v2"))
    assert warnings == [], f"FAIL: unexpected warnings for valid payload: {warnings}"

def test_v2_missing_client_id_warns():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["message"]["client_id"]
    warnings = capture_warnings(lambda: _validate_v2_required_fields(payload, "uuid-v2"))
    assert any("message.client_id" in w for w in warnings), f"FAIL: {warnings}"

def test_v2_missing_status_warns():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["message"]["status"]
    warnings = capture_warnings(lambda: _validate_v2_required_fields(payload, "uuid-v2"))
    assert any("message.status" in w for w in warnings), f"FAIL: {warnings}"

def test_v2_missing_schema_version_warns():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["metadata"]["schema_version"]
    warnings = capture_warnings(lambda: _validate_v2_required_fields(payload, "uuid-v2"))
    assert any("metadata.schema_version" in w for w in warnings), f"FAIL: {warnings}"

def test_v2_missing_capture_type_warns():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["metadata"]["capture"]["type"]
    warnings = capture_warnings(lambda: _validate_v2_required_fields(payload, "uuid-v2"))
    assert any("metadata.capture.type" in w for w in warnings), f"FAIL: {warnings}"

def test_v2_multiple_missing_fields_all_reported():
    import copy
    payload = copy.deepcopy(V2_PERIODIC)
    del payload["message"]["client_id"]
    del payload["metadata"]["capture"]["type"]
    warnings = capture_warnings(lambda: _validate_v2_required_fields(payload, "uuid-v2"))
    assert len(warnings) == 1, f"FAIL: expected 1 combined warning, got {warnings}"
    assert "message.client_id" in warnings[0], f"FAIL: {warnings}"
    assert "metadata.capture.type" in warnings[0], f"FAIL: {warnings}"

# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------

def run_all():
    tests = {k: v for k, v in globals().items() if k.startswith("test_") and callable(v)}
    passed = failed = 0
    for name, fn in sorted(tests.items()):
        try:
            fn()
            print(f"  PASS  {name}")
            passed += 1
        except AssertionError as e:
            print(f"  FAIL  {name}: {e}")
            failed += 1
        except Exception as e:
            print(f"  ERROR {name}: {type(e).__name__}: {e}")
            failed += 1
    print(f"\n{passed} passed, {failed} failed out of {passed + failed} tests")
    return failed == 0


if __name__ == "__main__":
    ok = run_all()
    sys.exit(0 if ok else 1)
