"""Testes de ``vas_variation``: quais VAS avulsos variaram entre as duas faturas.

É a base do gatilho e do escopo da retenção de VAS no pedido de humano (SPEC §9),
então um erro aqui cancela o serviço errado do cliente. Cobre as duas formas de
payload (ISO-8601 + ``value`` string, como o runtime entrega; e ``dd/mm/yyyy`` +
float, da serialização alternativa), a partição por vencimento, o desempate de data
única, a exclusão do bucket ``streaming``, a contagem por cobrança (multiset) e todas
as portas de falha fechada."""
from __future__ import annotations

import logging
from decimal import Decimal
from pathlib import Path

import pytest
import yaml

from app.domain.contas.invoice_resolver import InvoiceResolver
from app.domain.contas.vas_variation import (
    VariedCharge,
    charge_match_key,
    varied_avulso_items,
    varied_current_charges,
    varied_vas_charges,
    variation_source,
)

PAST = "2026-03-10T00:00:00.000Z"
CURRENT = "2026-04-07T00:00:00.000Z"

_REPO_ROOT = Path(__file__).resolve().parents[2]


# ----- factories ------------------------------------------------------------


def _item(desc: str, value, invoice: str, **extra) -> dict:
    return {"desc": desc, "value": value, "invoice": invoice, **extra}


def _group(*items: dict, type_: str = "servicos_contratados_de_parceiros") -> dict:
    return {
        "type": type_,
        "desc": "Serviços de valor adicionado",
        "nItems": str(len(items)),
        "items": list(items),
    }


def _payload(*groups: dict, current: list | None = None) -> dict:
    payload: dict = {"invoiceVariation": list(groups)}
    if current is not None:
        payload["currentInvoice"] = current
    return payload


def _current_group(*descs_values) -> list:
    return [
        {
            "type": "servicos_contratados_de_parceiros",
            "desc": "Serviços de valor adicionado",
            "items": [
                {"desc": d, "value": v, "contestable": True} for d, v in descs_values
            ],
        }
    ]


def _names(charges: tuple[VariedCharge, ...]) -> list[str]:
    return sorted(c.desc for c in charges)


# ----- caminho principal ----------------------------------------------------


def test_entrou_na_atual_e_detectado():
    """Serviço presente só na fatura atual → variou (entrou)."""
    payload = _payload(
        _group(
            _item("Aluguel de Filme 1", "19.9", PAST),
            _item("Aluguel de Filme 1", "19.9", CURRENT),
            _item("Aluguel de Filme 3", "19.9", CURRENT),
        )
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Aluguel de Filme 3", value=Decimal("19.90")),
    )


def test_inalterados_nao_entram():
    """Itens idênticos nas duas faturas não variaram — e são a MAIORIA do grupo
    (o payload lista as duas faturas inteiras, não só o diff)."""
    payload = _payload(
        _group(
            _item("Carnal Mensal", "16.99", PAST),
            _item("MasterChef Mensal", "19.99", PAST),
            _item("Carnal Mensal", "16.99", CURRENT),
            _item("MasterChef Mensal", "19.99", CURRENT),
        )
    )
    assert varied_current_charges(payload) == ()


def test_mudanca_de_valor_conta_o_valor_novo():
    """Mesmo serviço com valor diferente: entra a cobrança da fatura ATUAL."""
    payload = _payload(
        _group(
            _item("Games Station Mensal", "14.9", PAST),
            _item("Games Station Mensal", "16.99", CURRENT),
        )
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Games Station Mensal", value=Decimal("16.99")),
    )


def test_removido_nao_oferta():
    """Variação causada só por REMOÇÃO: o avulso saiu e a conta caiu. Não há o que
    cancelar → nada a ofertar (decisão de produto, SPEC §9)."""
    payload = _payload(
        _group(
            _item("Monicaverso Semanal", "5.99", PAST),
            _item("Carnal Mensal", "16.99", PAST),
            _item("Carnal Mensal", "16.99", CURRENT),
        )
    )
    assert varied_current_charges(payload) == ()


def test_cobranca_duplicada_conta_as_duas():
    """O mesmo serviço cobrado 2× no ciclo são DUAS cobranças reais: a segunda
    entra como variação (multiset, não conjunto)."""
    payload = _payload(
        _group(
            _item("Qualifica Mensal", "19.99", PAST),
            _item("Qualifica Mensal", "19.99", CURRENT),
            _item("Qualifica Mensal", "19.99", CURRENT),
        )
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Qualifica Mensal", value=Decimal("19.99")),
    )


def test_duas_cobrancas_identicas_novas_contam_as_duas():
    """Duas cobranças IDÊNTICAS (mesmo nome E mesmo valor) que entraram na fatura
    atual devolvem DUAS ``VariedCharge`` — um ``set`` colapsaria em uma e metade da
    variação ficaria sem cancelar."""
    payload = _payload(
        _group(
            _item("Aluguel de Filme 1", "19.9", CURRENT),
            _item("Aluguel de Filme 1", "19.9", CURRENT),
        ),
        current=_current_group(
            ("Aluguel de Filme 1", "19.9"), ("Aluguel de Filme 1", "19.9")
        ),
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Aluguel de Filme 1", value=Decimal("19.90")),
        VariedCharge(desc="Aluguel de Filme 1", value=Decimal("19.90")),
    )


def test_duas_identicas_com_uma_na_passada_conta_apenas_uma():
    """Três cobranças iguais no total (1 na passada, 2 na atual): só UMA entrou. O
    multiset cancela o par e sobra uma — cancelar as duas cobraria do cliente um
    cancelamento que ele não pediu."""
    payload = _payload(
        _group(
            _item("Aluguel de Filme 1", "19.9", PAST),
            _item("Aluguel de Filme 1", "19.9", CURRENT),
            _item("Aluguel de Filme 1", "19.9", CURRENT),
        )
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Aluguel de Filme 1", value=Decimal("19.90")),
    )


# ----- classes que NÃO disparam ---------------------------------------------


def test_streaming_e_ignorado():
    """O bucket ``streaming`` é o VAS ESTRATÉGICO e nunca dispara retenção."""
    payload = _payload(
        _group(
            _item("Netflix Padrão", "20.9", CURRENT),
            _item("Disney+ Premium", "62.9", CURRENT),
            type_="streaming",
        )
    )
    assert varied_current_charges(payload) == ()


def test_grupo_avulso_ausente():
    """Só grupos de outras categorias (plano, juros) → nada de avulso."""
    payload = _payload(
        _group(_item("TIM Black Premium 2 0", "239.99", CURRENT), type_="plano")
    )
    assert varied_current_charges(payload) == ()


# ----- desempate de data única ---------------------------------------------


def test_data_unica_casando_a_atual_conta_como_entrada():
    """Uma só data no grupo (a outra fatura não tinha avulso): se as cobranças
    aparecem em ``currentInvoice``, o bloco é o lado ATUAL → tudo entrou."""
    payload = _payload(
        _group(
            _item("Gamedom Mensal", "14.99", CURRENT),
            _item("Carnal Mensal", "16.99", CURRENT),
        ),
        current=_current_group(("Gamedom Mensal", "14.99"), ("Carnal Mensal", "16.99")),
    )
    assert _names(varied_current_charges(payload)) == [
        "Carnal Mensal",
        "Gamedom Mensal",
    ]


