"""
Unit-тести для telemetry.py (AGX_STEPAN_METRIC tag) — v2.7.2.

Перевіряє:
1. tlog() форматує рядок з тегом AGX_STEPAN_METRIC
2. tlog() коректно серіалізує типи (bool, int, float, list, dict)
3. depth_classifier — логи depth=light/deep мають тег
4. memory_manager.push_recent_topic — topics_push=true/false мають тег
5. Безпека: tlog() не кидає виняток якщо value некоректний
6. PII-safe: user_id/chat_id анонімізуються у tlog() (формат h:xxxxxxxxxx)
7. anonymize_id() — коректність, стабільність, edge cases
"""

import logging
import sys
from pathlib import Path

root = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(root))
sys.path.insert(0, str(root / 'packages' / 'agromatrix-tools'))

from crews.agromatrix_crew.telemetry import tlog, TELEMETRY_TAG, _fmt_value, anonymize_id
from crews.agromatrix_crew.depth_classifier import classify_depth
from crews.agromatrix_crew.memory_manager import (
    _default_user_profile,
    push_recent_topic,
)


# ─── tlog() unit ─────────────────────────────────────────────────────────────

class _CaptureHandler(logging.Handler):
    """Ловить LogRecord-и для перевірки."""
    def __init__(self):
        super().__init__()
        self.records: list[logging.LogRecord] = []

    def emit(self, record: logging.LogRecord) -> None:
        self.records.append(record)

    @property
    def messages(self) -> list[str]:
        return [r.getMessage() for r in self.records]


def _capture_logger(name: str) -> tuple[logging.Logger, _CaptureHandler]:
    lg = logging.getLogger(name)
    lg.setLevel(logging.DEBUG)
    h = _CaptureHandler()
    lg.addHandler(h)
    return lg, h


def test_tlog_contains_tag():
    lg, h = _capture_logger("test_tag")
    tlog(lg, "depth", depth="light", reason="greeting")
    assert any(TELEMETRY_TAG in m for m in h.messages), \
        f"Expected {TELEMETRY_TAG!r} in log, got: {h.messages}"


def test_tlog_message_format():
    lg, h = _capture_logger("test_fmt")
    tlog(lg, "crew_launch", launched=True, depth="deep")
    msg = h.messages[-1]
    assert msg.startswith(TELEMETRY_TAG)
    assert "crew_launch" in msg
    assert "launched=true" in msg
    assert "depth=deep" in msg


def test_tlog_bool_lowercase():
    lg, h = _capture_logger("test_bool")
    tlog(lg, "test_event", flag_a=True, flag_b=False)
    msg = h.messages[-1]
    assert "flag_a=true" in msg
    assert "flag_b=false" in msg


def test_tlog_list_joined():
    lg, h = _capture_logger("test_list")
    tlog(lg, "agents", agents=["ops", "iot", "platform"])
    msg = h.messages[-1]
    assert "ops,iot,platform" in msg


def test_tlog_dict_compact_json():
    lg, h = _capture_logger("test_dict")
    tlog(lg, "event", meta={"key": "val"})
    msg = h.messages[-1]
    assert '"key"' in msg or "key" in msg


def test_tlog_float_formatted():
    lg, h = _capture_logger("test_float")
    tlog(lg, "confidence", score=0.75)
    msg = h.messages[-1]
    assert "score=0.75" in msg


def test_tlog_no_kv():
    lg, h = _capture_logger("test_nokv")
    tlog(lg, "simple_event")
    msg = h.messages[-1]
    assert TELEMETRY_TAG in msg
    assert "simple_event" in msg


def test_tlog_safe_on_bad_value():
    """tlog() не має кидати виняток навіть при некоректному value."""
    lg, h = _capture_logger("test_safe")

    class _Bad:
        def __str__(self):
            raise RuntimeError("bad")

    # Should NOT raise
    tlog(lg, "event", bad=_Bad())
    # At least one message logged (fallback)
    assert len(h.messages) >= 1


# ─── _fmt_value unit ──────────────────────────────────────────────────────────

def test_fmt_bool_true():
    assert _fmt_value(True) == "true"


def test_fmt_bool_false():
    assert _fmt_value(False) == "false"


def test_fmt_int():
    assert _fmt_value(42) == "42"


def test_fmt_float():
    assert _fmt_value(3.14) == "3.14"


def test_fmt_list():
    assert _fmt_value(["a", "b", "c"]) == "a,b,c"


def test_fmt_str():
    assert _fmt_value("hello") == "hello"


# ─── depth_classifier logs tagged ────────────────────────────────────────────

