"""Suíte ADVERSARIAL do casamento ``invoiceVariation`` → ``invoice_detail``.

Contraexemplos difíceis para uma **futura** camada de similaridade. Nada aqui liga
similaridade no fluxo real: os 50 casos fixam o que o matcher determinístico faz hoje
e medem, em **shadow**, o score que uma camada de similaridade veria — para responder
*antes* de mexer em produção se existe threshold que separe com segurança.

Assimetria que governa o desenho: nesta rota a fila de cancelamento é autorizada **sem
confirmação pendente** (``runtime.py`` ``_handle_execute_action_queue``), então um falso
positivo não vira pergunta ao cliente — vira **cancelamento executado**. Logo::

    FALSO POSITIVO >> FALSO NEGATIVO        na dúvida, NÃO CASAR

Os nomes de serviço são reais do repositório (fixtures/cenários): ``Fluid Light`` ×
``Fluid Premium``, ``Aya Books Premium`` × ``Aya Audiobooks Premium``, ``EXA Cloud
500GB`` × ``EXA Segurança Premium``, ``Fit Me App``. Colisões inventadas seriam mais
fáceis que a realidade.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal

import pytest

from app.domain.contas.invoice_resolver import InvoiceResolver
from app.domain.contas.item_matcher import SimilarityItemMatcher
from app.domain.contas.vas_variation import varied_avulso_items

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

LINE_A = "11999999999"
LINE_B = "11888888888"

# Seções por classe resultante (``invoice_resolver.SECTION_DEFAULTS``).
AVULSO = "SVA Detalhe Total"
EVENTUAIS = "Itens Eventuais"
ESTRATEGICO = "Serviços Contratados de Terceiros"
BUNDLE = "Serviços Bundle Inclusos"
# Fora de SECTION_DEFAULTS → o item existe na fatura mas não é acionável.
SEM_CLASSE = "Mensalidades Adicionais"


# ----- modelo dos casos -------------------------------------------------------


@dataclass(frozen=True)
class Entry:
    """Uma linha de item no ``invoice_detail`` (PDF parseado)."""

    desc: str
    value: str
    section: str = AVULSO
    msisdn: str = LINE_A


@dataclass(frozen=True)
class Case:
    """Um contraexemplo.

    ``expected`` são os ``canonical_name`` que o matcher PODE devolver como
    canceláveis. Tupla vazia = **falha fechada** (o desfecho seguro por default)."""

    case_id: str
    category: str
    var_desc: str
    var_value: str
    invoice: tuple[Entry, ...]
    expected: tuple[str, ...]
    why: str
    # Linha esperada do item casado, quando o caso testa roteamento de msisdn.
    expected_msisdn: str | None = None


# ----- builders ---------------------------------------------------------------


def _variation(desc: str, value: str) -> dict:
    """``invoiceVariation`` de uma cobrança que ENTROU 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 o avulso."""
    item = {
        "type": "servicos_contratados_de_parceiros",
        "desc": "Serviços de valor adicionado",
        "nItems": "1",
        "items": [{"desc": desc, "value": value, "invoice": CURRENT}],
    }
    current = {
        "type": "servicos_contratados_de_parceiros",
        "desc": "Serviços de valor adicionado",
        "items": [{"desc": desc, "value": value, "contestable": True}],
    }
    return {"invoiceVariation": [item], "currentInvoice": [current]}


def _detail(entries: tuple[Entry, ...]) -> dict:
    """``invoice_detail`` no formato ``{msisdn: {seção: [entries]}}``."""
    buckets: dict[str, dict[str, list]] = {}
    for entry in entries:
        section = buckets.setdefault(entry.msisdn, {}).setdefault(entry.section, [])
        section.append({"desc": entry.desc, "value": entry.value})
    return buckets


def _run(case: Case):
    """Executa o matcher REAL de produção sobre o caso."""
    return varied_avulso_items(
        invoice_variation=_variation(case.var_desc, case.var_value),
        invoice_detail=_detail(case.invoice),
        resolver=InvoiceResolver(),
        by_charge=False,
    )


# ----- A. colisões semânticas (15) --------------------------------------------
# Produtos DIFERENTES com nomes quase idênticos e MESMO valor. Similaridade alta
# sozinha nunca pode autorizar cancelamento.

_A: tuple[Case, ...] = (
    Case(
        "A01-fluid-light-x-premium", "colisao", "Fluid Light", "12.90",
        (Entry("Fluid Premium", "12.90"),), (),
        "Light × Premium: mesma marca, produto e preço diferentes de propósito",
    ),
    Case(
        "A02-fluid-premium-x-light", "colisao", "Fluid Premium", "12.90",
        (Entry("Fluid Light", "12.90"),), (),
        "inverso de A01 — a direção não pode salvar o casamento",
    ),
    Case(
        "A03-aya-books-x-audiobooks", "colisao", "Aya Books Premium", "9.99",
        (Entry("Aya Audiobooks Premium", "9.99"),), (),
        "um token difere e ele CONTÉM o outro (books ⊂ audiobooks)",
    ),
    Case(
        "A04-aya-ensinah-x-books", "colisao", "Aya Ensinah Premium", "9.99",
        (Entry("Aya Books Premium", "9.99"),), (),
        "2 de 3 tokens iguais, mesmo preço",
    ),
    Case(
        "A05-exa-seguranca-x-cloud", "colisao", "EXA Segurança Premium", "19.99",
        (Entry("EXA Cloud 500GB", "19.99"),), (),
        "mesma marca EXA, serviços sem relação",
    ),
    Case(
        "A06-bancah-jornais-x-premium", "colisao", "Bancah Jornais II", "7.99",
        (Entry("Bancah Premium + Jornais", "7.99"),), (),
        "mesmos tokens em ORDEM diferente — bag-of-words casaria",
    ),
    Case(
        "A07-itgame-light-x-premium", "colisao", "ITGame Light", "14.99",
        (Entry("ITGame Premium", "14.99"),), (),
        "Light × Premium na mesma marca",
    ),
    Case(
        "A08-aluguel-filme-1-x-2", "colisao", "Aluguel de Filme 1", "19.90",
        (Entry("Aluguel de Filme 2", "19.90"),), (),
        "difere só no dígito final — Jaro-Winkler dá score altíssimo",
    ),
    Case(
        "A09-tim-games-mensal-x-semanal", "colisao", "TIM Games Mensal", "16.99",
        (Entry("TIM Games Semanal", "16.99"),), (),
        "Mensal × Semanal: periodicidade diferente é PRODUTO diferente",
    ),
    Case(
        "A10-kids-plus-x-premium", "colisao", "Kids Plus", "11.99",
        (Entry("Kids Premium", "11.99"),), (),
        "Plus × Premium",
    ),
    Case(
        "A11-exa-premium-x-protecao", "colisao", "EXA Segurança Premium", "19.99",
        (Entry("EXA Segurança + Proteção Stand", "19.99"),), (),
        "2 tokens iniciais idênticos, cauda diferente",
    ),
    Case(
        "A12-tim-games-prefixo", "colisao", "TIM Games", "16.99",
        (Entry("TIM Games Premium", "16.99"),), (),
        "PREFIXO DE TOKENS entre produtos DIFERENTES — indistinguível de FIT ME "
        "pela forma; só o valor separaria, e aqui ele coincide",
    ),
    Case(
        "A13-fit-app-x-fit-me-app", "colisao", "Fit App", "16.99",
        (Entry("Fit Me App", "16.99"),), (),
        "token inserido no MEIO — não é prefixo, mas a grafia é quase idêntica",
    ),
    Case(
        "A14-tim-music-plus-x-premium", "colisao", "Tim Music Plus", "9.90",
        (Entry("Tim Music Premium", "9.90"),), (),
        "difere só no último token",
    ),
    Case(
        "A15-kids-prefixo", "colisao", "Kids", "11.99",
        (Entry("Kids Premium", "11.99"),), (),
        "segundo prefixo perigoso: nome curto genérico casaria com qualquer coisa",
    ),
)


# ----- B. diferença mínima de valor (8) ---------------------------------------
# Nome idêntico (ou prefixo perfeito), candidato único, similaridade 1.0 — e o
# valor difere. Um centavo já basta para bloquear.

_B: tuple[Case, ...] = (
    Case(
        "B01-fitme-16.99-x-17.00", "valor", "Fit Me App", "16.99",
        (Entry("Fit Me App", "17.00"),), (),
        "nome idêntico, 1 centavo acima",
    ),
    Case(
        "B02-fitme-16.99-x-16.98", "valor", "Fit Me App", "16.99",
        (Entry("Fit Me App", "16.98"),), (),
        "nome idêntico, 1 centavo abaixo",
    ),
    Case(
        "B03-fluid-14.99-x-15.00", "valor", "Fluid Premium", "14.99",
        (Entry("Fluid Premium", "15.00"),), (),
        "cruza a fronteira de unidade",
    ),
    Case(
        "B04-exa-9.99-x-9.98", "valor", "EXA Cloud 500GB", "9.99",
        (Entry("EXA Cloud 500GB", "9.98"),), (),
        "1 centavo em valor baixo",
    ),
    Case(
        "B05-fitme-prefixo-valor-errado", "valor", "FIT ME App", "16.99",
        (Entry("FIT ME App Premium Mensal", "17.00"),), (),
        "OBRIGATÓRIO §5: o caso real, com o valor divergente — prefixo válido não "
        "compensa centavo errado",
    ),
    Case(
        "B06-aya-24.90-x-24.91", "valor", "Aya Books Premium", "24.90",
        (Entry("Aya Books Premium", "24.91"),), (),
        "1 centavo com nome longo idêntico",
    ),
    Case(
        "B07-itgame-12.90-x-12.09", "valor", "ITGame Light", "12.90",
        (Entry("ITGame Light", "12.09"),), (),
        "dígitos transpostos — erro de parser plausível",
    ),
    Case(
        "B08-fitme-16.99-x-169.90", "valor", "Fit Me App", "16.99",
        (Entry("Fit Me App", "169.90"),), (),
        "vírgula deslocada — 10× o valor",
    ),
)


# ----- C. estratégico disfarçado de avulso (7) --------------------------------
# A classificação da FATURA vence a da API. Nome e valor idênticos, candidato
# único, similaridade máxima — e mesmo assim não é cancelável.

_C: tuple[Case, ...] = (
    Case(
        "C01-hbo-secao-terceiros", "estrategico", "HBO Max Standard", "29.90",
        (Entry("HBO Max Standard", "29.90", section=ESTRATEGICO),), (),
        "seção de terceiros → item_type=estrategico",
    ),
    Case(
        "C02-netflix-em-secao-avulsa", "estrategico", "Netflix Premium", "39.90",
        (Entry("Netflix Premium", "39.90", section=AVULSO),), (),
        "guard 1: nome na lista fixa §12.A vence a seção avulsa",
    ),
    Case(
        "C03-youtube-mis-bucket", "estrategico", "YouTube Premium Mensal", "24.90",
        (Entry("YouTube Premium Mensal", "24.90", section=AVULSO),), (),
        "caso REAL documentado: a API bucketa streaming como avulso",
    ),
    Case(
        "C04-deezer-em-secao-avulsa", "estrategico", "Deezer Premium", "19.90",
        (Entry("Deezer Premium", "19.90", section=AVULSO),), (),
        "guard 1 sobre marca da lista fixa",
    ),
    Case(
        "C05-apple-music-em-secao-avulsa", "estrategico", "Apple Music SVA Mes", "16.90",
        (Entry("Apple Music SVA Mes", "16.90", section=AVULSO),), (),
        "marca de duas palavras na lista fixa",
    ),
    Case(
        "C06-bundle-incluso", "estrategico", "Tamboro Mensal", "19.99",
        (Entry("Tamboro Mensal", "19.99", section=BUNDLE),), (),
        "bundle incluso no plano: não se cancela item de bundle",
    ),
    Case(
        "C07-fitme-classificado-estrategico", "estrategico", "FIT ME App", "16.99",
        (Entry("FIT ME App Premium Mensal", "16.99", section=ESTRATEGICO),), (),
        "OBRIGATÓRIO §5: o caso real, mas a fatura o classifica estratégico",
    ),
)


# ----- D. ambiguidade extrema (8) ---------------------------------------------
# 2+ candidatos plausíveis. Nunca escolher o de maior score: falha FECHADA.

_D: tuple[Case, ...] = (
    Case(
        "D01-fitme-dois-candidatos", "ambiguidade", "FIT ME App", "16.99",
        (
            Entry("FIT ME App Premium", "16.99"),
            Entry("FIT ME App Premium Mensal", "16.99"),
        ),
        (),
        "OBRIGATÓRIO §5: dois prefixos válidos, mesmo valor — cancelar o errado "
        "custa o serviço de um cliente real",
    ),
    Case(
        "D02-fitme-tres-candidatos", "ambiguidade", "FIT ME App", "16.99",
        (
            Entry("FIT ME App Premium", "16.99"),
            Entry("FIT ME App Premium Mensal", "16.99"),
            Entry("FIT ME App Premium Anual", "16.99"),
        ),
        (),
        "três candidatos: mais opções não tornam a escolha mais segura",
    ),
    Case(
        "D03-aya-tres-marcas", "ambiguidade", "Aya", "9.99",
        (
            Entry("Aya Books Premium", "9.99"),
            Entry("Aya Audiobooks Premium", "9.99"),
            Entry("Aya Ensinah Premium", "9.99"),
        ),
        (),
        "prefixo de marca casa a família inteira",
    ),
    Case(
        "D04-mesmo-nome-duas-linhas", "ambiguidade", "Fit Me App", "16.99",
        (
            Entry("Fit Me App", "16.99", msisdn=LINE_A),
            Entry("Fit Me App", "16.99", msisdn=LINE_B),
        ),
        (),
        "nome e valor idênticos em DUAS linhas: cancelar na linha errada é dano real",
    ),
    Case(
        "D05-duplicata-mesma-linha", "ambiguidade", "Fit Me App", "16.99",
        (
            Entry("Fit Me App", "16.99"),
            Entry("Fit Me App", "16.99"),
        ),
        ("Fit Me App",),
        "cobrança duplicada na MESMA linha e seção: o dedupe do resolver colapsa "
        "em uma, e uma variação consome exatamente uma",
    ),
    Case(
        "D06-fluid-familia", "ambiguidade", "Fluid", "12.90",
        (
            Entry("Fluid Light", "12.90"),
            Entry("Fluid Premium", "12.90"),
        ),
        (),
        "marca sozinha não identifica o produto",
    ),
    Case(
        "D07-exa-familia", "ambiguidade", "EXA", "19.99",
        (
            Entry("EXA Cloud 500GB", "19.99"),
            Entry("EXA Segurança Premium", "19.99"),
        ),
        (),
        "dois produtos sem relação sob a mesma marca",
    ),
    Case(
        "D08-bancah-familia", "ambiguidade", "Bancah", "7.99",
        (
            Entry("Bancah Jornais II", "7.99"),
            Entry("Bancah Premium + Jornais", "7.99"),
        ),
        (),
        "scores praticamente empatados",
    ),
)


# ----- E. prefixo / substring perigosos (6) -----------------------------------
# Só a direção comprovada é permitida: variação CURTA → fatura QUALIFICADA.

_E: tuple[Case, ...] = (
    Case(
        "E01-prefixo-invertido", "prefixo", "Fit Me App Premium", "16.99",
        (Entry("Fit Me App", "16.99"),), (),
        "variação MAIS específica que a fatura: direção proibida",
    ),
    Case(
        "E02-substring-no-meio", "prefixo", "Me App", "16.99",
        (Entry("Fit Me App Premium Mensal", "16.99"),), (),
        "casa no meio do nome — substring solta cancelaria",
    ),
    Case(
        "E03-sufixo-games-premium", "prefixo", "Games Premium", "16.99",
        (Entry("TIM Games Premium", "16.99"),), (),
        "sufixo, não prefixo",
    ),
    Case(
        "E04-token-parcial-tube", "prefixo", "Tube Premium", "24.90",
        (Entry("YouTube Premium", "24.90"),), (),
        "pedaço de token: 'Tube' ⊂ 'YouTube' sem ser token",
    ),
    Case(
        "E05-token-solto-app", "prefixo", "App", "16.99",
        (Entry("Fit Me App", "16.99"),), (),
        "token genérico no fim casaria meia fatura",
    ),
    Case(
        "E06-fitme-fail-closed-por-padrao", "prefixo", "FIT ME App", "16.99",
        (Entry("FIT ME App Premium Mensal", "16.99"),), (),
        "OBRIGATÓRIO §5: o caso real do incidente e081812886a. Com "
        "TIM_VAS_QUALIFIED_NAME_MATCH desligada (padrão) ele NÃO casa — é o custo "
        "aceito de eliminar FP-1: 'FIT ME App'→'FIT ME App Premium Mensal' e "
        "'TIM Games'→'TIM Games Premium' têm a MESMA forma, e nenhum sinal "
        "disponível os separa. Ver test_flag_liga_a_segunda_passada_e_so_isso.",
    ),
)


# ----- F. invariantes estruturais (6) -----------------------------------------

_F: tuple[Case, ...] = (
    Case(
        "F01-msisdn-ausente", "estrutural", "Fit Me App", "16.99",
        (Entry("Fit Me App", "16.99", msisdn=""),), (),
        "sem linha não há como executar o cancelamento: falha fechada",
    ),
    Case(
        "F02-item-na-linha-secundaria", "estrutural", "Fit Me App", "16.99",
        (
            Entry("Fluid Light", "12.90", msisdn=LINE_A),
            Entry("Fit Me App", "16.99", msisdn=LINE_B),
        ),
        ("Fit Me App",),
        "casa na linha CERTA (B), não na primeira do payload",
        LINE_B,
    ),
    Case(
        "F03-secao-sem-classe", "estrutural", "Fit Me App", "16.99",
        (Entry("Fit Me App", "16.99", section=SEM_CLASSE),), (),
        "seção fora de SECTION_DEFAULTS → item existe mas não é acionável",
    ),
    Case(
        "F04-desc-vazia", "estrutural", "Fit Me App", "16.99",
        (Entry("", "16.99"),), (),
        "item sem nome não tem identidade: charge_match_key devolve None",
    ),
    Case(
        "F05-valor-nao-parseavel", "estrutural", "Fit Me App", "16.99",
        (Entry("Fit Me App", "indisponível"),), (),
        "valor ilegível no PDF não pode virar cancelamento",
    ),
    Case(
        "F06-fatura-sem-o-item", "estrutural", "Fit Me App", "16.99",
        (Entry("Fluid Light", "12.90"), Entry("EXA Cloud 500GB", "9.99")), (),
        "a API diz que variou, a fatura não tem: falha fechada (SPEC §9)",
    ),
)


ALL_CASES: tuple[Case, ...] = _A + _B + _C + _D + _E + _F

assert len(ALL_CASES) == 50, f"a suíte exige exatamente 50 casos, tem {len(ALL_CASES)}"


# ----- falsos positivos conhecidos -------------------------------------------
# VAZIO desde o fix dos 4 FPs que esta suíte encontrou (FP-1/FP-1' prefixo de
# tokens, FP-2 ambiguidade de linha, FP-3 msisdn ausente). Mantido como ponto de
# extensão: um caso novo que o matcher viole entra aqui com `strict=True`, e o
# XPASS quebra a suíte no dia em que for corrigido — a lista nunca envelhece em
# silêncio. Enquanto estiver vazia, TODOS os 50 casos passam de verdade.
KNOWN_UNSAFE: dict[str, str] = {}


def _cases_param():
    """Parametrização materializada; compatível com pytest 9/10."""
    rows = []
    for case in ALL_CASES:
        reason = KNOWN_UNSAFE.get(case.case_id)
        marks = [pytest.mark.xfail(reason=reason, strict=True)] if reason else []
        rows.append(pytest.param(case, id=case.case_id, marks=marks))
    return rows


def _shadow_rows() -> list[tuple[Case, float, bool]]:
    """(caso, score de similaridade, deve_casar) para os 50 — sem tocar produção."""
    matcher = SimilarityItemMatcher()
    rows = []
    for case in ALL_CASES:
        candidates = [e.desc for e in case.invoice if e.desc]
        score = matcher.best_similarity(case.var_desc, candidates) if candidates else 0.0
        rows.append((case, score, bool(case.expected)))
    return rows


# ----- testes -----------------------------------------------------------------


def test_suite_tem_exatamente_50_casos_com_a_distribuicao_exigida():
    """A distribuição é parte do contrato da suíte, não detalhe."""
    from collections import Counter

    assert len(ALL_CASES) == 50
    assert Counter(c.category for c in ALL_CASES) == {
        "colisao": 15,
        "valor": 8,
        "estrategico": 7,
        "ambiguidade": 8,
        "prefixo": 6,
        "estrutural": 6,
    }
    assert len({c.case_id for c in ALL_CASES}) == 50, "case_id duplicado"


@pytest.mark.parametrize("case", _cases_param())
def test_matcher_determinístico_nao_produz_falso_positivo(case: Case):
    """O matcher de produção devolve exatamente o conjunto seguro.

    Tupla vazia = falha fechada. Qualquer item a mais é um FALSO POSITIVO, e nesta
    rota falso positivo é cancelamento executado sem confirmação."""
    items = _run(case)

    resolved = [] if items is None else [it.canonical_name for it in items]
    assert resolved == list(case.expected), (
        f"{case.case_id} ({case.category}): esperado {list(case.expected)}, "
        f"veio {resolved} — {case.why}"
    )

    if case.expected_msisdn is not None and items:
        assert [it.msisdn for it in items] == [case.expected_msisdn], (
            f"{case.case_id}: cancelamento roteado para a linha errada"
        )


@pytest.mark.parametrize(
    "case",
    [
        pytest.param(
            c,
            id=c.case_id,
            marks=(
                [pytest.mark.xfail(reason=KNOWN_UNSAFE[c.case_id], strict=True)]
                if c.case_id in KNOWN_UNSAFE
                else []
            ),
        )
        for c in ALL_CASES
    ],
)
def test_nenhum_item_devolvido_e_estrategico_ou_bundle(case: Case):
    """Invariante transversal: o que sai do matcher é sempre cancelável.

    Independe da categoria — nenhum caminho pode devolver estratégico/bundle,
    nem item sem linha de destino."""
    items = _run(case) or []
    assert all(it.item_type == "avulso" for it in items), (
        f"{case.case_id}: item não-avulso na fila de cancelamento"
    )
    assert all(it.msisdn for it in items), (
        f"{case.case_id}: item sem msisdn na fila de cancelamento"
    )


def test_valores_sao_decimal_nunca_float():
    """``float`` em dinheiro reintroduz o erro de centavo que a suíte B testa."""
    items = _run(_F[1]) or []  # F02: casa na linha secundária
    assert items and all(isinstance(it.value, Decimal) for it in items)


def test_flag_liga_a_segunda_passada_e_so_isso(monkeypatch):
    """``TIM_VAS_QUALIFIED_NAME_MATCH`` governa exatamente o prefixo de tokens.

    Ligada, o FIT ME volta a casar — e os falsos positivos da MESMA forma
    (``TIM Games``→``TIM Games Premium``) voltam junto. É essa inseparabilidade
    que justifica o default desligado: a flag não escolhe "casar só o certo",
    escolhe aceitar ou recusar a classe inteira."""
    from app.domain.contas import vas_variation

    fitme = next(c for c in ALL_CASES if c.case_id.startswith("E06"))
    tim_games = next(c for c in ALL_CASES if c.case_id.startswith("A12"))

    assert _run(fitme) == []  # default: fail closed
    assert _run(tim_games) == []

    monkeypatch.setattr(vas_variation, "_VAS_QUALIFIED_NAME_MATCH", True)
    assert [it.canonical_name for it in _run(fitme) or []] == [
        "FIT ME App Premium Mensal"
    ]
    # o falso positivo volta junto — a prova de que não são separáveis
    assert [it.canonical_name for it in _run(tim_games) or []] == ["TIM Games Premium"]


def test_flag_ligada_nao_reabre_msisdn_nem_ambiguidade(monkeypatch):
    """A flag reabre SÓ o prefixo de tokens: os guards de msisdn e de ambiguidade
    de linha valem nos dois modos."""
    from app.domain.contas import vas_variation

    monkeypatch.setattr(vas_variation, "_VAS_QUALIFIED_NAME_MATCH", True)
    for prefix in ("F01", "D04", "D01"):
        case = next(c for c in ALL_CASES if c.case_id.startswith(prefix))
        assert _run(case) == [], f"{case.case_id} reaberto com a flag ligada"


# ----- shadow: o que uma camada de similaridade veria -------------------------


def test_shadow_similaridade_tem_overlap_irremovivel():
    """Positivos e negativos se SOBREPÕEM no eixo de similaridade.

    Achado central: o menor score de um caso que DEVE casar fica ABAIXO do maior
    score de um caso que NÃO pode casar. Nenhum corte único separa os dois lados —
    a similaridade não é o discriminante deste fluxo."""
    rows = _shadow_rows()
    min_pos = min(s for _, s, safe in rows if safe)
    max_neg = max(s for _, s, safe in rows if not safe)

    assert max_neg >= min_pos, (
        "sem overlap — se isto falhar, a suíte perdeu seus contraexemplos duros "
        "e precisa de casos novos antes de servir como evidência"
    )
    # Ancorado no medido: negativos chegam a 1.00 — nome IDÊNTICO com valor
    # diferente (bloco B) e estratégico mis-bucketado (bloco C) pontuam o máximo
    # e mesmo assim não podem ser cancelados. Nenhum corte superior a esse existe,
    # então nenhum threshold separa por cima.
    assert max_neg >= 1.0
    assert max_neg >= min_pos


@pytest.mark.parametrize("threshold", (0.85, 0.90, 0.95, 0.97, 0.99))
def test_nenhum_threshold_de_similaridade_zera_falso_positivo(threshold: float):
    """Para TODO corte avaliado sobra pelo menos um falso positivo.

    A similaridade sozinha jamais autoriza cancelamento: o que separa os casos é
    o **valor ao centavo** e a **classe vinda da fatura** — nomes idênticos com
    valores diferentes (bloco B) e estratégicos mis-bucketados (bloco C) pontuam
    1.0 e mesmo assim não podem ser cancelados."""
    rows = _shadow_rows()
    false_positives = [c.case_id for c, s, safe in rows if not safe and s >= threshold]

    assert false_positives, (
        f"threshold {threshold} zerou os FP — revisar: o veredito "
        "NOT_SAFE_FOR_AUTO_EXECUTION depende deste contraexemplo"
    )


@pytest.mark.parametrize(
    "case",
    [pytest.param(c, id=c.case_id) for c in ALL_CASES if c.category == "prefixo"],
)
def test_guards_de_prefixo_valem_com_a_flag_ligada(case: Case, monkeypatch):
    """Os guards do prefixo de tokens precisam valer QUANDO ele roda.

    Com a flag no default (off) o bloco E passaria por vacuidade — nada casa de
    qualquer jeito, e uma regressão em ``_is_token_prefix`` (aceitar substring no
    meio, aceitar prefixo invertido) não seria detectada por ninguém. Aqui a
    segunda passada é ligada de propósito, então os guards são exercitados."""
    from app.domain.contas import vas_variation

    monkeypatch.setattr(vas_variation, "_VAS_QUALIFIED_NAME_MATCH", True)
    items = _run(case) or []
    resolved = [it.canonical_name for it in items]

    # E06 é o FIT ME: com a flag ligada ele CASA (é o comportamento da passada).
    esperado = ["FIT ME App Premium Mensal"] if case.case_id.startswith("E06") else []
    assert resolved == esperado, f"{case.case_id}: {case.why}"


@pytest.mark.parametrize("case", [pytest.param(c, id=c.case_id) for c in ALL_CASES])
def test_explicacao_sem_data_nao_altera_nenhum_dos_50(case: Case):
    """Prova de propriedade: com os detalhes SEM ``period``, uma explicação
    sintética não pode mudar nenhum dos 50 desfechos.

    O desempate por data só age quando AMBOS os lados têm data legível. Como os
    casos desta suíte não carregam `period`, o resultado tem de ser idêntico ao
    run sem explicação — é o que prova que **ausência de data não autoriza
    escolha por eliminação**."""
    sintetica = (
        "Houve a variação dos seguintes serviços de valor adicionado:\n"
        f"* Cobrança {case.var_desc} no valor de R$ {case.var_value} no dia 01/01/26."
    )

    sem = _run(case) or []
    com = (
        varied_avulso_items(
            invoice_variation=_variation(case.var_desc, case.var_value),
            invoice_detail=_detail(case.invoice),
            resolver=InvoiceResolver(),
            by_charge=False,
            explanation_text=sintetica,
        )
        or []
    )

    assert [it.canonical_name for it in sem] == [it.canonical_name for it in com], (
        f"{case.case_id}: a explicação mudou o desfecho sem haver data no detalhe"
    )
