"""Offline unit tests for the deterministic + safety-critical logic in app.py.

These run with no network and no real Gemini key. They guard the parts a refactor could silently
break: the self-harm crisis gate, language detection, program mapping, BM25 retrieval, response
shaping, and the program/platform retrieval filter.
"""
import numpy as np
import pytest

import app


# ---------------------------------------------------------------- crisis gate (safety-critical)
@pytest.mark.parametrize("msg", [
    "I feel like I don't want to live anymore",
    "I want to end it all",
    "I've been thinking about killing myself",
    "mujhe jeena nahi hai",
    "main marna chahta hoon",
    "khud ko khatam kar lunga",
    "मुझे जीना नहीं है",
    "मैं आत्महत्या करना चाहता हूँ",
])
def test_crisis_fires(msg):
    assert app.is_crisis(msg) is True


@pytest.mark.parametrize("msg", [
    "I'm dying to smoke a cigarette right now",   # hyperbole, not self-harm
    "quitting is killing me lol",
    "I smoke to hurt myself, how do I stop",       # approved deception phrasing, must NOT crisis
    "how do I get a refund?",
    "what is mindful smoking?",
])
def test_crisis_does_not_false_fire(msg):
    assert app.is_crisis(msg) is False


# ---------------------------------------------------------------- language detection
def test_looks_english():
    assert app.looks_english("how do I get a refund?") is True
    assert app.looks_english("I am feeling sick post quitting") is True
    assert app.looks_english("mujhe jeena nahi hai") is False        # Hinglish stays untouched
    assert app.looks_english("yaar mera streak reset ho gaya") is False
    assert app.looks_english("hi") is False                          # too short


# ---------------------------------------------------------------- program canonicalization
def test_canonical_program():
    assert app.canonical_program("P9") == "P9"
    assert app.canonical_program("P11") == "P11"
    for c in ("P1", "P2", "P3", "P4", "P5", "P7", "p3"):
        assert app.canonical_program(c) == "P3"    # every other smoking program -> Original
    assert app.canonical_program(None) is None
    assert app.canonical_program("") is None


# ---------------------------------------------------------------- BM25 exact-token retrieval
def test_bm25_finds_exact_token():
    # BM25 should rank an entry containing a literal token highly, even when embeddings blur it.
    scores = app.bm25_scores("555 breathing")
    assert scores.shape[0] == len(app.INDEX_IDS)
    assert scores.max() > 0
    top_id = int(app.INDEX_IDS[int(np.argmax(scores))])
    assert "555" in app.KB[top_id]["question"].lower() or "555" in app.KB[top_id]["answer"].lower()


# ---------------------------------------------------------------- response finalization
def test_finalize_answer():
    out = app._finalize({"answer": "ok", "escalate": False})
    assert len(out["message_id"]) == 12
    assert out["suggested_channel"] is None


def test_finalize_routes_and_caps():
    out = app._finalize({"answer": "x", "escalate": True, "escalate_reason": "account_action",
                         "coach_summary": "y" * 900})
    assert out["suggested_channel"] == "technical"
    assert len(out["coach_summary"]) == app.COACH_SUMMARY_MAX
    coaching = app._finalize({"answer": "x", "escalate": True, "escalate_reason": "medical_topic"})
    assert coaching["suggested_channel"] == "coaching"


# ---------------------------------------------------------------- retrieval program/platform filter
def test_retrieve_filters_by_program(monkeypatch):
    # Stub the embedder so retrieve() runs offline; the program/platform filter is vector-independent.
    monkeypatch.setattr(app, "embed_query", lambda text: app.INDEX_VECS[0])
    hits, _ = app.retrieve("what is included in the program?", program="P3", k=5)
    for entry, _score in hits:
        progs = entry["programs"]
        assert "all" in progs or "P3" in progs   # a P3 user never sees a Relaxed-only (P9) answer


def test_retrieve_platform_filter_no_cross_leak(monkeypatch):
    # "all"-platform entries apply everywhere, but a platform-specific answer must not cross over:
    # an iOS user should never receive an android-only entry.
    monkeypatch.setattr(app, "embed_query", lambda text: app.INDEX_VECS[0])
    hits, _ = app.retrieve("how do I cancel my subscription?", platform="ios", k=5)
    for entry, _score in hits:
        assert entry.get("platform", "all") in ("all", "ios")


# ---------------------------------------------------------------- build_context is defensive
def test_build_context_tolerates_missing_topic():
    entry = {"id": 1, "source": "S", "question": "Q", "answer": "A"}   # no 'topic' key
    ctx = app.build_context([(entry, 0.9)])
    assert "Q" in ctx and "A" in ctx   # must not KeyError


# ---------------------------------------------------------------- rate limiter (cost/abuse guard)
def test_rate_ok_disabled_never_throttles(monkeypatch):
    # 0 (the local default) means the limiter is off: eval/simulations are never throttled.
    monkeypatch.setattr(app, "RATE_LIMIT_PER_MIN", 0)
    monkeypatch.setattr(app, "_rate_hits", {})
    assert all(app._rate_ok("u1") for _ in range(500))


def test_rate_ok_throttles_over_limit(monkeypatch):
    monkeypatch.setattr(app, "RATE_LIMIT_PER_MIN", 3)
    monkeypatch.setattr(app, "_rate_hits", {})
    assert [app._rate_ok("u1") for _ in range(4)] == [True, True, True, False]
    # a different user has an independent budget
    assert app._rate_ok("u2") is True