def test_depth_classifier_greeting_logs_tagged():
    """classify_depth("привіт") має emitнути рядок з TELEMETRY_TAG."""
    dc_logger = logging.getLogger("crews.agromatrix_crew.depth_classifier")
    dc_logger.setLevel(logging.DEBUG)
    h = _CaptureHandler()
    dc_logger.addHandler(h)
    try:
        classify_depth("привіт")
        assert any(TELEMETRY_TAG in m for m in h.messages), \
            f"No tagged log for greeting. Messages: {h.messages}"
    finally:
        dc_logger.removeHandler(h)


def test_depth_classifier_deep_logs_tagged():
    """classify_depth("зроби план на тиждень") → тег присутній."""
    dc_logger = logging.getLogger("crews.agromatrix_crew.depth_classifier")
    dc_logger.setLevel(logging.DEBUG)
    h = _CaptureHandler()
    dc_logger.addHandler(h)
    try:
        classify_depth("зроби план на тиждень")
        assert any(TELEMETRY_TAG in m for m in h.messages), \
            f"No tagged log for deep. Messages: {h.messages}"
    finally:
        dc_logger.removeHandler(h)


def test_depth_classifier_followup_logs_tagged():
    """classify_depth("а на завтра?", last_topic=...) → тег присутній."""
    dc_logger = logging.getLogger("crews.agromatrix_crew.depth_classifier")
    dc_logger.setLevel(logging.DEBUG)
    h = _CaptureHandler()
    dc_logger.addHandler(h)
    try:
        classify_depth("а на завтра?", last_topic="plan_day")
        assert any(TELEMETRY_TAG in m for m in h.messages), \
            f"No tagged log for followup. Messages: {h.messages}"
    finally:
        dc_logger.removeHandler(h)


# ─── memory_manager.push_recent_topic logs tagged ────────────────────────────

def test_push_recent_topic_push_tagged():
    """push_recent_topic → topics_push лог має тег."""
    mm_logger = logging.getLogger("crews.agromatrix_crew.memory_manager")
    mm_logger.setLevel(logging.DEBUG)
    h = _CaptureHandler()
    mm_logger.addHandler(h)
    try:
        profile = _default_user_profile("u_tag_push")
        push_recent_topic(profile, "plan_day", "план на завтра")
        tagged = [m for m in h.messages if TELEMETRY_TAG in m]
        assert tagged, f"No tagged log for topics_push. Messages: {h.messages}"
        assert any("topics_push" in m for m in tagged)
        assert any("pushed=true" in m for m in tagged)
    finally:
        mm_logger.removeHandler(h)


def test_push_recent_topic_dedup_tagged():
    """push_recent_topic dedup → topics_push=false лог має тег."""
    mm_logger = logging.getLogger("crews.agromatrix_crew.memory_manager")
    mm_logger.setLevel(logging.DEBUG)
    h = _CaptureHandler()
    mm_logger.addHandler(h)
    try:
        profile = _default_user_profile("u_tag_dedup")
        push_recent_topic(profile, "plan_day", "план на завтра")
        h.records.clear()
        push_recent_topic(profile, "plan_day", "план на завтра")  # dedup
        tagged = [m for m in h.messages if TELEMETRY_TAG in m]
        assert tagged, f"No tagged log for dedup. Messages: {h.messages}"
        assert any("pushed=false" in m for m in tagged)
    finally:
        mm_logger.removeHandler(h)


# ─── Log level passthrough ────────────────────────────────────────────────────

def test_tlog_default_level_is_info():
    """tlog без level= використовує INFO."""
    lg, h = _capture_logger("test_level")
    tlog(lg, "event")
    assert h.records[-1].levelno == logging.INFO


def test_tlog_warning_level():
    """tlog з level=logging.WARNING записує WARNING."""
    lg, h = _capture_logger("test_warn_level")
    import logging as _logging
    tlog(lg, "fallback_event", level=_logging.WARNING, reason="timeout")
    assert h.records[-1].levelno == _logging.WARNING


# ─── anonymize_id unit ────────────────────────────────────────────────────────

def test_anonymize_none_returns_none():
    assert anonymize_id(None) is None


def test_anonymize_empty_returns_empty():
    assert anonymize_id("") == ""


def test_anonymize_format_h_prefix():
    result = anonymize_id("123456789")
    assert result is not None
    assert result.startswith("h:")
    assert len(result) == 12  # "h:" + 10 hex chars


def test_anonymize_hex_chars_only():
    """Хеш частина містить тільки hex символи."""
    import re
    result = anonymize_id("some_user_id")
    assert result is not None
    hash_part = result[2:]  # skip "h:"
    assert re.fullmatch(r'[0-9a-f]{10}', hash_part), \
        f"Expected 10 lowercase hex chars, got: {hash_part!r}"


