"""
Invariant tests для Humanized Stepan v2.7 — захист від "повзучої ботячості".

Ці тести фіксують верхні межі та заборонені патерни.
При будь-якій зміні rule-банків / логіки — інваріанти мають залишатись зеленими.

Інваріанти:
  1. Greeting (light) ≤ 80 символів
  2. Thanks/Ack ≤ 40 символів
  3. Light відповіді не містять заборонених фраз
  4. Light не містить технічних слів з інфраструктури
  5. Weather + ZZR → завжди містить "за етикеткою" або "за регламентом"
  6. recent_topics horizon ≤ 5 після 7 deep взаємодій
  7. Міграція: last_topic без recent_topics → recent_topics з 1 елементом
"""

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.light_reply import (
    build_light_reply,
    classify_light_event,
    _weather_reply,
    _GREETING_NEUTRAL,
    _GREETING_SOFT,
    _GREETING_CONTEXTUAL,
    _GREETING_WITH_TOPIC,
    _THANKS,
    _ACK,
)
from crews.agromatrix_crew.memory_manager import (
    _default_user_profile,
    push_recent_topic,
    migrate_profile_topics,
    summarize_topic_label,
)
from crews.agromatrix_crew.depth_classifier import classify_depth


# ─── Forbidden phrases & technical words ────────────────────────────────────

_FORBIDDEN_PHRASES = [
    "чим можу допомогти",
    "чим допомогти",
    "оберіть",
    "я як агент",
    "я бот",
    "я є бот",
    "я є штучний",
    "я є ai",
]

_TECHNICAL_WORDS = [
    "container", "uvicorn", "trace_id", "STEPAN_IMPORTS_OK",
    "env var", "docker", "crewai", "agromatrix_tools",
]


def _check_no_forbidden(text: str) -> None:
    tl = text.lower()
    for phrase in _FORBIDDEN_PHRASES:
        assert phrase not in tl, f"Forbidden phrase {phrase!r} found in: {text!r}"


def _check_no_technical(text: str) -> None:
    tl = text.lower()
    for word in _TECHNICAL_WORDS:
        assert word not in tl, f"Technical word {word!r} found in: {text!r}"


# ─── Invariant 1: Greeting ≤ 80 chars ────────────────────────────────────────

def test_inv1_all_greeting_no_topic_le_80():
    """Всі greeting фрази без теми ≤ 80 символів (з підстановкою пустого name)."""
    for bank in [_GREETING_NEUTRAL, _GREETING_SOFT, _GREETING_CONTEXTUAL]:
        for template in bank:
            rendered = template.format(name="")
            assert len(rendered) <= 80, \
                f"Greeting template too long ({len(rendered)} > 80): {template!r}"


def test_inv1_greeting_with_topic_le_80():
    """Greeting з темою ≤ 80 символів для типових тем."""
    for template in _GREETING_WITH_TOPIC:
        rendered = template.format(name="", topic="план на день", topic_cap="План на день", text_frag="")
        assert len(rendered) <= 80, \
            f"Topic greeting too long ({len(rendered)} > 80): {template!r}"


def test_inv1_build_greeting_le_80():
    """build_light_reply на привітання повертає ≤ 80 символів."""
    for count in [0, 5, 10]:
        profile = {"user_id": f"u_count_{count}", "name": None, "last_topic": None,
                   "interaction_count": count, "recent_topics": [], "last_topic_label": None}
        reply = build_light_reply("привіт", profile)
        if reply:
            assert len(reply) <= 80, f"Greeting too long (count={count}): {reply!r}"


# ─── Invariant 2: Thanks/Ack ≤ 40 chars ──────────────────────────────────────

def test_inv2_all_thanks_le_40():
    for phrase in _THANKS:
        assert len(phrase) <= 40, f"Thanks phrase too long: {phrase!r}"


def test_inv2_all_ack_le_40():
    for phrase in _ACK:
        assert len(phrase) <= 40, f"Ack phrase too long: {phrase!r}"


def test_inv2_build_thanks_le_40():
    profile = {"user_id": "u1", "name": None, "last_topic": None, "interaction_count": 0}
    for text in ["дякую", "спасибі", "велике дякую"]:
        reply = build_light_reply(text, profile)
        if reply:
            assert len(reply) <= 40, f"Thanks too long: {reply!r}"


def test_inv2_build_ack_le_40():
    profile = {"user_id": "u2", "name": None, "last_topic": None, "interaction_count": 0}
    for text in ["ок", "зрозумів", "добре", "чудово"]:
        reply = build_light_reply(text, profile)
        if reply:
            assert len(reply) <= 40, f"Ack too long: {reply!r}"


# ─── Invariant 3: No forbidden phrases ───────────────────────────────────────

def test_inv3_greeting_no_forbidden():
    for bank in [_GREETING_NEUTRAL, _GREETING_SOFT, _GREETING_CONTEXTUAL, _GREETING_WITH_TOPIC]:
        for template in bank:
            rendered = template.format(name="", topic="план", topic_cap="План", text_frag="")
            _check_no_forbidden(rendered)


def test_inv3_thanks_no_forbidden():
    for phrase in _THANKS:
        _check_no_forbidden(phrase)


def test_inv3_ack_no_forbidden():
    for phrase in _ACK:
        _check_no_forbidden(phrase)


def test_inv3_build_replies_no_forbidden():
    profile = {"user_id": "u3", "name": None, "last_topic": "plan_day",
               "interaction_count": 5, "recent_topics": [], "last_topic_label": "план на завтра"}
    for text in ["привіт", "дякую", "ок", "зрозумів", "а на завтра?"]:
        reply = build_light_reply(text, profile)
        if reply:
            _check_no_forbidden(reply)


