"""
tests/test_stepan_doc_mode_hardening_v36.py

v3.6 Doc Mode Hardening:
  - Cooldown blocks implicit doc re-activate (Rule 1)
  - Explicit doc token bypasses cooldown (Rule 3)
  - Fact signal allows implicit doc (Rule 2)
  - No fact signal + no explicit → clarifier
  - Auto-clear sets cooldown (cooldown_set)
  - build_mode_clarifier (URL, vision, fact, neutral)
  - detect_context_signals correctness
  - is_doc_focus_cooldown_active TTL
  - /doc status shows cooldown_left
"""
import time
import pytest


# ─── helpers для ізольованого тестування без crewai ──────────────────────────
from crews.agromatrix_crew.doc_focus import (
    _is_doc_question,
    _detect_domain,
    detect_context_signals,
    build_mode_clarifier,
    handle_doc_focus,
)
from crews.agromatrix_crew.session_context import (
    _default_session,
    is_doc_focus_cooldown_active,
    is_doc_focus_active,
    DOC_FOCUS_TTL,
    DOC_FOCUS_COOLDOWN_S,
)


# ─── 1. detect_context_signals ────────────────────────────────────────────────

class TestDetectContextSignals:
    def test_empty_text(self):
        s = detect_context_signals("")
        assert not any(s.values()), "Empty text should produce all-False signals"

    def test_explicit_doc_token(self):
        s = detect_context_signals("по звіту що таке прибуток?")
        assert s["has_explicit_doc_token"]
        assert s["has_doc_trigger"]

    def test_url_detected(self):
        s = detect_context_signals("дивись https://example.com")
        assert s["has_url"]
        assert not s["has_explicit_doc_token"]

    def test_vision_trigger(self):
        s = detect_context_signals("на листі плями чорні")
        assert s["has_vision_trigger"]
        assert not s["has_explicit_doc_token"]

    def test_fact_signal_units(self):
        s = detect_context_signals("скільки грн/га вийшло?")
        assert s["has_fact_signal"]

    def test_fact_signal_words(self):
        s = detect_context_signals("що з витратами на добрива?")
        assert s["has_fact_signal"]

    def test_no_signals(self):
        s = detect_context_signals("привіт, як справи?")
        assert not s["has_explicit_doc_token"]
        assert not s["has_url"]
        assert not s["has_fact_signal"]

    def test_fact_units_number(self):
        s = detect_context_signals("4500 грн на гектар")
        assert s["has_fact_signal"]


# ─── 2. build_mode_clarifier ──────────────────────────────────────────────────

class TestBuildModeClarifier:
    def test_url_clarifier(self):
        c = build_mode_clarifier("ось посилання https://site.com")
        assert "посилання" in c.lower()
        assert "?" in c
        assert "!" not in c

    def test_vision_clarifier(self):
        c = build_mode_clarifier("на листі плями є")
        assert "фото" in c.lower() or "рослин" in c.lower()

    def test_fact_clarifier(self):
        c = build_mode_clarifier("а що з витратами?")
        assert "цифри" in c.lower() or "звіт" in c.lower()

    def test_neutral_clarifier(self):
        c = build_mode_clarifier("що там?")
        assert "звіт" in c.lower() or "інше" in c.lower()

    def test_no_exclamation(self):
        for text in ["https://x.com", "листя плями", "витрати?", "привіт"]:
            assert "!" not in build_mode_clarifier(text)

    def test_exactly_one_question_mark(self):
        for text in ["https://x.com", "листя плями", "витрати?", "привіт"]:
            assert build_mode_clarifier(text).count("?") == 1


# ─── 3. is_doc_focus_cooldown_active ─────────────────────────────────────────

