from types import SimpleNamespace

import pytest

from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.workflows.input_contract import match_semantic_classifier_output


ROUTING_YAML = """
router:
  fallback_agent: fallback_agent
  confidence_threshold: 0.70
intents: []
"""


class _ClassifierLLM:
    def __init__(self, answers):
        self.answers = list(answers)
        self.calls = []

    async def ainvoke(self, messages, **kwargs):
        self.calls.append((messages, kwargs))
        return self.answers.pop(0)


def _router(tmp_path, answers):
    routing = tmp_path / "routing.yaml"
    routing.write_text(ROUTING_YAML, encoding="utf-8")
    settings = SimpleNamespace(
        ROUTING_CONFIG_PATH=str(routing),
        ENABLE_LLM_ROUTER=False,
        ENABLE_ROUTE_STICKINESS=False,
    )
    return EnterpriseRouter(settings, llm=_ClassifierLLM(answers))


def _state(text, allowed, prompt, *, history=None, include_relevant_context=False):
    return {
        "user_text": text,
        "sanitized_input": text,
        "route": "owner_agent",
        "active_agent": "owner_agent",
        "intent": "owner_intent",
        "route_decision": {"route": "owner_agent", "agent": "owner_agent", "intent": "owner_intent"},
        "pending_domain_workflow": {
            "workflow_name": "example",
            "execution_id": "exec-1",
            "resume_tool": "retomar_workflow",
            "owner_agent": "owner_agent",
            "owner_intent": "owner_intent",
            "pause": {
                "prompt": "Pergunta pendente",
                "expected_input": {
                    "key": "resposta_usuario",
                    "allowed_values": allowed,
                    "normalize": "upper_strip",
                    "reprompt": "Escolha novamente.",
                    "semantic_classifier": {
                        "enabled": True,
                        "include_relevant_context": include_relevant_context,
                        "prompt": prompt,
                    },
                },
            },
        },
        "history": list(history or []),
    }


@pytest.mark.asyncio
async def test_semantic_classifier_maps_acknowledgement_to_configured_option(tmp_path):
    router = _router(tmp_path, ["SIM"])
    decision = await router.route(_state("legal!", ["SIM", "NAO"], "Classifique {{ user_input }} em {{ allowed_values }}"))
    assert decision.metadata["workflow_semantic_classifier"] is True
    assert decision.metadata["normalized_input"] == "SIM"
    assert decision.metadata["original_input"] == "legal!"


@pytest.mark.asyncio
async def test_semantic_classifier_can_map_forward_fact_question_to_nao(tmp_path):
    router = _router(tmp_path, ["NAO"])
    decision = await router.route(_state("então minha fatura ficaria R$ 275,00, certo?", ["SIM", "NAO"], "Hipóteses => NAO. Opções {{ allowed_values }}"))
    assert decision.metadata["normalized_input"] == "NAO"
    assert decision.metadata["original_input"].startswith("então minha fatura")


@pytest.mark.asyncio
async def test_semantic_classifier_is_generic_for_three_dynamic_options(tmp_path):
    router = _router(tmp_path, ["ALTERAR"])
    decision = await router.route(_state("quero mudar", ["CONFIRMAR", "ALTERAR", "CANCELAR"], "Escolha uma de {{ allowed_values }}"))
    assert decision.metadata["normalized_input"] == "ALTERAR"
    assert decision.metadata["allowed_values"] == ["CONFIRMAR", "ALTERAR", "CANCELAR"]


@pytest.mark.asyncio
async def test_semantic_classifier_reprompts_when_llm_returns_value_outside_allowlist(tmp_path):
    router = _router(tmp_path, ["TALVEZ"])
    decision = await router.route(_state("hmm", ["SIM", "NAO"], "Retorne uma de {{ allowed_values }}"))
    assert decision.mcp_tools == []
    assert decision.metadata["workflow_input_invalid"] is True
    assert decision.metadata["workflow_reprompt"] == "Escolha novamente."


@pytest.mark.asyncio
async def test_semantic_classifier_configured_unmatched_value_reprompts(tmp_path):
    router = _router(tmp_path, ["OUTRO"])
    state = _state("fala sem significado", ["SIM", "NAO", "CONTINUAR"], "Classifique {{ user_input }} em {{ allowed_values }}")
    state["pending_domain_workflow"]["pause"]["expected_input"]["semantic_classifier"]["unmatched_value"] = "OUTRO"

    decision = await router.route(state)

    assert decision.mcp_tools == []
    assert decision.metadata["workflow_input_invalid"] is True
    assert decision.metadata["workflow_reprompt"] == "Escolha novamente."
    assert decision.metadata["classifier_raw_output"] == "OUTRO"
    system_prompt = router.llm.calls[0][0][0]["content"]
    assert '"OUTRO"' in system_prompt