def test_data_unica_sem_casar_a_atual_conta_como_saida():
    """Uma só data e as cobranças NÃO estão na fatura atual: o bloco é o lado
    PASSADO (tudo saiu) → nada a cancelar."""
    payload = _payload(
        _group(
            _item("Gamedom Mensal", "14.99", PAST),
            _item("Carnal Mensal", "16.99", PAST),
        ),
        current=_current_group(("Netflix Padrão", "20.9")),
    )
    assert varied_current_charges(payload) == ()


def test_data_unica_sem_current_invoice_nao_oferta():
    """Sem ``currentInvoice`` não há como desempatar a direção → falha fechada."""
    payload = _payload(_group(_item("Gamedom Mensal", "14.99", CURRENT)))
    assert varied_current_charges(payload) == ()


def test_data_unica_casa_item_em_outro_bucket_da_fatura_atual():
    """Os dois campos vêm de geradores distintos e DISCORDAM da classe do mesmo
    item (caso real: avulso na variação, ``streaming`` na fatura atual). O
    desempate pergunta "está na fatura atual?", não "de que classe é" — a classe é
    do resolver. Restringir ao bucket avulso inverteria a direção e perderia a
    cobrança."""
    payload = _payload(
        _group(_item("Truecaller Family Mensal", "13.99", CURRENT)),
        current=[
            {
                "type": "streaming",
                "desc": "Streamings",
                "items": [{"desc": "Truecaller Family Mensal", "value": "13.99"}],
            }
        ],
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Truecaller Family Mensal", value=Decimal("13.99")),
    )


# ----- formas de payload ----------------------------------------------------


def test_forma_alternativa_ddmmyyyy_e_float():
    """Serialização alternativa do mesmo pipeline: ``dd/mm/yyyy`` e ``value``
    float. Ordena por data real, não lexicograficamente."""
    payload = _payload(
        _group(
            _item("Fluid Mensal", 14.99, "10/07/2026"),
            _item("Fluid Mensal", 14.99, "10/08/2026"),
            _item("Neymar Jr.Experience-", 14.99, "10/08/2026"),
        )
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Neymar Jr.Experience-", value=Decimal("14.99")),
    )


def test_ordenacao_por_data_nao_lexicografica():
    """``dd/mm/yyyy``: 09/12/2025 é ANTERIOR a 09/01/2026 — ordenar como string
    inverteria a direção e cancelaria o item errado."""
    payload = _payload(
        _group(
            _item("Apps Club Mensal", 12.99, "09/12/2025"),
            _item("Apps Club Mensal", 12.99, "09/01/2026"),
            _item("Pocoyo Mensal", 19.99, "09/01/2026"),
        )
    )
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Pocoyo Mensal", value=Decimal("19.99")),
    )


def test_bucket_por_msisdn():
    """``invoiceVariation`` dentro de um bucket por linha (fatura família) é lido
    igual ao do top-level."""
    payload = {
        "Fatura Resumo": {"total": "100"},
        "2196460-1930": {
            "invoiceVariation": [
                _group(
                    _item("Fit Me App", "16.99", PAST),
                    _item("Fit Me App", "16.99", CURRENT),
                    _item("Budz Premium mensal", "14.99", CURRENT),
                )
            ]
        },
    }
    assert varied_current_charges(payload) == (
        VariedCharge(desc="Budz Premium mensal", value=Decimal("14.99")),
    )


def test_desc_devolvido_e_o_cru_da_fatura():
    """O ``desc`` devolvido é o texto CRU (com acento/pontuação), nunca o
    normalizado usado para casar."""
    payload = _payload(
        _group(_item("VOD +Canais Abertos", "19.9", CURRENT)),
        current=_current_group(("VOD +Canais Abertos", "19.9")),
    )
    assert varied_current_charges(payload)[0].desc == "VOD +Canais Abertos"


# ----- falha fechada --------------------------------------------------------


@pytest.mark.parametrize(
    "payload",
    [
        pytest.param(None, id="none"),
        pytest.param({}, id="vazio"),
        pytest.param({"invoiceVariation": []}, id="lista-vazia"),
        pytest.param({"invoiceVariation": "nao-e-lista"}, id="tipo-errado"),
        pytest.param({"invoiceVariation": [None, 42]}, id="grupos-invalidos"),
        pytest.param(
            {"invoiceVariation": [{"type": "servicos_contratados_de_parceiros"}]},
            id="grupo-sem-items",
        ),
    ],
)
def test_payload_invalido_devolve_vazio(payload):
    """Sem dado utilizável não oferta retenção — nunca promete cancelar o que não
    se sabe ter variado."""
    assert varied_current_charges(payload) == ()


def test_vencimento_ilegivel_aborta():
    """Item que não pode ser atribuído a nenhuma das duas faturas corromperia o
    diff — e um diff errado cancela o serviço errado. Aborta tudo."""
    payload = _payload(
        _group(
            _item("Carnal Mensal", "16.99", PAST),
            _item("Gamedom Mensal", "14.99", "sem-data"),
            _item("Gamedom Mensal", "14.99", CURRENT),
        )
    )
    assert varied_current_charges(payload) == ()


def test_item_sem_valor_nao_entra():
    """Cobrança sem identidade (valor não parseável) fica fora do diff."""
    payload = _payload(
        _group(
            _item("Carnal Mensal", "16.99", PAST),
            _item("Carnal Mensal", "16.99", CURRENT),
            _item("Servico Sem Valor", "n/a", CURRENT),
        )
    )
    assert varied_current_charges(payload) == ()


# ----- charge_match_key -----------------------------------------------------


def test_charge_match_key_normaliza_nome_e_valor():
    """A chave é a MESMA dos dois lados do casamento (cobrança variada × item do
    resolver): acento/caixa/``+`` normalizados, valor com 2 casas."""
    assert charge_match_key("VOD + Canais Abertos", "19.9") == charge_match_key(
        "vod +canais abertos", Decimal("19.90")
    )


@pytest.mark.parametrize(
    "desc,value",
    [("", "19.9"), (None, "19.9"), ("Carnal Mensal", None), ("Carnal Mensal", "n/a")],
)
def test_charge_match_key_sem_identidade(desc, value):
    assert charge_match_key(desc, value) is None


# ----- varied_avulso_items: cruzamento com a classificação da fatura ---------
#
# ``varied_current_charges`` (acima) sabe o que VARIOU; só o PDF parseado sabe o que é
# CANCELÁVEL. ``varied_avulso_items`` cruza os dois, e é a função que os DOIS
# consumidores chamam — a retenção no pedido de humano (SPEC §9) e o ramo NÃO do
# ``invoice_explanation``. Os dois guards contra estratégico mis-bucketado pela API
# vivem aqui, e antes só eram exercitados de through-the-runtime.


def _detail(sections: dict) -> dict:
    """``invoice_detail`` de uma linha, no formato que o resolver espera."""
    return {"11999999999": sections}


def _entrou(*descs_values) -> dict:
    """Análise em que TODAS as cobranças entraram na fatura atual: um único
    vencimento no bloco de variação, desempatado por ``currentInvoice`` (a forma que o
    runtime entrega quando a fatura passada não tinha avulso)."""
    return _payload(
        _group(*(_item(d, v, CURRENT) for d, v in descs_values)),
        current=_current_group(*descs_values),
    )