# ─── Invariant 4: No technical words ─────────────────────────────────────────

def test_inv4_all_banks_no_technical():
    all_phrases = (
        _GREETING_NEUTRAL + _GREETING_SOFT + _GREETING_CONTEXTUAL
        + _GREETING_WITH_TOPIC + _THANKS + _ACK
    )
    for phrase in all_phrases:
        _check_no_technical(phrase)


# ─── Invariant 5: Weather + ZZR → disclaimer ──────────────────────────────────

def test_inv5_weather_zzr_has_disclaimer():
    """Якщо текст містить ZZR + погодний тригер — відповідь містить застереження."""
    zzr_texts = [
        "обприскування гербіцидом якщо дощ",
        "обробка фунгіцидом при дощі",
        "ЗЗР і сильний вітер",
        "застосування пестициду — є мороз",
    ]
    for text in zzr_texts:
        reply = _weather_reply(text, None)
        if reply:  # may be None if no matching rule — that's ok, but if not None → must have disclaimer
            assert "за етикеткою" in reply or "за регламентом" in reply, \
                f"ZZR weather reply missing disclaimer: {reply!r} for text: {text!r}"


def test_inv5_weather_no_zzr_no_disclaimer():
    """Звичайний погодний запит без ZZR — без застереження."""
    reply = _weather_reply("а якщо дощ?", {"season_state": "growing"})
    assert reply is not None
    assert "за етикеткою" not in reply
    assert "за регламентом" not in reply


def test_inv5_zzr_rain_disclaimer_present():
    """Конкретний кейс: 'обприскування якщо дощ' → disclaimer."""
    reply = _weather_reply("обприскування якщо дощ", {"season_state": "growing"})
    assert reply is not None
    assert "за етикеткою" in reply or "за регламентом" in reply


# ─── Invariant 6: recent_topics horizon ≤ 5 ─────────────────────────────────

def test_inv6_horizon_after_7_deep():
    """Після 7 push_recent_topic → len(recent_topics) == 5."""
    profile = _default_user_profile("u_horizon")
    for i in range(7):
        push_recent_topic(profile, f"intent_{i}", f"Тема {i}")
    assert len(profile["recent_topics"]) == 5, \
        f"Expected 5, got {len(profile['recent_topics'])}"


def test_inv6_horizon_order_preserves_latest():
    """recent_topics зберігає 5 найновіших, не перших."""
    profile = _default_user_profile("u_order")
    for i in range(7):
        push_recent_topic(profile, f"intent_{i}", f"Тема {i}")
    labels = [t["label"] for t in profile["recent_topics"]]
    assert "Тема 2" in labels  # third oldest of 7 (7-5=2)
    assert "Тема 6" in labels  # latest
    assert "Тема 0" not in labels  # first was dropped
    assert "Тема 1" not in labels  # second was dropped


# ─── Invariant 7: migration ───────────────────────────────────────────────────

def test_inv7_migration_adds_recent_topics():
    """Профіль з last_topic але без recent_topics → міграція додає recent_topics."""
    old_profile = {
        "user_id": "u_old",
        "last_topic": "plan_day",
        "interaction_count": 5,
    }
    changed = migrate_profile_topics(old_profile)
    assert changed is True
    assert "recent_topics" in old_profile
    assert len(old_profile["recent_topics"]) == 1
    assert old_profile["recent_topics"][0]["intent"] == "plan_day"


def test_inv7_migration_adds_last_topic_label():
    """Профіль без last_topic_label → міграція додає поле."""
    profile = {
        "user_id": "u_nolabel",
        "last_topic": "plan_vs_fact",
        "recent_topics": [{"label": "план/факт", "intent": "plan_vs_fact", "ts": "2026-01-01"}],
    }
    changed = migrate_profile_topics(profile)
    assert "last_topic_label" in profile
    assert profile["last_topic_label"] == "план/факт"


def test_inv7_migration_idempotent():
    """Повторна міграція не змінює вже мігрований профіль."""
    profile = _default_user_profile("u_idem")
    push_recent_topic(profile, "plan_day", "план на день")
    changed_first = migrate_profile_topics(profile)
    # Already has everything → no change
    changed_second = migrate_profile_topics(profile)
    assert changed_second is False


def test_inv7_migration_tone_constraints():
    """Профіль без tone_constraints → міграція додає."""
    profile = {
        "user_id": "u_notc",
        "preferences": {"units": "ha"},
    }
    changed = migrate_profile_topics(profile)
    assert "tone_constraints" in profile["preferences"]


# ─── summarize_topic_label ────────────────────────────────────────────────────

def test_label_removes_action_verb():
    label = summarize_topic_label("зроби план на завтра по полю 12")
    assert "зроби" not in label.lower()
    assert "план" in label.lower() or "завтра" in label.lower()


def test_label_max_8_words():
    text = "зроби детальний план на завтра по полям один два три чотири пять"
    label = summarize_topic_label(text)
    assert len(label.split()) <= 9  # ≤8 words + minor slack for strip


def test_label_preserves_field_number():
    label = summarize_topic_label("перевір вологість на полі 7")
    # "полі" is a stop-word but "7" or "поле/поля" may be kept
    assert label  # just assert non-empty


def test_label_nonempty_for_short_text():
    label = summarize_topic_label("план")
    assert len(label) > 0