def test_classifier_output_validator_uses_dynamic_allowlist():
    contract = {"allowed_values": ["A", "B", "C"], "normalize": "upper_strip"}
    assert match_semantic_classifier_output(" b ", contract) == "B"
    assert match_semantic_classifier_output("D", contract) is None


@pytest.mark.asyncio
async def test_semantic_classifier_receives_contiguous_relevant_context(tmp_path):
    router = _router(tmp_path, ["NAO"])
    history = [
        {"role": "user", "content": "qual é meu plano?", "metadata": {}},
        {"role": "assistant", "content": "Seu plano é X.", "metadata": {"intent": "contas_plan_query"}},
        {"role": "user", "content": "tem uma cobrança aqui que eu não reconheço", "metadata": {}},
        {"role": "assistant", "content": "Expliquei a fatura. Com essa explicação, sanei sua dúvida?", "metadata": {"intent": "owner_intent"}},
        {"role": "user", "content": "é a de quatorze e noventa e nove", "metadata": {}},
    ]
    prompt = (
        "Contexto:\
{{ relevant_conversation_context }}\
"
        "Atual={{ user_input }} Opções={{ allowed_values }}"
    )
    decision = await router.route(
        _state(
            "é a de quatorze e noventa e nove",
            ["SIM", "NAO"],
            prompt,
            history=history,
            include_relevant_context=True,
        )
    )
    assert decision.metadata["normalized_input"] == "NAO"
    context = decision.metadata["relevant_conversation_context"]
    assert "tem uma cobrança aqui que eu não reconheço" in context
    assert "Expliquei a fatura" in context
    assert "qual é meu plano?" not in context
    assert "Seu plano é X." not in context

    messages, kwargs = router.llm.calls[0]
    rendered = messages[0]["content"]
    assert "tem uma cobrança aqui que eu não reconheço" in rendered
    assert "é a de quatorze e noventa e nove" in rendered
    assert "max_tokens" not in kwargs


@pytest.mark.asyncio
async def test_semantic_classifier_context_does_not_inject_transaction_state(tmp_path):
    router = _router(tmp_path, ["NAO"])
    state = _state(
        "é a de quatorze e noventa e nove",
        ["SIM", "NAO"],
        "Contexto={{ relevant_conversation_context }}",
        history=[
            {"role": "user", "content": "tem uma cobrança que não reconheço", "metadata": {}},
            {"role": "assistant", "content": "Expliquei. Sanei sua dúvida?", "metadata": {"intent": "owner_intent"}},
            {"role": "user", "content": "é a de quatorze e noventa e nove", "metadata": {}},
        ],
        include_relevant_context=True,
    )
    state["active_transaction"] = {"tool": "contestar_cobranca", "subject": "x"}
    state["transaction_evidence"] = [{"secret": "should-not-be-in-context"}]
    decision = await router.route(state)
    context = decision.metadata["relevant_conversation_context"]
    assert "contestar_cobranca" not in context
    assert "should-not-be-in-context" not in context


@pytest.mark.asyncio
async def test_semantic_classifier_failure_exposes_raw_output_for_audit(tmp_path):
    router = _router(tmp_path, ["TALVEZ porque..."])
    decision = await router.route(
        _state("hmm", ["SIM", "NAO"], "Retorne {{ allowed_values }}")
    )
    assert decision.metadata["workflow_input_invalid"] is True
    assert decision.metadata["workflow_semantic_classifier"] is True
    assert decision.metadata["classifier_raw_output"] == "TALVEZ porque..."
    assert decision.metadata["allowed_values"] == ["SIM", "NAO"]