def _varied(invoice_detail: dict, variation: dict | None = None, *, by_charge=False):
    return varied_avulso_items(
        invoice_variation=variation,
        invoice_detail=invoice_detail,
        resolver=InvoiceResolver(),
        by_charge=by_charge,
    )


def test_avulso_variado_casa_nome_e_valor():
    """O caminho feliz: o serviço variou na análise E está na fatura como avulso."""
    detail = _detail(
        {"SVA Detalhe Total": [{"desc": "TIM Fashion Mensal", "value": 10.0}]}
    )

    items = _varied(detail, _entrou(("TIM Fashion Mensal", "10.0")))

    assert [it.canonical_name for it in items] == ["TIM Fashion Mensal"]


def test_guard_nome_estrategico_no_bucket_avulso_da_api():
    """Guard 1: a API bucketa streaming como avulso (visto em fatura real com
    ``YouTube Premium Mensal``) e a seção classifica por ``contestable``, então o item
    sairia ``avulso``. A lista fixa §12.A vence — estratégico não é cancelável."""
    detail = _detail(
        {"SVA Detalhe Total": [{"desc": "YouTube Premium Mensal", "value": 24.9}]}
    )

    assert _varied(detail, _entrou(("YouTube Premium Mensal", "24.9"))) == []


def test_guard_classe_mista_na_mesma_fatura():
    """Guard 2: o MESMO nome aparece como avulso numa seção e como bundle noutra —
    discordância entre os dois geradores da API. A não-cancelável vence."""
    detail = _detail(
        {
            "SVA Detalhe Total": [{"desc": "Tamboro Mensal", "value": 14.99}],
            "Serviços Bundle Inclusos": [{"desc": "Tamboro Mensal", "value": 14.99}],
        }
    )

    assert _varied(detail, _entrou(("Tamboro Mensal", "14.99"))) == []


def test_valor_divergente_entre_as_fontes_nao_casa(caplog):
    """Casar só por nome pegaria a cobrança ERRADA do mesmo serviço (2 cobranças, só
    uma variou). Fora da lista — e logado, para a taxa não sumir."""
    detail = _detail(
        {"SVA Detalhe Total": [{"desc": "Tamboro Mensal", "value": 14.99}]}
    )

    with caplog.at_level(logging.INFO):
        assert _varied(detail, _entrou(("Tamboro Mensal", "19.99"))) == []

    assert "value_mismatch" in caplog.text


def test_variado_ausente_da_fatura_e_logado(caplog):
    """A análise diz que variou, mas o serviço não está na fatura atual."""
    detail = _detail(
        {"SVA Detalhe Total": [{"desc": "Tamboro Mensal", "value": 14.99}]}
    )

    with caplog.at_level(logging.INFO):
        assert _varied(detail, _entrou(("Serviço Fantasma", "5.0"))) == []

    assert "name_absent" in caplog.text


def test_sem_variacao_devolve_lista_vazia():
    """Distinto de ``None``: sabemos que nada variou."""
    detail = _detail(
        {"SVA Detalhe Total": [{"desc": "Tamboro Mensal", "value": 14.99}]}
    )

    assert _varied(detail, _payload(_group())) == []


def test_sem_fatura_devolve_none():
    """``None`` = não deu para decidir. Cada consumidor escolhe o que fazer com a
    dúvida (a retenção não oferta; o ramo NÃO devolve ao orquestrador)."""
    assert _varied({}, _entrou(("X", "1.0"))) is None


def test_variation_source_prefere_a_analise():
    payload = _payload(_group(_item("X", "1.0", CURRENT)))
    assert variation_source(payload, {"outro": 1}) is payload


def test_variation_source_cai_no_invoice_detail():
    """Entradas em que o próprio ``invoice_detail`` já é o payload de análise."""
    detail = _payload(_group(_item("X", "1.0", CURRENT)))
    assert variation_source({}, detail) is detail
    assert variation_source(None, detail) is detail


# ----- varied_vas_charges: recorte AVULSO + ESTRATÉGICO ----------------------
#
# O ramo NÃO do ``invoice_explanation`` não pergunta "o que eu cancelo?" (aí só avulso
# serve, e é o recorte da retenção acima) — pergunta "existe assunto que eu trate?". O
# estratégico o agente trata pela tool ``vas_estrategico``, então ele conta.
#
# E, ao contrário da retenção, esta função NÃO cruza com o PDF: o ``type`` do grupo já é
# a classe. Os testes abaixo provam as duas metades disso — o grupo ``streaming`` entra,
# e a fatura não é consultada em nenhum caminho.


def _entrou_vas(*, avulsos=(), estrategicos=()) -> dict:
    """Análise em que TODAS as cobranças entraram na fatura atual, nos dois grupos de
    VAS (avulso e ``streaming``) — a forma que o runtime entrega quando a fatura passada
    não tinha VAS."""
    groups = []
    if avulsos:
        groups.append(_group(*(_item(d, v, CURRENT) for d, v in avulsos)))
    if estrategicos:
        groups.append(
            _group(
                *(_item(d, v, CURRENT) for d, v in estrategicos), type_="streaming"
            )
        )
    return _payload(*groups, current=_current_group(*avulsos, *estrategicos))


def _varied_vas(variation: dict | None, invoice_detail: dict | None = None):
    return varied_vas_charges(
        invoice_variation=variation, invoice_detail=invoice_detail
    )


def test_vas_variado_pega_o_estrategico_que_variou():
    """Variação causada só por streaming: o recorte da retenção não vê nada (não é
    cancelável), o do ramo NÃO vê — e é a diferença que faz o atendimento voltar ao
    orquestrador em vez de finalizar ``nao_resolvido``."""
    variation = _entrou_vas(estrategicos=[("Max Mensal", "34.9")])
    detail = _detail({"Streamings": [{"desc": "Max Mensal", "value": 34.9}]})

    assert _names(_varied_vas(variation)) == ["Max Mensal"]
    assert _varied(detail, variation) == []


def test_vas_variado_pega_o_avulso_tambem():
    """O recorte é mais largo, não outro: o avulso continua contando."""
    charges = _varied_vas(_entrou_vas(avulsos=[("TIM Fashion Mensal", "10.0")]))

    assert _names(charges) == ["TIM Fashion Mensal"]


def test_vas_variado_soma_os_dois_grupos():
    variation = _entrou_vas(
        avulsos=[("Tamboro Mensal", "14.99")], estrategicos=[("Max Mensal", "34.9")]
    )

    assert _names(_varied_vas(variation)) == ["Max Mensal", "Tamboro Mensal"]


def test_vas_variado_particiona_pela_uniao_dos_grupos():
    """A partição passada × atual é feita sobre a UNIÃO dos dois grupos. Aqui só o
    avulso tem as duas faturas e o streaming existe só na atual: lido sozinho, o
    streaming cairia no desempate de data única (que exige ``currentInvoice``); na
    união, os dois vencimentos do avulso dão a direção."""
    variation = _payload(
        _group(
            _item("Tamboro Mensal", "19.99", PAST),
            _item("Tamboro Mensal", "19.99", CURRENT),
        ),
        _group(_item("Max Mensal", "34.9", CURRENT), type_="streaming"),
    )

    assert _names(_varied_vas(variation)) == ["Max Mensal"]


