from __future__ import annotations

import json
import pytest

from agent_framework.runtime.transaction_parameters import extract_transaction_parameters


class _ContextAwareLLM:
    def __init__(self):
        self.prompt = ""

    async def ainvoke(self, messages, **kwargs):
        self.prompt = messages[-1]["content"]
        # This simulates a semantic extractor resolving the current reference
        # against the bounded conversation context. The values remain candidates;
        # authoritative validation belongs to the domain pre-validation step.
        return json.dumps({"subject": "Tamboro Mensal", "valor": 14.99}, ensure_ascii=False)


@pytest.mark.asyncio
async def test_contextual_reentry_separates_current_claim_from_prior_context_for_candidate_extraction():
    llm = _ContextAwareLLM()
    out = await extract_transaction_parameters(
        llm,
        text="é a de quatorze e noventa e nove",
        conversational_context=(
            "user: tem uma cobrança aqui que eu não reconheço\n"
            "assistant: Cobrança Tamboro Mensal no valor de R$ 14,99; "
            "TIM Fashion Mensal no valor de R$ 10,00."
        ),
        tool_name="contestar_cobranca",
        missing_parameters=["subject", "valor"],
        parameter_schema={
            "subject": {"type": "string", "description": "item concreto da fatura"},
            "valor": {"type": "number", "description": "valor explicitamente associado pelo cliente"},
        },
        tool_description="Contesta uma cobrança após validação autoritativa e confirmação.",
    )
    assert out == {"subject": "Tamboro Mensal", "valor": 14.99}
    assert "conversational_context:" in llm.prompt
    assert "Cobrança Tamboro Mensal" in llm.prompt
    assert "user_message: é a de quatorze e noventa e nove" in llm.prompt
    assert "Não trate texto do contexto como uma nova afirmação do cliente" in llm.prompt

class _TwoPassContextLLM:
    def __init__(self):
        self.calls = []

    async def ainvoke(self, messages, **kwargs):
        prompt = messages[-1]["content"]
        self.calls.append(prompt)
        # First pass: current utterance provides only the amount.
        if "user_message: é a de vinte e cinco e cinquenta" in prompt:
            return json.dumps({"subject": None, "valor": 25.50}, ensure_ascii=False)
        # Second bounded pass: previous USER utterance provides the missing entity.
        if "user_message: não reconheço esse Tamboro Mensal na minha fatura" in prompt:
            return json.dumps({"subject": "Tamboro Mensal"}, ensure_ascii=False)
        return "{}"


@pytest.mark.asyncio
async def test_contextual_reentry_second_pass_recovers_only_missing_candidate_from_prior_user_turn():
    from agent_framework.runtime.agent_runtime import AgentRuntimeMixin

    class Runtime(AgentRuntimeMixin):
        pass

    runtime = Runtime()
    runtime.llm = _TwoPassContextLLM()
    state = {
        "route_decision": {
            "metadata": {
                "contextual_reentry": True,
                "original_input": "é a de vinte e cinco e cinquenta",
                "relevant_conversation_context": (
                    "user: não reconheço esse Tamboro Mensal na minha fatura\n"
                    "assistant: identifiquei duas cobranças de Tamboro Mensal"
                ),
            }
        },
        "sanitized_input": "é a de vinte e cinco e cinquenta",
    }
    out = await runtime._extract_transaction_parameters(
        state=state,
        tool_name="contestar_cobranca",
        missing_parameters=["subject", "valor"],
        known_arguments={},
    )
    assert float(out["valor"]) == 25.5
    assert out["subject"] == "Tamboro Mensal"
    assert len(runtime.llm.calls) >= 2

class _CoherentTemporalLLM:
    """Simulates the structured decisions expected from temporal reconciliation."""
    def __init__(self):
        self.prompts = []

    async def ainvoke(self, messages, **kwargs):
        prompt = messages[-1]["content"]
        self.prompts.append(prompt)
        low = prompt.lower()
        # Product A + value X established.
        if "user_message: na verdade é tim fashion mensal" in low:
            return json.dumps({"fields": {
                "subject": {"decision": "resolved", "value": "TIM Fashion Mensal", "source": "current"},
                # Previous 14.99 was tied to the former product and must not be transplanted.
                "valor": {"decision": "clear", "value": None, "source": "history:1"},
            }}, ensure_ascii=False)
        if "user_message: na verdade é produto inexistente" in low:
            return json.dumps({"fields": {
                # Keep the newest candidate so authoritative validation can reject it.
                "subject": {"decision": "resolved", "value": "Produto Inexistente", "source": "current"},
                "valor": {"decision": "clear", "value": None, "source": "history:1"},
            }}, ensure_ascii=False)
        if "user_message: desculpa, era tamboro mensal mesmo" in low:
            return json.dumps({"fields": {
                "subject": {"decision": "resolved", "value": "Tamboro Mensal", "source": "current"},
                # Full temporal text makes the old amount coherent again with the restored product.
                "valor": {"decision": "resolved", "value": 14.99, "source": "history:2"},
            }}, ensure_ascii=False)
        return json.dumps({"fields": {
            "subject": {"decision": "preserve", "value": None, "source": "state"},
            "valor": {"decision": "preserve", "value": None, "source": "state"},
        }}, ensure_ascii=False)