def test_anonymize_stable_same_input():
    """Той самий input → той самий псевдонім (детерміновано)."""
    a = anonymize_id("user_42")
    b = anonymize_id("user_42")
    assert a == b


def test_anonymize_different_inputs_different_hashes():
    """Різні inputs → різні псевдоніми (з високою ймовірністю)."""
    a = anonymize_id("user_1")
    b = anonymize_id("user_2")
    assert a != b


def test_anonymize_int_via_str():
    """Типовий user_id як число (stringified)."""
    result = anonymize_id("987654321")
    assert result is not None
    assert result.startswith("h:")
    assert len(result) == 12


def test_anonymize_telegram_negative_chat_id():
    """Telegram chat_id може бути від'ємним числом."""
    result = anonymize_id("-1001234567890")
    assert result is not None
    assert result.startswith("h:")
    assert len(result) == 12


# ─── tlog PII-safe behavior ───────────────────────────────────────────────────

def test_tlog_user_id_is_anonymized():
    """user_id=<raw> → у лог-рядку з'являється h:... а не сире значення."""
    lg, h = _capture_logger("test_pii_uid")
    raw_user_id = "987654321"
    tlog(lg, "memory_save", user_id=raw_user_id, ok=True)
    msg = h.messages[-1]
    # Сирого id не має бути у рядку
    assert raw_user_id not in msg, \
        f"Raw user_id found in log: {msg!r}"
    # Замість нього має бути анонімізований псевдонім
    assert "user_id=h:" in msg, f"Expected 'user_id=h:' in: {msg!r}"


def test_tlog_chat_id_is_anonymized():
    """chat_id → анонімізується автоматично."""
    lg, h = _capture_logger("test_pii_cid")
    raw_chat_id = "-1001234567890"
    tlog(lg, "memory_save", entity="FarmProfile", chat_id=raw_chat_id, ok=True)
    msg = h.messages[-1]
    assert raw_chat_id not in msg, \
        f"Raw chat_id found in log: {msg!r}"
    assert "chat_id=h:" in msg, f"Expected 'chat_id=h:' in: {msg!r}"


def test_tlog_non_pii_key_not_anonymized():
    """Звичайні ключі (не user_id/chat_id) не анонімізуються."""
    lg, h = _capture_logger("test_nonpii")
    tlog(lg, "depth", depth="light", reason="greeting", words=3)
    msg = h.messages[-1]
    assert "depth=light" in msg
    assert "reason=greeting" in msg
    assert "words=3" in msg


def test_tlog_pii_anonymize_format_stable():
    """Один і той самий user_id → той самий псевдонім у різних tlog викликах."""
    lg, h = _capture_logger("test_pii_stable")
    uid = "user_test_stable"
    tlog(lg, "event_a", user_id=uid)
    tlog(lg, "event_b", user_id=uid)
    # Витягаємо псевдоніми
    anon_a = [p for p in h.messages[0].split() if p.startswith("user_id=")][0]
    anon_b = [p for p in h.messages[1].split() if p.startswith("user_id=")][0]
    assert anon_a == anon_b, \
        f"Unstable anonymization: {anon_a!r} vs {anon_b!r}"


def test_tlog_custom_pii_keys():
    """Можна передати власний set pii_keys для додаткових полів."""
    lg, h = _capture_logger("test_custom_pii")
    tlog(lg, "custom", pii_keys=frozenset({"session_id"}), session_id="abc123xyz")
    msg = h.messages[-1]
    assert "abc123xyz" not in msg
    assert "session_id=h:" in msg


def test_tlog_none_user_id_is_null():
    """None user_id → логується як null (не помилка)."""
    lg, h = _capture_logger("test_pii_none")
    tlog(lg, "event", user_id=None)
    msg = h.messages[-1]
    assert "user_id=null" in msg


def test_memory_manager_logs_no_raw_user_id():
    """
    Перевіряємо що memory_manager tlog логи не містять сирого user_id.
    Симулюємо через push_recent_topic (логи topics_push).
    """
    mm_logger = logging.getLogger("crews.agromatrix_crew.memory_manager")
    mm_logger.setLevel(logging.DEBUG)
    h = _CaptureHandler()
    mm_logger.addHandler(h)
    try:
        from crews.agromatrix_crew.memory_manager import _default_user_profile, push_recent_topic
        raw_uid = "99887766554"
        profile = _default_user_profile(raw_uid)
        push_recent_topic(profile, "plan_day", "план на завтра")
        for msg in h.messages:
            if TELEMETRY_TAG in msg:
                assert raw_uid not in msg, \
                    f"Raw user_id found in telemetry log: {msg!r}"
    finally:
        mm_logger.removeHandler(h)