def test_vas_variado_nao_consulta_a_fatura():
    """O contrato que separa este recorte do da retenção: a decisão sai só da análise.

    O mesmo payload de variação devolve o MESMO resultado com fatura ausente, com
    fatura que classifica o item como ``bundle`` e com fatura ilegível — três entradas
    que, no cruzamento antigo, davam três respostas diferentes e ENCERRAVAM a ligação."""
    variation = _entrou_vas(avulsos=[("Tamboro Mensal", "14.99")])
    faturas = [
        None,
        {},
        _detail({"Serviços Bundle Inclusos": [{"desc": "Tamboro Mensal", "value": 14.99}]}),
        {"vocalized_msisdn": {}},  # PDF não processado (incidente Neymar Jr.Experience)
        _detail({"SVA Detalhe Total": [{"desc": "Tamboro Mensal Premium", "value": 17.00}]}),
    ]

    assert all(_names(_varied_vas(variation, f)) == ["Tamboro Mensal"] for f in faturas)


def test_vas_variado_sem_variacao_devolve_vazio():
    """Nada variou nos dois grupos → finaliza ``nao_resolvido``."""
    assert _varied_vas(_payload(_group())) == ()


def test_vas_variado_sem_analise_devolve_vazio():
    """Análise ausente é indistinguível de "nada variou" — falha fechada de
    ``varied_current_charges``, herdada por este recorte (decisão de 2026-08-10)."""
    assert _varied_vas({"invoiceVariation": []}) == ()
    assert _varied_vas(None) == ()


def test_vas_variado_ignora_grupo_que_nao_e_vas():
    """Plano, juros e roaming vivem em outros ``type`` e não entram no recorte."""
    variation = _payload(
        _group(_item("Torpedos Internacionais", "0.79", CURRENT), type_="pacote_do_tim_viagem_roaming"),
        _group(_item("Multas", "2.59", CURRENT), type_="juros_e_multa_por_atraso_no_pagamento"),
        current=_current_group(("Torpedos Internacionais", "0.79"), ("Multas", "2.59")),
    )

    assert _varied_vas(variation) == ()


def test_vas_variado_le_a_analise_do_invoice_detail_como_fallback():
    """``variation_source``: entradas em que o ``invoice_detail`` JÁ é o payload de
    análise. É o único uso que sobrou do parâmetro."""
    analise = _entrou_vas(avulsos=[("Tamboro Mensal", "14.99")])

    assert _names(_varied_vas(None, analise)) == ["Tamboro Mensal"]


def test_grupo_streaming_fora_do_recorte_avulso():
    """A retenção lê SÓ o grupo avulso: streaming na variação não vira cobrança
    variada (é o default de ``varied_current_charges``)."""
    payload = _payload(
        _group(_item("Max Mensal", "34.9", CURRENT), type_="streaming"),
        current=_current_group(("Max Mensal", "34.9")),
    )

    assert varied_current_charges(payload) == ()


# ----- regressão contra dados reais -----------------------------------------
#
# A regra foi validada durante o desenvolvimento contra 88 pares de fatura reais
# (88/88, usando as faturas normalizadas como verdade). Esse corpus NÃO virou fixture:
# medido por mutação, ele pegava 6 de 8 defeitos injetados e nenhum que os testes acima
# já não peguem — era cego justamente ao multiset e ao parse de data. Somado a não ser
# regenerável (as CSVs de origem vivem fora do repo), virava asserção opaca. Os dois
# testes abaixo cobrem o que importa em dado real: a FORMA do payload de runtime.
#
# O dado vem COM o teste, não de mock do runtime: mock de gateway é editado à mão em
# teste manual, e um teste que o lê quebra por edição legítima. O cenário cy0001 é a
# exceção consciente — ele é a captura de um atendimento real e tem guarda de skip.


# Payload copiado do mock do gateway fake (`billingAnalysis` de `divergencia.json`),
# mas MANTIDO AQUI de propósito: aquele arquivo é mock de desenvolvimento e é editado à
# mão para exercitar ramos em teste manual (esvaziar `invoiceVariation` reproduz o ramo
# "sem VAS avulso"). Lendo-o, este teste ficava vermelho por causa de uma edição
# legítima — falha pelo motivo errado. O dado é do teste; o mock é do runtime local.
_FATURA_COM_AVULSOS_VARIADOS = {
    "invoiceVariation": [
        {
            "value": "46.99",
            "type": "servicos_contratados_de_parceiros",
            "desc": "Serviços de valor adicionado",
            "nItems": "10",
            "items": [
                # Fatura PASSADA (venc. 20/10) — 3 cobranças.
                _item("Tamboro Mensal", "19.99", "2025-10-20T00:00:00.000Z"),
                _item("VOD + Canais abertos", "10.0", "2025-10-20T00:00:00.000Z"),
                _item("VOD + Canais abertos", "10.0", "2025-10-20T00:00:00.000Z"),
                # Fatura ATUAL (venc. 20/11) — as 3 acima, inalteradas...
                _item("Tamboro Mensal", "19.99", "2025-11-20T00:00:00.000Z"),
                _item("VOD + Canais abertos", "10.0", "2025-11-20T00:00:00.000Z"),
                _item("VOD + Canais abertos", "10.0", "2025-11-20T00:00:00.000Z"),
                # ...mais 4 que ENTRARAM.
                _item("Tamboro Mensal", "14.99", "2025-11-20T00:00:00.000Z"),
                _item("TIM Fashion Mensal", "10.0", "2025-11-20T00:00:00.000Z"),
                _item("Neymar Jr", "12.0", "2025-11-20T00:00:00.000Z"),
                _item("Youtube Premium", "10.0", "2025-11-20T00:00:00.000Z"),
            ],
        }
    ]
}


def test_fatura_realista_expoe_so_os_avulsos_que_entraram():
    """Payload no formato do runtime (ISO-8601 + ``value`` string), com as três
    situações que convivem numa fatura real:

    - **entraram:** ``Tamboro Mensal`` R$14,99, ``TIM Fashion Mensal`` R$10,00,
      ``Neymar Jr`` R$12,00 e ``Youtube Premium`` R$10,00;
    - **não variaram:** ``Tamboro Mensal`` R$19,99 — mesmo NOME de um que variou, outra
      cobrança: é a discriminação por VALOR que a feature existe para fazer — e
      ``VOD + Canais abertos`` R$10,00 ×2 (multiplicidade preservada dos dois lados);
    - ``Youtube Premium`` vem no bucket avulso, reproduzindo o mis-bucket real da API;
      barrá-lo é do ``varied_avulso_items`` (guard de estratégico), não daqui.

    Soma da variação: R$46,99 = R$36,99 canceláveis + R$10,00 do estratégico."""
    got = {
        (c.desc, str(c.value))
        for c in varied_current_charges(_FATURA_COM_AVULSOS_VARIADOS)
    }

    assert got == {
        ("Tamboro Mensal", "14.99"),
        ("TIM Fashion Mensal", "10.00"),
        ("Neymar Jr", "12.00"),
        ("Youtube Premium", "10.00"),
    }
    canceláveis = {v for d, v in got if d != "Youtube Premium"}
    assert sum(Decimal(v) for v in canceláveis) == Decimal("36.99")