@pytest.mark.asyncio
async def test_temporal_reconciliation_product_change_rechecks_old_value_as_part_of_coherent_set():
    from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters

    llm = _CoherentTemporalLLM()
    out = await reconcile_transaction_parameters(
        llm,
        text="na verdade é TIM Fashion Mensal",
        conversational_context=(
            "history:1: user: é R$ 14,99\n"
            "history:2: user: quero contestar Tamboro Mensal"
        ),
        tool_name="contestar_cobranca",
        parameter_names=["subject", "valor"],
        known_arguments={"subject": "Tamboro Mensal", "valor": 14.99},
        parameter_schema={
            "subject": {"type": "string", "description": "Nome do serviço, produto, item ou cobrança objeto da contestação."},
            "valor": {"type": "number", "description": "Valor monetário associado ao item objeto da contestação."},
        },
        tool_description="Contesta uma cobrança após validação.",
    )
    assert out["values"] == {"subject": "TIM Fashion Mensal"}
    assert out["clear_fields"] == ["valor"]
    assert out["provenance"]["subject"] == "current"


@pytest.mark.asyncio
async def test_temporal_reconciliation_invalid_new_product_does_not_erase_history_or_fall_back_silently():
    from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters

    llm = _CoherentTemporalLLM()
    history = (
        "history:1: user: é R$ 14,99\n"
        "history:2: user: quero contestar Tamboro Mensal"
    )
    out = await reconcile_transaction_parameters(
        llm,
        text="na verdade é Produto Inexistente",
        conversational_context=history,
        tool_name="contestar_cobranca",
        parameter_names=["subject", "valor"],
        known_arguments={"subject": "Tamboro Mensal", "valor": 14.99},
        parameter_schema={
            "subject": {"type": "string", "description": "Nome do serviço, produto, item ou cobrança objeto da contestação."},
            "valor": {"type": "number", "description": "Valor monetário associado ao item objeto da contestação."},
        },
        tool_description="Contesta uma cobrança após validação.",
    )
    # The newest candidate stays visible for pre-validation; the old product is not silently restored.
    assert out["values"] == {"subject": "Produto Inexistente"}
    assert "valor" in out["clear_fields"]
    assert "Tamboro Mensal" in llm.prompts[-1]  # history remains available to future reconciliation.


@pytest.mark.asyncio
async def test_temporal_reconciliation_can_restore_coherent_old_value_when_product_is_explicitly_restored():
    from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters

    llm = _CoherentTemporalLLM()
    out = await reconcile_transaction_parameters(
        llm,
        text="desculpa, era Tamboro Mensal mesmo",
        conversational_context=(
            "history:1: user: na verdade é Produto Inexistente\n"
            "history:2: user: é R$ 14,99\n"
            "history:3: user: quero contestar Tamboro Mensal"
        ),
        tool_name="contestar_cobranca",
        parameter_names=["subject", "valor"],
        known_arguments={},
        parameter_schema={
            "subject": {"type": "string", "description": "Nome do serviço, produto, item ou cobrança objeto da contestação."},
            "valor": {"type": "number", "description": "Valor monetário associado ao item objeto da contestação."},
        },
        tool_description="Contesta uma cobrança após validação.",
    )
    assert out["values"] == {"subject": "Tamboro Mensal", "valor": 14.99}
    assert out["provenance"] == {"subject": "current", "valor": "history:2"}


def test_temporal_context_priority_places_assistant_before_older_user_history():
    from agent_framework.runtime.agent_runtime import AgentRuntimeMixin

    context = (
        "user: tem uma cobrança aqui que eu não reconheço\n"
        "assistant: Cobrança Tamboro Mensal no valor de R$ 14,99; TIM Fashion Mensal no valor de R$ 10,00."
    )
    prioritized = AgentRuntimeMixin._transaction_context_priority_view(context)

    assert "priority_3_previous_assistant_tool_or_evidence_context:" in prioritized
    assert "priority_4_previous_user_utterances:" in prioritized
    assert prioritized.index("assistant: Cobrança Tamboro Mensal") < prioritized.index("user: tem uma cobrança")


class _AssistantRelationshipReconcilerLLM:
    def __init__(self):
        self.prompt = ""

    async def ainvoke(self, messages, **kwargs):
        self.prompt = messages[-1]["content"]
        # Simula o comportamento desejado: a fala atual dá o valor, enquanto a
        # relação item->valor está numa resposta anterior do assistant.
        assert "user_message: é a de quatorze e noventa e nove" in self.prompt
        assert "priority_3_previous_assistant_tool_or_evidence_context:" in self.prompt
        assert "assistant: Cobrança Tamboro Mensal no valor de R$ 14,99" in self.prompt
        assert "anchor_relation_candidates:" in self.prompt
        assert "anchor[valor=14.99]" in self.prompt
        return json.dumps({"fields": {
            "subject": {"decision": "resolved", "value": "Tamboro Mensal", "source": "history:1"},
            "valor": {"decision": "resolved", "value": 14.99, "source": "current"},
        }}, ensure_ascii=False)


