"""Tests for knowledge-awareness: empty KB vs no relevant vs found."""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest

from app.saas.knowledge_state import (
    EMPTY_KB_MESSAGE,
    NO_RELEVANT_MESSAGE,
    KnowledgeStatus,
    assess_agent_knowledge,
    needs_knowledge,
)


def test_knowledge_status_empty():
    s = KnowledgeStatus(vector_count=0, ready_docs=0, ready_chunks=0, statement_jobs=0)
    assert s.has_knowledge is False


def test_knowledge_status_with_vectors():
    s = KnowledgeStatus(vector_count=3, ready_docs=0, ready_chunks=0, statement_jobs=0)
    assert s.has_knowledge is True


def test_knowledge_status_with_ready_docs():
    s = KnowledgeStatus(vector_count=0, ready_docs=1, ready_chunks=0, statement_jobs=0)
    assert s.has_knowledge is True


def test_knowledge_status_with_statement_jobs():
    s = KnowledgeStatus(vector_count=0, ready_docs=0, ready_chunks=0, statement_jobs=1)
    assert s.has_knowledge is True


def test_needs_knowledge_for_spend():
    assert needs_knowledge("spending_summary", "data") is True
    assert needs_knowledge("category_spending", "finance") is True
    assert needs_knowledge("general", "greeting") is False
    assert needs_knowledge("general", "help") is False
    assert needs_knowledge("general", "math") is False
    assert needs_knowledge("general", "concept") is False
    assert needs_knowledge("general", "concept", "tell me about the knowledge base") is True
    assert needs_knowledge("general", "data", "How much spent on grocery?") is True


def test_help_is_not_finance_follow_up():
    from app.saas.conversation import (
        classify_conversational_intent,
        clear_turns,
        remember_turn,
        resolve_turn,
    )

    for q in ("how can you help me", "help", "what can you do", "hi", "hello"):
        assert classify_conversational_intent(q, has_history=True) == "new_question"

    # Self-contained spend asks stay new questions (not glued to prior date context).
    assert classify_conversational_intent("how much spent", has_history=True) == "new_question"
    # Pronoun follow-ups still count as follow-ups.
    assert classify_conversational_intent("why only that", has_history=True) == "follow_up"

    sid = "t-help-routing"
    clear_turns(sid)
    remember_turn(sid, "how much did I spend", "Total spent ₹ 100")
    resolved = resolve_turn(sid, "how can you help me")
    assert resolved.conv_intent == "new_question"
    assert resolved.rewritten == "how can you help me"
    assert "Referring to" not in resolved.rewritten
    clear_turns(sid)


def test_help_does_not_need_knowledge():
    assert needs_knowledge("general", "help", "how can you help me") is False
    assert needs_knowledge("general", "greeting", "hi") is False
    assert needs_knowledge("spending_summary", "data", "How much did I spend?") is True


def test_how_are_you_is_greeting_not_data():
    from app.rag.chain import _detect_intent
    from app.saas.conversation import classify_conversational_intent

    for q in ("how are you", "How are you?", "how's it going", "how are you doing"):
        assert _detect_intent(q) == "greeting", q
        assert classify_conversational_intent(q, has_history=True) == "new_question"
    assert _detect_intent("How much did I spend?") == "data"


@patch("app.saas.knowledge_state.saas_db")
def test_assess_empty_agent(mock_db):
    from app.saas.knowledge_state import invalidate_knowledge_cache

    invalidate_knowledge_cache()
    mock_db.list_kb_documents.return_value = []
    status = assess_agent_knowledge("org-1", "agent-1")
    assert status.has_knowledge is False
    assert status.vector_count == 0


@patch("app.saas.knowledge_state.saas_db")
def test_assess_agent_with_indexed_files(mock_db):
    from app.saas.knowledge_state import invalidate_knowledge_cache

    invalidate_knowledge_cache()
    mock_db.list_kb_documents.return_value = [
        {"status": "ready", "filename": "a.pdf", "chunk_count": 25}
    ]
    status = assess_agent_knowledge("org-1", "agent-1")
    assert status.has_knowledge is True
    assert status.ready_docs == 1
    assert status.ready_chunks == 25
    assert status.statement_jobs == 1