def test_cenario_real_cy0001_pega_so_o_item_que_variou():
    """Payload de runtime REAL (ISO-8601 + ``value`` string, `nItems` camelCase).

    A fatura tem 5 avulsos cobrados, 4 deles idênticos ao ciclo anterior — só
    `Aluguel de Filme 3` entrou. O critério antigo ("a fatura TEM avulso")
    enfileirava os 5 para cancelamento."""
    scenario = (
        _REPO_ROOT
        / "automated_conversation_test/scenarios"
        / "cy0001_explicacao_de_fatura_planos_pos"
        / "cy0001_explicacao_de_fatura_planos_pos.yaml"
    )
    if not scenario.exists():  # pragma: no cover - cenário opcional no checkout
        pytest.skip("cenário cy0001 ausente")
    payloads: list[dict] = []

    def walk(node):
        if isinstance(node, dict):
            if "invoiceVariation" in node:
                payloads.append(node)
            for value in node.values():
                walk(value)
        elif isinstance(node, list):
            for value in node:
                walk(value)

    walk(yaml.safe_load(scenario.read_text(encoding="utf-8")))
    assert payloads, "cenário sem invoiceVariation"
    assert varied_current_charges(payloads[0]) == (
        VariedCharge(desc="Aluguel de Filme 3", value=Decimal("19.90")),
    )


# ----- nome curto na variação × nome qualificado na fatura -------------------
# Incidente e081812886a (URA 06/08): a análise de variação traz o nome CURTO do
# serviço ("FIT ME App") e a fatura traz o nome COMPLETO ("FIT ME App Premium
# Mensal"). A igualdade estrita de nome descartava o item, ``_has_retention_vas``
# devolvia False e o pedido de atendente encerrava ``nao_resolvido`` sem oferecer
# a retenção. O casamento é por PREFIXO DE TOKENS, com trava de valor ao centavo,
# item avulso pós-guards e candidato único — ambiguidade falha FECHADA (SPEC §9).


@pytest.fixture
def prefixo_de_tokens_ligado(monkeypatch):
    """Liga ``TIM_VAS_QUALIFIED_NAME_MATCH``.

    A segunda passada é DESLIGADA por padrão (ver o comentário da flag em
    ``vas_variation``): o mesmo prefixo qualifica o mesmo produto em
    "FIT ME App"→"FIT ME App Premium Mensal" e separa produtos DIFERENTES em
    "Fluid Light"×"Fluid Premium". Os testes abaixo fixam o comportamento da
    passada em si, então precisam ligá-la explicitamente."""
    from app.domain.contas import vas_variation

    monkeypatch.setattr(vas_variation, "_VAS_QUALIFIED_NAME_MATCH", True)


def test_prefixo_de_tokens_desligado_por_padrao():
    """Contrato do default: sem a flag, o caso do incidente NÃO casa.

    É o custo aceito de eliminar o falso positivo "TIM Games"→"TIM Games
    Premium", que tem forma idêntica e nenhum sinal disponível para separar."""
    detail = _detail(
        {"Itens Eventuais": [{"desc": "FIT ME App Premium Mensal", "value": 16.99}]}
    )

    assert _varied(detail, _entrou(("FIT ME App", "16.99"))) == []


def test_nome_curto_casa_com_nome_qualificado_da_fatura(prefixo_de_tokens_ligado):
    """Com a flag ligada: variação "FIT ME App" × fatura "FIT ME App Premium
    Mensal", mesmo valor → casa."""
    detail = _detail(
        {"Itens Eventuais": [{"desc": "FIT ME App Premium Mensal", "value": 16.99}]}
    )

    items = _varied(detail, _entrou(("FIT ME App", "16.99")))

    assert [it.canonical_name for it in items] == ["FIT ME App Premium Mensal"]


def test_nome_identico_continua_casando_pela_primeira_passada():
    """A segunda passada não pode roubar o que a igualdade estrita já resolveu:
    nome idêntico casa uma única vez (sem duplicar o item)."""
    detail = _detail(
        {"Itens Eventuais": [{"desc": "FIT ME App", "value": 16.99}]}
    )

    items = _varied(detail, _entrou(("FIT ME App", "16.99")))

    assert [it.canonical_name for it in items] == ["FIT ME App"]


def test_valor_divergente_nao_casa_mesmo_com_prefixo(prefixo_de_tokens_ligado):
    """Trava de valor: prefixo de nome bate, centavo não → fora. Cancelar por
    nome parecido com valor diferente é cancelar o serviço errado."""
    detail = _detail(
        {"Itens Eventuais": [{"desc": "FIT ME App Premium Mensal", "value": 29.90}]}
    )

    assert _varied(detail, _entrou(("FIT ME App", "16.99"))) == []


def test_prefixo_no_meio_do_nome_nao_casa(prefixo_de_tokens_ligado):
    """Não é substring solta: os tokens da variação têm de ser o INÍCIO do nome
    do item. "ME App" no meio de "FIT ME App Premium" não casa."""
    detail = _detail(
        {"Itens Eventuais": [{"desc": "FIT ME App Premium Mensal", "value": 16.99}]}
    )

    assert _varied(detail, _entrou(("ME App", "16.99"))) == []


def test_ambiguidade_falha_fechada(caplog, prefixo_de_tokens_ligado):
    """Dois itens com o mesmo prefixo e o mesmo valor → NENHUM é escolhido.
    SPEC §9: nunca prometer cancelamento do que não se sabe ter variado."""
    detail = _detail(
        {
            "Itens Eventuais": [
                {"desc": "Gamedom Mensal", "value": 16.99},
                {"desc": "Gamedom Semanal", "value": 16.99},
            ]
        }
    )

    with caplog.at_level(logging.INFO):
        items = _varied(detail, _entrou(("Gamedom", "16.99")))

    assert items == []
    assert "reason=multiple_candidates" in caplog.text


def test_nome_da_variacao_mais_longo_que_o_da_fatura_nao_casa(prefixo_de_tokens_ligado):
    """Direção única: a variação é o prefixo do item, nunca o contrário. Nome da
    variação mais específico que o da fatura não casa (evita casar "X Premium"
    com um "X" que pode ser outro serviço)."""
    detail = _detail({"Itens Eventuais": [{"desc": "FIT ME App", "value": 16.99}]})

    assert _varied(detail, _entrou(("FIT ME App Premium Mensal", "16.99"))) == []


def test_estrategico_com_nome_qualificado_continua_fora(prefixo_de_tokens_ligado):
    """Os guards não são contornáveis pela segunda passada: estratégico não é
    cancelável (SPEC §12.A), mesmo casando por prefixo e valor."""
    detail = _detail(
        {"Itens Eventuais": [{"desc": "YouTube Premium Mensal", "value": 24.90}]}
    )

    assert _varied(detail, _entrou(("YouTube Premium", "24.90"))) == []