@pytest.mark.asyncio
async def test_context_anchor_excludes_older_same_intent_topic(tmp_path):
    router = _router(tmp_path, ["NAO"])
    state = _state(
        "é a de quatorze e noventa e nove",
        ["SIM", "NAO"],
        "Contexto={{ relevant_conversation_context }}",
        history=[
            {"role": "user", "content": "explique a fatura de janeiro", "metadata": {"message_id": "old-user"}},
            {"role": "assistant", "content": "Expliquei janeiro.", "metadata": {"intent": "owner_intent", "message_id": "old-assistant"}},
            {"role": "user", "content": "tem uma cobrança aqui que eu não reconheço", "metadata": {"message_id": "anchor-1"}},
            {"role": "assistant", "content": "Expliquei. Sanei sua dúvida?", "metadata": {"intent": "owner_intent", "message_id": "assistant-anchor"}},
            {"role": "user", "content": "é a de quatorze e noventa e nove", "metadata": {"message_id": "current"}},
        ],
        include_relevant_context=True,
    )
    state["pending_domain_workflow"]["context_anchor_message_id"] = "anchor-1"
    decision = await router.route(state)
    context = decision.metadata["relevant_conversation_context"]
    assert "tem uma cobrança aqui que eu não reconheço" in context
    assert "Expliquei. Sanei sua dúvida?" in context
    assert "explique a fatura de janeiro" not in context
    assert "Expliquei janeiro" not in context

@pytest.mark.asyncio
async def test_contextual_reentry_option_releases_pause_and_reroutes_with_bounded_context(tmp_path):
    routing = tmp_path / "routing.yaml"
    routing.write_text(
        """
router:
  fallback_agent: fallback_agent
  confidence_threshold: 0.70
intents:
  - name: invoice_explanation
    agent: billing_agent
    description: explanation
    domain: demo
    mcp_tools: [invoice_explanation]
  - name: contestation
    agent: contestation_agent
    description: contestation
    domain: demo
    mcp_tools: [consultar_faturas, contestar_cobranca]
""",
        encoding="utf-8",
    )
    llm = _ClassifierLLM([
        "CONTINUAR",
        '{"intent":"contestation","agent":"contestation_agent","confidence":0.99,"reason":"pedido anterior de não reconhecimento agora tem alvo identificado"}',
    ])
    settings = SimpleNamespace(
        ROUTING_CONFIG_PATH=str(routing),
        ENABLE_LLM_ROUTER=True,
        ENABLE_ROUTE_STICKINESS=False,
    )
    router = EnterpriseRouter(settings, llm=llm)
    state = _state(
        "é a de quatorze e noventa e nove",
        ["SIM", "NAO", "CONTINUAR"],
        "Classifique {{ user_input }} considerando {{ relevant_conversation_context }} em {{ allowed_values }}",
        history=[
            {"role": "user", "content": "tem uma cobrança aqui que eu não reconheço", "metadata": {"message_id": "anchor"}},
            {"role": "assistant", "content": "Tamboro Mensal R$ 14,99. Sanei sua dúvida?", "metadata": {"intent": "owner_intent"}},
            {"role": "user", "content": "é a de quatorze e noventa e nove", "metadata": {}},
        ],
        include_relevant_context=True,
    )
    state["pending_domain_workflow"]["context_anchor_message_id"] = "anchor"
    state["pending_domain_workflow"]["pause"]["expected_input"]["semantic_classifier"]["option_actions"] = {
        "CONTINUAR": {"action": "contextual_reentry"}
    }

    decision = await router.route(state)

    assert decision.intent == "contestation"
    assert decision.agent == "contestation_agent"
    assert decision.metadata["contextual_reentry"] is True
    assert decision.metadata["classifier_output"] == "CONTINUAR"
    assert decision.metadata["original_input"] == "é a de quatorze e noventa e nove"
    assert decision.metadata["user_claims_are_evidence"] is False
    effective = decision.metadata["contextual_reentry_input"]
    assert "tem uma cobrança aqui que eu não reconheço" in effective
    assert "Tamboro Mensal R$ 14,99" in effective
    assert "é a de quatorze e noventa e nove" in effective
    assert decision.mcp_tools == ["consultar_faturas", "contestar_cobranca"]


def test_invoice_explanation_uses_continue_as_contextual_reentry_option():
    import yaml
    from pathlib import Path

    root = Path(__file__).resolve().parents[2]
    workflow = yaml.safe_load((root / "workflows" / "invoice_explanation.v2.yaml").read_text(encoding="utf-8"))
    formatar = next(node for node in workflow["nodes"] if node["id"] == "formatar")
    contract = formatar["pause"]["expected_input"]
    assert contract["allowed_values"] == ["SIM", "NAO", "CONTINUAR"]
    classifier = contract["semantic_classifier"]
    assert classifier["option_actions"]["CONTINUAR"]["action"] == "contextual_reentry"
    assert classifier["unmatched_value"] == "OUTRO"
    prompt = classifier["prompt"]
    assert "R$ 275,00" in prompt and "CONTINUAR" in prompt
    assert "quatorze e noventa e nove" in prompt and "CONTINUAR" in prompt
    assert "Nunca trate" in prompt
    assert "OUTRO" in prompt