@patch("app.saas.knowledge_state.saas_db")
def test_assess_empty_chroma_collection(mock_db):
    """No ready docs → empty knowledge (no Chroma probe on hot path)."""
    from app.saas.knowledge_state import invalidate_knowledge_cache

    invalidate_knowledge_cache()
    mock_db.list_kb_documents.return_value = []
    status = assess_agent_knowledge("org-1", "agent-empty-chroma")
    assert status.has_knowledge is False


def _auth():
    return SimpleNamespace(
        org_id="org-1",
        org_name="Test Org",
        kb_mode="tenant",
        user_id="u1",
    )


def _agent(**kwargs):
    base = {
        "id": "agent-1",
        "org_id": "org-1",
        "name": "Alex",
        "kb_mode": "tenant",
        "welcome_message": "Hi",
        "description": None,
    }
    base.update(kwargs)
    return base


@patch("app.saas.agent.agent_document_count", return_value=0)
@patch("app.saas.agent.get_rag_service")
@patch("app.saas.agent.assess_agent_knowledge")
@patch("app.saas.agent.saas_db")
def test_agent_chat_empty_kb_skips_llm(mock_db, mock_assess, mock_rag, mock_vec):
    from app.saas.agent import agent_chat
    from app.saas.knowledge_state import KnowledgeStatus

    mock_db.get_agent.return_value = _agent()
    mock_db.count_ready_kb_chunks.return_value = 0
    mock_assess.return_value = KnowledgeStatus(0, 0, 0, 0)
    mock_llm = MagicMock()
    mock_rag.return_value = mock_llm

    resp = agent_chat(
        question="how much did I spend on groceries?",
        auth=_auth(),
        agent_id="agent-1",
        session_id="sess-empty",
        kb_mode="tenant",
    )

    assert resp.knowledge_state == "empty"
    assert resp.show_upload_cta is True
    assert "don't have a knowledge base" in resp.answer.lower()
    assert "upload a knowledge base first" in resp.answer.lower()
    assert "Upload a PDF statement" in resp.answer
    assert "CSV export" in resp.answer
    mock_llm._invoke_ollama.assert_not_called()
    assert "reviewed" not in resp.answer.lower()


@patch("app.saas.agent.agent_document_count", return_value=0)
@patch("app.saas.agent.retrieve_agent_scored")
@patch("app.saas.agent.get_rag_service")
@patch("app.saas.agent.assess_agent_knowledge")
@patch("app.saas.agent.saas_db")
def test_agent_chat_platform_mode_empty_never_uses_dataset(
    mock_db, mock_assess, mock_rag, mock_retrieve, mock_vec
):
    """Legacy kb_mode=platform must not leak shared finance datasets."""
    from app.saas.agent import agent_chat
    from app.saas.knowledge_state import KnowledgeStatus

    mock_db.get_agent.return_value = _agent(kb_mode="platform")
    mock_db.count_ready_kb_chunks.return_value = 0
    mock_assess.return_value = KnowledgeStatus(0, 0, 0, 0)
    mock_llm = MagicMock()
    mock_rag.return_value = mock_llm
    mock_rag.return_value.vector_store_ready.return_value = True

    for q in (
        "How much spent on grocery?",
        "Tell me about the knowledge base",
        "How much did I spend?",
    ):
        resp = agent_chat(
            question=q,
            auth=_auth(),
            agent_id="agent-1",
            session_id="sess-platform-empty",
            kb_mode="platform",
        )
        assert resp.knowledge_state == "empty", q
        assert resp.show_upload_cta is True, q
        assert "upload a knowledge base first" in resp.answer.lower(), q
        assert not resp.citations, q

    mock_retrieve.assert_not_called()
    mock_llm._invoke_ollama.assert_not_called()