# ----- linha de destino: msisdn e ambiguidade entre linhas -------------------
# A fila desta rota é autorizada SEM confirmação pendente, e o ``msisdn`` vai cru
# para os args de ``cancelar_vas_avulso``. Cancelar na linha errada — ou sem
# linha — é dano irreversível no cliente, então os dois casos falham FECHADOS.
# Nenhum destes cenários tinha cobertura antes: os helpers do arquivo montam uma
# única linha (``_detail``), e a suíte inteira era cega a fatura multi-linha.


def _detail_multi(buckets: dict) -> dict:
    """``invoice_detail`` com VÁRIAS linhas: ``{msisdn: {seção: [entries]}}``."""
    return dict(buckets)


def test_msisdn_ausente_nao_produz_item_cancelavel(caplog):
    """Bucket sem linha → nenhum item cancelável, com razão própria.

    Sem ``msisdn`` não há para onde mandar o cancelamento. O descarte não pode
    sair como ``name_absent``: o nome estava lá, faltava a linha."""
    detail = _detail_multi({"": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]}})

    with caplog.at_level(logging.INFO):
        items = _varied(detail, _entrou(("Fit Me App", "16.99")))

    assert items == []
    assert "reason=missing_msisdn" in caplog.text


def test_mesma_cobranca_em_duas_linhas_falha_fechada(caplog):
    """Mesmo ``(nome, valor)`` em duas linhas e só UMA variou → não se sabe qual."""
    detail = _detail_multi(
        {
            "11999999999": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
            "11888888888": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
        }
    )

    with caplog.at_level(logging.INFO):
        items = _varied(detail, _entrou(("Fit Me App", "16.99")))

    assert items == []
    assert "reason=multiple_candidates" in caplog.text


def test_ordem_das_linhas_nao_altera_a_decisao():
    """A ordem do payload nunca pode mudar o resultado.

    Antes, o loop era item-driven e consumia a cobrança no PRIMEIRO item casado —
    o cancelamento saía na linha que o dict listasse antes."""
    linha_a = {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]}
    linha_b = {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]}

    ab = _varied(_detail_multi({"11999999999": linha_a, "11888888888": linha_b}),
                 _entrou(("Fit Me App", "16.99")))
    ba = _varied(_detail_multi({"11888888888": linha_b, "11999999999": linha_a}),
                 _entrou(("Fit Me App", "16.99")))

    assert ab == ba == []


def test_cobranca_so_na_linha_secundaria_casa_naquela_linha():
    """Fatura multi-linha NÃO é motivo de recusa: o que barra é a chave ambígua.

    Com o serviço em uma só das linhas há candidato único → casa, e o ``msisdn``
    devolvido é o da linha CERTA (não o da primeira do payload)."""
    detail = _detail_multi(
        {
            "11999999999": {"Itens Eventuais": [{"desc": "Fluid Light", "value": 12.90}]},
            "11888888888": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
        }
    )

    items = _varied(detail, _entrou(("Fit Me App", "16.99")))

    assert [(it.canonical_name, it.msisdn) for it in items] == [
        ("Fit Me App", "11888888888")
    ]


def test_multiplicidade_preservada_quando_a_contagem_bate():
    """SPEC §9: o mesmo serviço cobrado 2× no ciclo são DUAS cobranças, e cada uma
    entra na fila. A trava de candidato é por contagem, não "no máximo um" — duas
    variações + duas cobranças na mesma linha casam as duas."""
    detail = _detail(
        {
            "Itens Eventuais": [
                {"desc": "Aluguel de Filme 1", "value": 19.90, "period": "05/04/26"},
                {"desc": "Aluguel de Filme 1", "value": 19.90, "period": "18/04/26"},
            ]
        }
    )

    items = _varied(
        detail,
        _entrou(("Aluguel de Filme 1", "19.90"), ("Aluguel de Filme 1", "19.90")),
        by_charge=True,
    )

    assert [it.canonical_name for it in items] == [
        "Aluguel de Filme 1",
        "Aluguel de Filme 1",
    ]


# ----- ORDEM DOS GUARDS: identidade antes de executabilidade -----------------
# O caso que separa um matcher correto de um perigoso. Com uma cobrança variada
# e DUAS identidades possíveis — uma executável, outra não —, filtrar por
# ``msisdn`` primeiro COROARIA a executável: o descarte da outra viraria
# "evidência" de que sobrou a certa. Não é. A ambiguidade de identidade é
# anterior e independente da executabilidade.


def _duas_cobrancas_iguais(desc: str, value: str) -> dict:
    """Análise em que a MESMA cobrança variou duas vezes no ciclo."""
    return _payload(
        _group(_item(desc, value, CURRENT), _item(desc, value, CURRENT)),
        current=_current_group((desc, value), (desc, value)),
    )


def test_uma_variacao_e_duas_identidades_uma_sem_msisdn_falha_fechada(caplog):
    """CASO A: 1 cobrança variada, 2 itens de mesma identidade (um sem linha).

    O item sem ``msisdn`` NÃO pode fazer o outro vencer por eliminação."""
    detail = {
        "11999999999": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
        "": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
    }

    with caplog.at_level(logging.INFO):
        items = _varied(detail, _entrou(("Fit Me App", "16.99")))

    assert items == []
    assert "reason=multiple_candidates" in caplog.text


def test_ordem_nao_altera_o_caso_a():
    """CASO A permutado: quem aparece primeiro no payload é irrelevante."""
    com_linha = {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]}
    sem_linha = {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]}
    variacao = _entrou(("Fit Me App", "16.99"))

    ab = _varied({"11999999999": com_linha, "": sem_linha}, variacao)
    ba = _varied({"": sem_linha, "11999999999": com_linha}, variacao)

    assert ab == ba == []


def test_duas_variacoes_e_duas_identidades_uma_sem_msisdn(caplog):
    """CASO B: 2 cobranças variadas para 2 identidades → não há ambiguidade.

    A executável entra na fila; a sem linha é descartada com a razão própria e
    NÃO herda o ``msisdn`` da outra."""
    detail = {
        "11999999999": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
        "": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
    }

    with caplog.at_level(logging.INFO):
        items = _varied(detail, _duas_cobrancas_iguais("Fit Me App", "16.99"))

    assert [(it.canonical_name, it.msisdn) for it in items] == [
        ("Fit Me App", "11999999999")
    ]
    assert "reason=missing_msisdn" in caplog.text
    # a razão certa, e SÓ ela
    assert "reason=name_absent" not in caplog.text
    assert "reason=value_mismatch" not in caplog.text


def test_descarte_emite_exatamente_um_evento_por_cobranca(caplog):
    """Telemetria: uma cobrança descartada gera UM evento.

    Reportar a mesma cobrança como ``multiple_candidates`` e depois como
    ``value_mismatch`` no resíduo corrompe a taxa de não-casamento."""
    detail = {
        "11999999999": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
        "11888888888": {"Itens Eventuais": [{"desc": "Fit Me App", "value": 16.99}]},
    }

    with caplog.at_level(logging.INFO):
        _varied(detail, _entrou(("Fit Me App", "16.99")))

    eventos = [m for m in caplog.messages if "varied_charge_unmatched" in m]
    assert len(eventos) == 1, eventos
    assert "reason=multiple_candidates" in eventos[0]