class TestCooldownHelper:
    def test_inactive_by_default(self):
        s = _default_session()
        assert not is_doc_focus_cooldown_active(s)

    def test_active_when_in_future(self):
        now = time.time()
        s = _default_session()
        s["doc_focus_cooldown_until"] = now + 60
        assert is_doc_focus_cooldown_active(s, now)

    def test_expired(self):
        now = time.time()
        s = _default_session()
        s["doc_focus_cooldown_until"] = now - 1
        assert not is_doc_focus_cooldown_active(s, now)

    def test_zero_is_inactive(self):
        s = _default_session()
        s["doc_focus_cooldown_until"] = 0.0
        assert not is_doc_focus_cooldown_active(s)

    def test_fail_safe_on_garbage(self):
        s = _default_session()
        s["doc_focus_cooldown_until"] = "broken"
        # Should return False without exception
        result = is_doc_focus_cooldown_active(s)
        assert result is False


# ─── 4. DOC_FOCUS_COOLDOWN_S constant ────────────────────────────────────────

class TestCooldownConstants:
    def test_cooldown_120s(self):
        assert DOC_FOCUS_COOLDOWN_S == 120.0

    def test_ttl_600s(self):
        assert DOC_FOCUS_TTL == 600.0


# ─── 5. Gating симуляція (без LLM/HTTP) ──────────────────────────────────────

def _simulate_gating(text: str, session: dict, now: float) -> tuple[str, str | None]:
    """
    Спрощена симуляція логіки context_mode з run.py.
    Повертає (context_mode, denied_reason | None).
    """
    signals = detect_context_signals(text)
    domain = _detect_domain(text)
    focus_active = is_doc_focus_active(session, now)
    cooldown_active = is_doc_focus_cooldown_active(session, now)

    if domain == "doc":
        is_explicit = signals["has_explicit_doc_token"]
        # Rule 1: cooldown blocks implicit
        if cooldown_active and not is_explicit:
            return "general", "cooldown"
        # Rule 2: implicit → needs fact_signal
        if not is_explicit:
            if signals["has_fact_signal"]:
                return "doc", None
            return "general", "no_fact_signal"
        # Rule 3: explicit always allowed
        return "doc", None

    return "general", None


class TestGatingRules:
    """Rule 1/2/3 з v3.6."""

    def test_cooldown_blocks_implicit_doc(self):
        now = time.time()
        s = _default_session()
        s["doc_focus"] = True
        s["doc_focus_ts"] = now
        s["doc_focus_cooldown_until"] = now + 120
        # "документ що там" — загальний тригер (_DOC_QUESTION_RE),
        # але НЕ explicit ("в документі" потрапляє в explicit regex).
        # Тому використовуємо текст без конструкції "в/у документі":
        mode, reason = _simulate_gating("документ що там", s, now)
        assert mode == "general"
        assert reason == "cooldown"

    def test_explicit_bypasses_cooldown(self):
        now = time.time()
        s = _default_session()
        s["doc_focus"] = True
        s["doc_focus_ts"] = now
        s["doc_focus_cooldown_until"] = now + 120
        mode, reason = _simulate_gating("по звіту прибуток?", s, now)
        assert mode == "doc"
        assert reason is None

    def test_no_explicit_no_fact_signal_denied(self):
        now = time.time()
        s = _default_session()
        s["doc_facts"] = {}
        mode, reason = _simulate_gating("а що там у документі?", s, now)
        # domain=doc (загальний тригер), але signals.has_fact_signal=False → denied
        # Може бути mode=doc якщо has_doc_trigger, перевіряємо логіку
        signals = detect_context_signals("а що там у документі?")
        if not signals["has_fact_signal"] and not signals["has_explicit_doc_token"]:
            assert mode == "general"
            assert reason == "no_fact_signal"

    def test_fact_signal_allows_implicit_doc(self):
        now = time.time()
        s = _default_session()
        s["doc_focus"] = True
        s["doc_focus_ts"] = now
        s["doc_focus_cooldown_until"] = 0.0  # cooldown inactive
        mode, reason = _simulate_gating("скільки грн/га витрат?", s, now)
        # has_fact_signal=True і domain=?
        signals = detect_context_signals("скільки грн/га витрат?")
        domain = _detect_domain("скільки грн/га витрат?")
        if domain == "doc":
            assert mode == "doc"
        # Якщо domain != doc (загальний текст з units) — general, ok теж
        assert reason is None or reason == "no_fact_signal"

    def test_url_domain_goes_general(self):
        now = time.time()
        s = _default_session()
        mode, reason = _simulate_gating("дивись https://example.com", s, now)
        assert mode == "general"

    def test_empty_text_general(self):
        now = time.time()
        s = _default_session()
        mode, reason = _simulate_gating("", s, now)
        assert mode == "general"