@patch("app.saas.agent.agent_document_count", return_value=5)
@patch("app.saas.agent.retrieve_agent_scored", return_value=([], "no_hits"))
@patch("app.saas.agent.try_structured_kb_answer", return_value=None)
@patch("app.saas.agent.list_agent_statement_jobs", return_value=[])
@patch("app.saas.agent.get_rag_service")
@patch("app.saas.agent.assess_agent_knowledge")
@patch("app.saas.agent.saas_db")
def test_agent_chat_zero_retrieval_no_relevant(
    mock_db, mock_assess, mock_rag, mock_jobs, mock_structured, mock_retrieve, mock_vec
):
    from app.saas.agent import agent_chat
    from app.saas.knowledge_state import KnowledgeStatus

    mock_db.get_agent.return_value = _agent(kb_mode="tenant")
    mock_db.count_ready_kb_chunks.return_value = 10
    # Has indexed files, but retrieval returns nothing and no statement jobs
    mock_assess.return_value = KnowledgeStatus(5, 1, 10, 0)
    mock_rag.return_value = MagicMock()
    mock_rag.return_value.vector_store_ready.return_value = False

    resp = agent_chat(
        question="how much did I spend on unicorns?",
        auth=_auth(),
        agent_id="agent-1",
        session_id="sess-norelevant-2",
        kb_mode="tenant",
    )
    assert resp.knowledge_state == "no_relevant"
    assert "couldn't find any information related to your question" in resp.answer.lower()
    assert "I reviewed your" not in resp.answer
    mock_rag.return_value._invoke_ollama.assert_not_called()


@patch("app.saas.agent.agent_document_count", return_value=8)
@patch("app.saas.agent.retrieve_agent_scored")
@patch("app.saas.agent.try_structured_kb_answer", return_value=None)
@patch("app.saas.agent.get_rag_service")
@patch("app.saas.agent.assess_agent_knowledge")
@patch("app.saas.agent.saas_db")
def test_agent_chat_with_indexed_files_calls_retrieval(
    mock_db, mock_assess, mock_rag, mock_structured, mock_retrieve, mock_vec
):
    from app.saas.agent import agent_chat
    from app.saas.knowledge_state import KnowledgeStatus

    mock_db.get_agent.return_value = _agent(kb_mode="tenant")
    mock_db.count_ready_kb_chunks.return_value = 20
    mock_assess.return_value = KnowledgeStatus(8, 1, 20, 0)

    doc = MagicMock()
    doc.page_content = "Total Money Paid Rs.100. Payments made 2. Paid to Zepto -50."
    doc.metadata = {"source_file": "stmt.pdf", "page": 1, "data_category": "tenant_kb"}
    mock_retrieve.return_value = ([(doc, 0.9)], None)

    llm = MagicMock()
    llm._invoke_ollama.return_value = "You spent about 50 on Zepto."
    mock_rag.return_value = llm
    mock_rag.return_value.vector_store_ready.return_value = False

    resp = agent_chat(
        question="tell me about Zepto on my statement",
        auth=_auth(),
        agent_id="agent-1",
        session_id="sess-found",
        kb_mode="tenant",
    )

    assert resp.knowledge_state == "found"
    assert resp.show_upload_cta is False
    mock_retrieve.assert_called()
    llm._invoke_ollama.assert_called()


def test_empty_and_no_relevant_messages_are_distinct():
    assert EMPTY_KB_MESSAGE != NO_RELEVANT_MESSAGE
    assert "don't have a knowledge base" in EMPTY_KB_MESSAGE.lower()
    assert "upload a knowledge base first" in EMPTY_KB_MESSAGE.lower()
    assert "couldn't find any information related" in NO_RELEVANT_MESSAGE.lower()
    assert "reviewed" not in EMPTY_KB_MESSAGE.lower()
    assert "reviewed" not in NO_RELEVANT_MESSAGE.lower()


@patch("app.saas.agent.agent_document_count", return_value=0)
@patch("app.saas.agent.saas_db")
def test_resolve_kb_mode_empty_tenant_stays_tenant(mock_db, mock_count):
    from app.saas.agent import resolve_kb_mode

    mock_db.count_ready_kb_chunks.return_value = 0
    auth = _auth()
    mode = resolve_kb_mode(auth, _agent(kb_mode="tenant"))
    assert mode == "tenant"


@patch("app.saas.agent.agent_document_count", return_value=0)
@patch("app.saas.agent.saas_db")
def test_resolve_kb_mode_platform_remaps_to_tenant_when_empty(mock_db, mock_count):
    from app.saas.agent import resolve_kb_mode

    mock_db.count_ready_kb_chunks.return_value = 0
    mode = resolve_kb_mode(_auth(), _agent(kb_mode="platform"))
    assert mode == "tenant"