# ----- desempate por DATA DE COBRANÇA ----------------------------------------
# A análise de variação não carrega data utilizável (o campo estruturado vem
# sentinela/ausente em produção) e não carrega msisdn. A única fonte de data é o
# texto CRU do ``invoiceExplanation`` (``invoice_explanation_base``):
#
#     * Cobrança FIT ME App no valor de R$ 16.99 no dia 29/06/26.
#
# O PDF traz a outra ponta em ``ResolvedInvoiceItem.charge_date`` (do ``period``).
# A data só ESTREITA candidatos que hoje seriam descartados — nunca amplia, nunca
# elimina por ausência, e a cardinalidade tem de fechar exatamente.

from app.domain.contas.vas_variation import (  # noqa: E402
    _charge_date_key,
    _explained_charges,
)


def _det_linhas(*entries) -> dict:
    """``invoice_detail`` multi-linha: ``(desc, value, period, msisdn)``."""
    buckets: dict = {}
    for desc, value, period, msisdn in entries:
        entry = {"desc": desc, "value": value}
        if period is not None:
            entry["period"] = period
        buckets.setdefault(msisdn, {}).setdefault("SVA Detalhe Total", []).append(entry)
    return buckets


def _varied_expl(detail, variation, explanation=None, *, by_charge=True):
    return varied_avulso_items(
        invoice_variation=variation,
        invoice_detail=detail,
        resolver=InvoiceResolver(),
        by_charge=by_charge,
        explanation_text=explanation,
    )


def _bullet(desc, value, dia):
    return f"* Cobrança {desc} no valor de R$ {value} no dia {dia}."


# ----- normalizador ----------------------------------------------------------


@pytest.mark.parametrize(
    "raw,esperado",
    [
        ("2025-11-01T00:00:00.000Z", ("2025", "11", "01")),
        ("01/11/25", ("2025", "11", "01")),
        ("25/06/26", ("2026", "06", "25")),
        ("01/11/2025", ("2025", "11", "01")),
        ("0000-12-31T20:53:32.000Z", None),  # sentinela do backend
        ("", None),
        (None, None),
        ("lixo", None),
        ("32/13/25", None),  # data impossível
    ],
    ids=lambda v: str(v)[:26],
)
def test_charge_date_key_normaliza_ou_recusa(raw, esperado):
    """Sentinela e ilegível viram ``None``: "não sei" tem de ser indistinguível
    de "não tem", senão vira desempate falso."""
    assert _charge_date_key(raw) == esperado


def test_explained_charges_so_extrai_bullet_com_data():
    """Formatos sem dia não participam — sem data não há evidência."""
    texto = (
        "Houve a variação dos seguintes serviços de valor adicionado:\n"
        "* Cobrança FIT ME App no valor de R$ 16.99 no dia 29/06/26.\n"
        "* Gamedom Mensal no valor de R$ 14,99 no dia 25/06/26.\n"
        "* MasterChef variou em R$ 19.99.\n"
        "Foram cobrados os seguintes serviços de terceiros: PARAGUAI."
    )

    achados = _explained_charges(texto)

    assert [(c.desc, str(c.value), c.date_key) for c in achados] == [
        ("FIT ME App", "16.99", ("2026", "06", "29")),
        ("Gamedom Mensal", "14.99", ("2026", "06", "25")),
    ]


@pytest.mark.parametrize("texto", ["", "   ", None, "texto sem nada", "x" * 20000])
def test_explained_charges_degrada_sem_quebrar(texto):
    assert _explained_charges(texto) == [] or all(
        c.date_key for c in _explained_charges(texto)
    )


# ----- Caso A: nome qualificado (o incidente e081812886a) --------------------


def test_nome_qualificado_casa_por_valor_e_data():
    """"FIT ME App" na análise × "FIT ME App Premium Mensal" no PDF: os nomes
    divergem, mas valor ao centavo + data identificam a MESMA cobrança."""
    detail = _det_linhas(
        ("FIT ME App Premium Mensal", "16.99", "29/06/26", "11999999999")
    )
    variacao = _entrou(("FIT ME App", "16.99"))

    assert _varied_expl(detail, variacao) == []  # sem explicação: fail closed
    items = _varied_expl(detail, variacao, _bullet("FIT ME App", "16.99", "29/06/26"))

    assert [(it.canonical_name, it.msisdn) for it in items] == [
        ("FIT ME App Premium Mensal", "11999999999")
    ]


def test_nome_qualificado_com_data_divergente_nao_casa():
    """A ponte é (valor, data). Data diferente → não é a mesma cobrança."""
    detail = _det_linhas(
        ("FIT ME App Premium Mensal", "16.99", "29/06/26", "11999999999")
    )

    assert (
        _varied_expl(
            detail,
            _entrou(("FIT ME App", "16.99")),
            _bullet("FIT ME App", "16.99", "30/06/26"),
        )
        == []
    )


# ----- Caso B: mesmo valor, produtos diferentes, datas diferentes ------------


def test_mesmo_valor_produtos_diferentes_datas_distintas():
    """Dois produtos ao mesmo preço não podem ser separados por valor — a data
    separa, e os DOIS casam corretamente."""
    detail = _det_linhas(
        ("MasterChef Mensal", "19.99", "14/06/26", "11999999999"),
        ("Bancah Jornais II", "19.99", "18/06/26", "11999999999"),
    )
    variacao = _entrou(("MasterChef Mensal", "19.99"), ("Bancah Jornais II", "19.99"))
    explicacao = (
        _bullet("MasterChef Mensal", "19.99", "14/06/26")
        + "\n"
        + _bullet("Bancah Jornais II", "19.99", "18/06/26")
    )

    items = _varied_expl(detail, variacao, explicacao)

    assert sorted(it.canonical_name for it in items) == [
        "Bancah Jornais II",
        "MasterChef Mensal",
    ]


# ----- Caso C: titular × dependente (a colisão de linha) ---------------------


def test_titular_e_dependente_datas_diferentes_casa_a_linha_certa(caplog):
    """Mesmo nome, mesmo valor, DUAS linhas: a data diz qual variou."""
    detail = _det_linhas(
        ("TIM Fashion Mensal", "10.00", "01/11/25", "1199999999"),
        ("TIM Fashion Mensal", "10.00", "30/04/25", "91998188119"),
    )

    with caplog.at_level(logging.INFO):
        items = _varied_expl(
            detail,
            _entrou(("TIM Fashion Mensal", "10.00")),
            _bullet("TIM Fashion Mensal", "10.00", "01/11/25"),
        )

    assert [(it.canonical_name, it.msisdn) for it in items] == [
        ("TIM Fashion Mensal", "1199999999")
    ]
    assert "reason=charge_date" in caplog.text


def test_titular_e_dependente_mesma_data_falha_fechada(caplog):
    """Datas iguais não desempatam nada."""
    detail = _det_linhas(
        ("TIM Fashion Mensal", "10.00", "01/11/25", "1199999999"),
        ("TIM Fashion Mensal", "10.00", "01/11/25", "91998188119"),
    )

    with caplog.at_level(logging.INFO):
        items = _varied_expl(
            detail,
            _entrou(("TIM Fashion Mensal", "10.00")),
            _bullet("TIM Fashion Mensal", "10.00", "01/11/25"),
        )

    assert items == []
    assert "reason=multiple_candidates" in caplog.text
    assert "date_hint=ambiguous" in caplog.text