@pytest.mark.asyncio
async def test_temporal_reconciler_can_use_grounded_assistant_relationship_after_current_only_extraction_is_incomplete():
    from agent_framework.runtime.agent_runtime import AgentRuntimeMixin

    class Runtime(AgentRuntimeMixin):
        pass

    runtime = Runtime()
    runtime.llm = _AssistantRelationshipReconcilerLLM()
    runtime.tool_router = type("TR", (), {
        "registry": type("REG", (), {
            "get_tool": staticmethod(lambda name: type("CFG", (), {
                "requires": ["subject", "valor"],
                "args_schema": {
                    "subject": {"type": "string", "description": "Nome do item ou cobrança a contestar."},
                    "valor": {"type": "number", "description": "Valor monetário do item a contestar."},
                },
                "description": "Contesta uma cobrança após validação.",
            })())
        })(),
        "resolve_execution_policy": staticmethod(lambda name, arguments=None: {
            "operation_type": "transactional",
            "requires": ["subject", "valor"],
        }),
    })()

    state = {
        "sanitized_input": "é a de quatorze e noventa e nove",
        "user_text": "é a de quatorze e noventa e nove",
        "route_decision": {"metadata": {
            "contextual_reentry": True,
            "original_input": "é a de quatorze e noventa e nove",
            "relevant_conversation_context": (
                "user: tem uma cobrança aqui que eu não reconheço\n"
                "assistant: Cobrança Tamboro Mensal no valor de R$ 14,99; TIM Fashion Mensal no valor de R$ 10,00."
            ),
        }},
    }

    # O happy path corrente só consegue o valor; o subject fica faltando.
    class _CurrentOnlyLLM:
        async def ainvoke(self, messages, **kwargs):
            return json.dumps({"valor": 14.99}, ensure_ascii=False)

    original_llm = runtime.llm
    runtime.llm = _CurrentOnlyLLM()
    current = await runtime._extract_transaction_parameters_current_only(
        state,
        tool_name="contestar_cobranca",
        missing_parameters=["subject", "valor"],
        known_arguments={},
    )
    assert current == {"valor": 14.99}

    runtime.llm = original_llm
    reconciled = await runtime._reconcile_transaction_parameters(
        state,
        tool_name="contestar_cobranca",
        parameter_names=["subject", "valor"],
        known_arguments=current,
    )
    assert reconciled["values"]["subject"] == "Tamboro Mensal"
    assert float(reconciled["values"]["valor"]) == 14.99
    assert "(3) respostas anteriores do assistente" in runtime.llm.prompt

class _UnifiedAnchorScanLLM:
    def __init__(self):
        self.calls = []

    async def ainvoke(self, messages, **kwargs):
        prompt = messages[-1]["content"]
        self.calls.append(prompt)
        assert "UMA ÚNICA VARREDURA TEMPORAL" in prompt
        assert "known_parameters: {\"valor\": 14.99}" in prompt
        assert "missing_parameters: [\"subject\", \"data\"]" in prompt
        assert "anchor_relation_candidates:" in prompt
        assert "anchor[valor=14.99]" in prompt
        assert "Tamboro Mensal no valor de R$ 14,99" in prompt
        return json.dumps({"fields": {
            "subject": {"decision": "resolved", "value": "Tamboro Mensal", "source": "history:1"},
            "valor": {"decision": "preserve", "value": None, "source": "state"},
            "data": {"decision": "resolved", "value": "01/11/2025", "source": "history:1"},
        }}, ensure_ascii=False)


@pytest.mark.asyncio
async def test_temporal_reconciler_single_scan_uses_all_known_fields_as_anchors_and_resolves_all_missing_fields():
    from agent_framework.runtime.transaction_parameters import reconcile_transaction_parameters

    llm = _UnifiedAnchorScanLLM()
    result = await reconcile_transaction_parameters(
        llm,
        text="é a de quatorze e noventa e nove",
        tool_name="contestar_cobranca",
        parameter_names=["subject", "valor", "data"],
        known_arguments={"valor": 14.99},
        parameter_schema={
            "subject": {"type": "string", "description": "Nome do item ou cobrança a contestar."},
            "valor": {"type": "number", "description": "Valor monetário do item a contestar."},
            "data": {"type": "string", "description": "Data da cobrança selecionada."},
        },
        tool_description="Contesta uma cobrança após validação.",
        conversational_context=(
            "priority_3_previous_assistant_tool_or_evidence_context:\n"
            "history:1: assistant: Cobrança Tamboro Mensal no valor de R$ 14,99 no dia 01/11/2025 * "
            "TIM Fashion Mensal no valor de R$ 10,00 no dia 01/11/2025.\n"
            "priority_4_previous_user_utterances:\n"
            "history:2: user: tem uma cobrança aqui que eu não reconheço"
        ),
    )

    assert len(llm.calls) == 1
    assert result["values"]["subject"] == "Tamboro Mensal"
    assert float(result["values"]["valor"]) == 14.99
    assert result["values"]["data"] == "01/11/2025"
    assert result["provenance"]["subject"] == "history:1"