# ─── 6. Auto-clear sets cooldown симуляція ────────────────────────────────────

def _simulate_auto_clear(session: dict, domain: str, now: float):
    """Симуляція auto-clear логіки."""
    focus_active = is_doc_focus_active(session, now)
    if focus_active and domain in ("vision", "web"):
        session["doc_focus"] = False
        session["doc_focus_ts"] = 0.0
        session["doc_focus_cooldown_until"] = now + DOC_FOCUS_COOLDOWN_S
        return True  # cleared
    return False


class TestAutoClearCooldown:
    def test_web_domain_sets_cooldown(self):
        now = time.time()
        s = _default_session()
        s["doc_focus"] = True
        s["doc_focus_ts"] = now
        cleared = _simulate_auto_clear(s, "web", now)
        assert cleared
        assert not s["doc_focus"]
        assert is_doc_focus_cooldown_active(s, now)
        assert s["doc_focus_cooldown_until"] == pytest.approx(now + 120, abs=1)

    def test_vision_domain_sets_cooldown(self):
        now = time.time()
        s = _default_session()
        s["doc_focus"] = True
        s["doc_focus_ts"] = now
        _simulate_auto_clear(s, "vision", now)
        assert is_doc_focus_cooldown_active(s, now)

    def test_general_domain_no_clear(self):
        now = time.time()
        s = _default_session()
        s["doc_focus"] = True
        s["doc_focus_ts"] = now
        cleared = _simulate_auto_clear(s, "general", now)
        assert not cleared
        assert s["doc_focus"]

    def test_cooldown_expires_after_120s(self):
        now = time.time()
        s = _default_session()
        s["doc_focus_cooldown_until"] = now - 1  # вже закінчився
        assert not is_doc_focus_cooldown_active(s, now)


# ─── 7. /doc status shows cooldown ────────────────────────────────────────────

class TestDocFocusHandlerCooldown:
    def test_status_no_cooldown(self, monkeypatch):
        """Status при відсутньому cooldown."""
        from crews.agromatrix_crew.session_context import _STORE
        chat_id = "test_v36_status_no_cooldown"
        _STORE[chat_id] = _default_session()
        result = handle_doc_focus("status", chat_id)
        assert result["ok"]
        assert "cooldown" not in result["message"]

    def test_status_with_cooldown(self, monkeypatch):
        """Status показує cooldown_left якщо cooldown активний."""
        from crews.agromatrix_crew.session_context import _STORE
        now = time.time()
        chat_id = "test_v36_status_with_cooldown"
        s = _default_session()
        s["doc_focus_cooldown_until"] = now + 60
        s["updated_at"] = now  # щоб сесія не вважалась протухлою
        _STORE[chat_id] = s
        result = handle_doc_focus("status", chat_id)
        assert result["ok"]
        assert "cooldown" in result["message"]

    def test_doc_on_resets_cooldown(self):
        """'/doc on' скидає cooldown."""
        from crews.agromatrix_crew.session_context import _STORE
        now = time.time()
        chat_id = "test_v36_on_resets_cooldown"
        s = _default_session()
        s["doc_focus_cooldown_until"] = now + 100
        s["updated_at"] = now
        _STORE[chat_id] = s
        handle_doc_focus("on", chat_id)
        updated = _STORE[chat_id]
        assert updated["doc_focus"] is True
        assert updated.get("doc_focus_cooldown_until", 0.0) == 0.0


# ─── 8. Default session has new field ─────────────────────────────────────────

class TestDefaultSession:
    def test_has_cooldown_field(self):
        s = _default_session()
        assert "doc_focus_cooldown_until" in s
        assert s["doc_focus_cooldown_until"] == 0.0

    def test_has_doc_focus(self):
        s = _default_session()
        assert "doc_focus" in s
        assert s["doc_focus"] is False