def test_candidato_sem_data_nao_pode_ser_eliminado(caplog):
    """A REGRA CRÍTICA: ausência de data NÃO é evidência negativa.

    Com um candidato datado e outro sem data, escolher o datado seria decidir
    por eliminação — proibido."""
    detail = _det_linhas(
        ("TIM Fashion Mensal", "10.00", "01/11/25", "1199999999"),
        ("TIM Fashion Mensal", "10.00", None, "91998188119"),
    )

    with caplog.at_level(logging.INFO):
        items = _varied_expl(
            detail,
            _entrou(("TIM Fashion Mensal", "10.00")),
            _bullet("TIM Fashion Mensal", "10.00", "01/11/25"),
        )

    assert items == []
    assert "date_hint=undated" in caplog.text


def test_ambos_sem_data_falha_fechada():
    detail = _det_linhas(
        ("TIM Fashion Mensal", "10.00", None, "1199999999"),
        ("TIM Fashion Mensal", "10.00", None, "91998188119"),
    )

    assert (
        _varied_expl(
            detail,
            _entrou(("TIM Fashion Mensal", "10.00")),
            _bullet("TIM Fashion Mensal", "10.00", "01/11/25"),
        )
        == []
    )


def test_sem_hint_para_a_cobranca_falha_fechada(caplog):
    """Explicação existe mas não menciona ESTA cobrança."""
    detail = _det_linhas(
        ("TIM Fashion Mensal", "10.00", "01/11/25", "1199999999"),
        ("TIM Fashion Mensal", "10.00", "30/04/25", "91998188119"),
    )

    with caplog.at_level(logging.INFO):
        items = _varied_expl(
            detail,
            _entrou(("TIM Fashion Mensal", "10.00")),
            _bullet("Outro Servico", "99.99", "01/11/25"),
        )

    assert items == []
    assert "date_hint=absent" in caplog.text


def test_sentinela_na_explicacao_nao_vira_evidencia():
    """Bullet com data sentinela é descartado no parser."""
    detail = _det_linhas(
        ("TIM Fashion Mensal", "10.00", "01/11/25", "1199999999"),
        ("TIM Fashion Mensal", "10.00", "30/04/25", "91998188119"),
    )

    assert (
        _varied_expl(
            detail,
            _entrou(("TIM Fashion Mensal", "10.00")),
            "* Cobrança TIM Fashion Mensal no valor de R$ 10.00 no dia 31/12/00.",
        )
        == []
    )


# ----- cardinalidade e reuso -------------------------------------------------


def test_resolucao_parcial_e_proibida():
    """``missing=2`` e a data identifica só 1 → nada casa (tudo ou nada)."""
    detail = _det_linhas(
        ("Aya Books Premium", "9.99", "10/06/26", "11999999999"),
        ("Aya Books Premium", "9.99", "20/06/26", "11999999999"),
        ("Aya Books Premium", "9.99", "30/06/26", "11999999999"),
    )
    variacao = _entrou(("Aya Books Premium", "9.99"), ("Aya Books Premium", "9.99"))

    items = _varied_expl(
        detail, variacao, _bullet("Aya Books Premium", "9.99", "10/06/26")
    )

    assert items == []


def test_multiplicidade_real_dois_hints_casam_dois_itens():
    """Cardinalidade fecha: 2 cobranças variadas, 2 hints, 2 itens datados."""
    detail = _det_linhas(
        ("Aya Books Premium", "9.99", "10/06/26", "11999999999"),
        ("Aya Books Premium", "9.99", "20/06/26", "11999999999"),
    )
    variacao = _entrou(("Aya Books Premium", "9.99"), ("Aya Books Premium", "9.99"))
    explicacao = (
        _bullet("Aya Books Premium", "9.99", "10/06/26")
        + "\n"
        + _bullet("Aya Books Premium", "9.99", "20/06/26")
    )

    items = _varied_expl(detail, variacao, explicacao)

    assert len(items) == 2
    assert sorted(it.charge_date for it in items) == ["10/06/26", "20/06/26"]


def test_hint_nao_e_reutilizado_por_duas_cobrancas():
    """Um bullet identifica UMA cobrança.

    Duas chaves distintas (nomes curtos diferentes) disputam o mesmo hint no
    passe residual: a primeira o consome, a segunda fica sem evidência e falha
    fechada — o hint NÃO é reaproveitado."""
    detail = _det_linhas(
        ("Aya Books Premium", "9.99", "10/06/26", "11999999999"),
        ("Aya Ensinah Premium", "9.99", "20/06/26", "11999999999"),
    )
    variacao = _entrou(("Aya Books", "9.99"), ("Aya Ensinah", "9.99"))

    items = _varied_expl(
        detail, variacao, _bullet("Aya Books", "9.99", "10/06/26")
    )

    assert [it.canonical_name for it in items] == ["Aya Books Premium"]


def test_guards_valem_no_passe_por_data():
    """Estratégico não é resgatado pela data."""
    detail = _det_linhas(
        ("YouTube Premium Mensal", "24.90", "10/06/26", "11999999999")
    )

    assert (
        _varied_expl(
            detail,
            _entrou(("YouTube Premium", "24.90")),
            _bullet("YouTube Premium", "24.90", "10/06/26"),
        )
        == []
    )


def test_msisdn_ausente_continua_bloqueando_no_passe_por_data(caplog):
    """Guard 3 vale nos dois passes: identidade resolvida, mas sem linha."""
    detail = _det_linhas(("FIT ME App Premium Mensal", "16.99", "29/06/26", ""))

    with caplog.at_level(logging.INFO):
        items = _varied_expl(
            detail,
            _entrou(("FIT ME App", "16.99")),
            _bullet("FIT ME App", "16.99", "29/06/26"),
        )

    assert items == []
    assert "reason=missing_msisdn" in caplog.text


def test_explicacao_nao_altera_casamento_ja_valido():
    """Nenhum match existente pode regredir por a explicação existir."""
    detail = _det_linhas(("Tamboro Mensal", "19.99", "02/11/25", "11999999999"))
    variacao = _entrou(("Tamboro Mensal", "19.99"))

    sem = _varied_expl(detail, variacao)
    com = _varied_expl(detail, variacao, _bullet("Tamboro Mensal", "19.99", "02/11/25"))

    assert [it.canonical_name for it in sem] == ["Tamboro Mensal"]
    assert [it.canonical_name for it in com] == ["Tamboro Mensal"]


def test_passe_residual_exige_cardinalidade_exata():
    """Duas cobranças datadas candidatas para UMA variada → nada casa.

    Sem a igualdade estrita (``!= missing``), um ``< missing`` deixaria a fila
    levar as DUAS — cancelando uma cobrança que ninguém disse ter variado."""
    detail = _det_linhas(
        ("Aya Books Premium", "9.99", "10/06/26", "11999999999"),
        ("Aya Ensinah Premium", "9.99", "10/06/26", "11999999999"),
    )
    explicacao = (
        _bullet("Aya", "9.99", "10/06/26") + "\n" + _bullet("Aya", "9.99", "10/06/26")
    )

    assert _varied_expl(detail, _entrou(("Aya", "9.99")), explicacao) == []
