"""Testes do parser de actos do registo comercial, contra actos reais.

As fixtures em `fixtures/registry/` foram recolhidas do IRN em julho de 2026 e
cobrem os layouts que existem de facto. São elas que sustentam as afirmações do
módulo: o NIF indentado, as duas variantes de secção de quotas, os dois formatos
de montante e a dupla titular+gerente na mesma constituição.

Correr:  cd backend && python -m pytest tests/ -q
"""
import json
import pathlib

import pytest

from app.services.registry_act_parser import (
    classify_act,
    entity_key,
    entity_kind,
    is_own_quota,
    normalize_address,
    parse_act,
    parse_money,
    wants_content,
)

FIXTURES = pathlib.Path(__file__).parent / "fixtures" / "registry"
INDEX = json.loads((FIXTURES / "index.json").read_text(encoding="utf-8"))


def load(name: str) -> tuple[str, str]:
    return (FIXTURES / f"{name}.txt").read_text(encoding="utf-8"), INDEX[name]["act"]


# ------------------------------------------------------------------- montantes

@pytest.mark.parametrize("raw,expected,currency", [
    ("26.289,60 Euros", 26289.60, "EUR"),
    ("1.000,00 Euros", 1000.00, "EUR"),
    ("107.420,80 Euros", 107420.80, "EUR"),
    # ponto decimal sem separador de milhares — a regra pt-PT daria 1 525 000
    ("15250.00 Euros", 15250.00, "EUR"),
    ("480.000,00 Euros", 480000.00, "EUR"),
    ("5.000.000,00 Escudos", 5000000.00, "PTE"),
    ("1.000", 1000.00, None),
    (None, None, None),
    ("", None, None),
])
def test_parse_money(raw, expected, currency):
    value, cur = parse_money(raw)
    assert value == expected
    assert cur == currency


# ----------------------------------------------------------------- entidades

def test_entity_kind_por_primeiro_digito():
    assert entity_kind("215165195") == "person"      # 2 -> singular
    assert entity_kind("510248900") == "company"     # 5 -> colectiva
    assert entity_kind("980785758") == "foreign"     # 98 -> não residente
    # sem NIF (publicações de 2006 em prosa) e NIF com mod-11 errado são casos
    # diferentes, e nenhum deles propaga no grafo
    assert entity_kind(None) == "unknown"
    assert entity_kind("510248901") == "invalid"


def test_quotas_amortizadas_nao_sao_titulares():
    """"AMORTIZADAS" aparece onde deveria estar um titular: é o estado da quota,
    que pertence à sociedade. Contava para o total mas não pode gerar um sócio."""
    assert is_own_quota("AMORTIZADAS") is True
    assert is_own_quota("Quotas próprias") is True
    assert is_own_quota("a sociedade") is True
    assert is_own_quota("CRESCENTIMOB - IMOBILIÁRIA, LDA") is False
    assert is_own_quota(None) is False


def test_entity_key_usa_nome_quando_nao_ha_nif():
    assert entity_key("510248900", "Qualquer") == "nif:510248900"
    assert entity_key(None, "TOWER VALLEY, SOCIEDAD LIMITADA") == "name:tower valley sociedad limitada"
    assert entity_key(None, "") is None


def test_normalize_address_junta_rua_e_codigo_postal():
    a = normalize_address("Rua Monsenhor Ferreira nº 228 Esquerdo", "4710-407")
    b = normalize_address("R. Monsenhor Ferreira n.º 228 Esq.", "4710-407")
    assert a == b, f"{a!r} != {b!r}"
    assert normalize_address(None, None) is None


# ---------------------------------------------------------------- classificação

@pytest.mark.parametrize("title,expected", [
    ("Constituição de sociedade, designação de membro(s) de órgão(s) social(ais)", "constituicao"),
    ("Alterações ao contrato de sociedade(online)", "alteracao_contrato"),
    ("Aumento do capital(online)", "aumento_capital"),
    ("Redução do capital e Alterações ao contrato de sociedade", "reducao_capital"),
    ("Designação de membro(s) de orgão(s) social(ais)", "designacao"),
    ("Cessação de funções de membro(s) do(s) orgão(s) social(ais)", "cessacao"),
    ("Dissolução e encerramento da liquidação", "dissolucao"),
    ("PRESTAÇÃO DE CONTAS INDIVIDUAL", "prestacao_contas"),
    ("Convocatória", "outro"),
])
def test_classify_act(title, expected):
    assert classify_act(title) == expected


def test_prestacao_de_contas_nao_vale_abrir():
    assert wants_content("PRESTAÇÃO DE CONTAS INDIVIDUAL") is False
    assert wants_content("Aumento do capital(online)") is True


# ------------------------------------------------------- cap table de 4 titulares

def test_aumento_capital_extrai_os_quatro_titulares():
    body, act = load("aumento_capital_4_titulares")
    r = parse_act(body, act)

    assert r["act_class"] == "aumento_capital"
    assert r["counts"]["quotas"] == 4
    nifs = [q["nif"] for q in r["quotas"]]
    assert nifs == ["215165195", "177483776", "510248900", "980785758"]
    assert [q["quota_amount"] for q in r["quotas"]] == [26289.60, 26289.60, 107420.80, 320000.00]
    assert r["quota_total"] == 480000.00
    # o capital resultante do acto bate com a soma das quotas
    assert r["capital"] == 480000.00
    assert r["capital_is_resulting"] is True


def test_titular_empresa_e_reconhecido_como_empresa():
    body, act = load("aumento_capital_4_titulares")
    r = parse_act(body, act)
    crescentimob = next(q for q in r["quotas"] if q["nif"] == "510248900")
    assert crescentimob["kind"] == "company"
    assert crescentimob["name"] == "CRESCENTIMOB - IMOBILIÁRIA, LDA"
    # a vírgula da firma não é a vírgula da nacionalidade
    assert "Nacionalidade" not in crescentimob["name"]


def test_sufixo_de_nacionalidade_sai_do_nome_da_pessoa():
    body, act = load("aumento_capital_4_titulares")
    r = parse_act(body, act)
    fernando = next(q for q in r["quotas"] if q["nif"] == "215165195")
    # a fonte escreve ", de Nacionalidade Portugusa" — com a gralha
    assert fernando["name"] == "FERNANDO JORGE BARBOSA DA COSTA"
    assert fernando["nacionalidade"] is None or "Portug" in fernando["nacionalidade"]


def test_conjuge_e_morada_ficam_disponiveis_para_resolver_identidade():
    body, act = load("aumento_capital_4_titulares")
    r = parse_act(body, act)
    fernando = next(q for q in r["quotas"] if q["nif"] == "215165195")
    assert fernando["estado_civil"].startswith("Casado")
    assert fernando["conjuge"] == "Andreia Filipa Batista Balsemão Barbosa"
    assert fernando["regime_bens"] == "Comunhão de adquiridos"
    assert fernando["codigo_postal"] == "4710-407"
    assert fernando["address_norm"] and "monsenhor ferreira" in fernando["address_norm"]
    # o cônjuge herda a morada do titular, e é isso que dá confiança à sugestão
    assert "4710-407" in fernando["address_norm"]


def test_titular_seguinte_nao_perde_o_seu_nif():
    """O bloco de cada titular acaba no próximo QUOTA. Sem esse limite, um
    titular sem NIF apanharia o NIF do titular a seguir — e uma participação
    ficaria atribuída à pessoa errada."""
    body, act = load("aumento_capital_4_titulares")
    r = parse_act(body, act)
    tower = next(q for q in r["quotas"] if "TOWER VALLEY" in (q["name"] or ""))
    assert tower["nif"] == "980785758"
    assert tower["quota_amount"] == 320000.00
    assert len({q["nif"] for q in r["quotas"]}) == 4


# ------------------------------------------------- segunda variante da secção

def test_reducao_capital_usa_quota_s_e_titular_es():
    """A redução de capital chama à secção `QUOTA(S) E TITULAR(ES):` em vez de
    `SÓCIOS E QUOTAS:`. Reconhecer só a primeira perderia estes actos."""
    body, act = load("reducao_capital_quotas_titulares")
    r = parse_act(body, act)
    assert r["act_class"] == "reducao_capital"
    assert r["counts"]["quotas"] == 2
    assert [q["quota_amount"] for q in r["quotas"]] == [14000.00, 5750.00]
    assert r["quota_total"] == 19750.00
    assert r["capital"] == 19750.00 and r["capital_is_resulting"] is True
    hubip = next(q for q in r["quotas"] if q["nif"] == "514061944")
    assert hubip["kind"] == "company"
    assert hubip["pais"] == "Portugal"


def test_ordem_dos_campos_no_bloco_nao_importa():
    """Na redução o `Estado civil` vem antes da `Nacionalidade`; no aumento não
    há `Nacionalidade` nenhuma. Os campos lêem-se por etiqueta."""
    body, act = load("reducao_capital_quotas_titulares")
    r = parse_act(body, act)
    ana = next(q for q in r["quotas"] if q["nif"] == "167363816")
    assert ana["estado_civil"].startswith("Casado")
    assert ana["nacionalidade"] == "Portuguesa"
    assert ana["conjuge"] == "João Francisco Cordeiro de Oliveira Barros"


# ------------------------------------------------------- gerência e dupla função

def test_designacao_extrai_gerente_com_nif_indentado():
    """O caso que estava a falhar em produção: a linha `NIF/NIPC:` vem com um
    espaço à esquerda e o parser antigo exigia-a colada ao fim de linha."""
    body, act = load("designacao_gerencia")
    r = parse_act(body, act)
    assert r["act_class"] == "designacao"
    assert r["counts"]["people"] == 1
    p = r["officers"][0]
    assert p["nif"] == "128288604"
    assert p["name"] == "MARIA DA CONCEIÇÃO MENDES GUERREIRO"
    assert p["cargo"] == "gerente"
    assert p["organ"] and "GER" in p["organ"].upper()
    assert p["kind"] == "person"


def test_cessacao_traz_causa_e_data():
    body, act = load("cessacao_gerencia")
    r = parse_act(body, act)
    assert r["act_class"] == "cessacao"
    p = r["officers"][0]
    assert p["nif"] == "198394632"
    assert p["cargo"] == "gerente"
    assert p["causa"] == "Renúncia"
    assert p["data"] == "2026/05/15"


def test_constituicao_da_a_mesma_pessoa_como_titular_e_gerente():
    """A dualidade que o índice `(publication_id, person_nif)` do
    `publication_people` destruía: numa constituição o fundador é sócio e
    gerente, e são dois factos distintos."""
    body, act = load("constituicao_socio_gerente")
    r = parse_act(body, act)
    assert r["act_class"] == "constituicao"
    assert r["counts"]["quotas"] == 1 and r["counts"]["people"] == 1
    titular = r["quotas"][0]
    gerente = r["officers"][0]
    assert titular["nif"] == gerente["nif"] == "317448447"
    assert titular["quota_amount"] == 1000.00
    assert gerente["cargo"] == "sócio-gerente"
    # capital inicial, não resultante — não serve como capital actual
    assert r["capital"] == 1000.00 and r["capital_is_resulting"] is False
    assert r["quota_total"] == r["capital"]


def test_constituicao_preenche_o_cabecalho_da_entidade():
    body, act = load("constituicao_socio_gerente")
    r = parse_act(body, act)
    h = r["header"]
    assert h["nipc"] == "519522842"
    assert h["firma"] == "VÁRZEA MARAVILHA UNIPESSOAL LDA"
    assert h["concelho"] == "Odivelas"
    assert h["codigo_postal"] == "1685-457"
    assert h["objeto_social"] and "armações de ferro" in h["objeto_social"]


# ------------------------------------------------------------------ sem cap table

def test_alteracao_sem_titulares_nao_produz_fotografia_vazia():
    """Uma alteração ao contrato que só mudou a sede não tem cap table. Tem de
    devolver `quotas: 0`, e é `parse_quotas > 0` que decide se um acto pode
    substituir a estrutura societária — senão apagava os sócios da empresa."""
    body, act = load("alteracao_sem_captable")
    r = parse_act(body, act)
    assert r["act_class"] == "alteracao_contrato"
    assert r["counts"]["quotas"] == 0
    assert r["quotas"] == []
    assert r["quota_total"] is None
    assert r["captable_complete"] is False
    assert r["captable_linkable"] is False


# ------------------------------------------------------- completude e ligação

def test_captable_completa_quando_a_soma_bate_com_o_capital():
    for name in ("aumento_capital_4_titulares", "reducao_capital_quotas_titulares",
                 "constituicao_socio_gerente"):
        body, act = load(name)
        r = parse_act(body, act)
        assert r["captable_complete"] is True, name
        assert r["captable_linkable"] is True, name


def test_captable_parcial_nao_se_diz_completa():
    """Quando o acto lista só as quotas que mexeram, a soma não bate com o
    capital. Medido no corpus: acontece em 8 de 171. Apresentar uma parte como se
    fosse o todo é o erro a evitar."""
    body = (
        "UTC - AUMENTO DO CAPITAL\n"
        "Capital após o aumento : 300.000,00 Euros\n"
        "SÓCIOS E QUOTAS:\n"
        " QUOTA : 115.000,00 Euros \n"
        " TITULAR: MARISA FERNANDES VIEIRA PINHEIRO \n"
        " NIF/NIPC: 215165195\n"
    )
    r = parse_act(body, "Aumento do capital")
    assert r["quota_total"] == 115000.00
    assert r["capital"] == 300000.00
    assert r["captable_complete"] is False
    assert r["captable_linkable"] is True


def test_captable_de_2006_em_prosa_nao_e_ligavel():
    """As publicações importadas pela extensão em 2006 não têm blocos
    `NIF/NIPC:`. O nome e a quota valem para a ficha da empresa, mas um nó só com
    nome não se segue no grafo sem inventar uma identidade."""
    body = (
        "Insc. 9- 01/20061006 - AUMENTO DE CAPITAL\n"
        "SÓCIOS E QUOTAS:\n"
        " QUOTA : 600.000,00 euros. Titular: António Fernando Couto de Sousa.\n"
        " TITULAR: António Fernando Couto de Sousa.\n"
    )
    r = parse_act(body, "Aumento de capital")
    assert r["counts"]["quotas"] == 1
    assert r["quotas"][0]["nif"] is None
    assert r["quotas"][0]["kind"] == "unknown"
    assert r["captable_linkable"] is False


def test_carriage_return_nao_entra_nos_nomes_nem_nos_montantes():
    from app.services.mj_company_enrichment import html_to_text

    html = "<p>SÓCIOS E QUOTAS:</p><p> QUOTA : 2.280,00 Euros \r</p><p> TITULAR: MANUEL MARTINS GUERREIRO \r</p><p> NIF/NIPC: 215165195</p>"
    r = parse_act(html_to_text(html), "Aumento do capital")
    assert r["counts"]["quotas"] == 1
    q = r["quotas"][0]
    assert q["name"] == "MANUEL MARTINS GUERREIRO"
    assert "\r" not in (q["name"] or "")
    assert q["quota_amount"] == 2280.00


def test_dissolucao_apanha_o_depositario():
    body, act = load("dissolucao_depositario")
    r = parse_act(body, act)
    assert r["act_class"] == "dissolucao"
    dep = next(p for p in r["officers"] if p["cargo"] == "depositário")
    assert dep["nif"] == "204620392"
    assert dep["name"] == "BRUNO SILVESTRE GONÇALVES PERES"


def test_corpo_vazio_nao_estoura():
    for value in (None, "", "   \n  "):
        r = parse_act(value)
        assert r["counts"] == {"quotas": 0, "people": 0, "orphans": 0}
        assert r["quotas"] == [] and r["officers"] == []
        assert r["captable_eligible"] is False


# ------------------------------------------------------------- quotas órfãs

def test_quota_sem_titular_invalida_o_acto_como_fotografia():
    """O caso real do `510036899`: a fonte lista três quotas e só nomeia duas.
    O acto não pode substituir a estrutura conhecida, senão a empresa perdia o
    maior sócio — 213.684,21 em três titulares passava a 10.685,21 em dois."""
    body = (
        "UTC - ALTERAÇÕES AO CONTRATO DE SOCIEDADE(ONLINE)\n"
        "SÓCIOS E QUOTAS:\n"
        "\n"
        " QUOTA : 202.999,00 Euros \n"
        "\n"
        " QUOTA : 10.684,21 Euros \n"
        " \n"
        " TITULAR: GESTION ENERGETICA DE LA BIOMASA, SOCIEDAD LIMITADA \n"
        " NIF/NIPC: 980498643\n"
        " QUOTA : 1,00 \n"
        " \n"
        " TITULAR: PRIMITIVO MALAGA GONZALEZ \n"
        " NIF/NIPC: 277219035\n"
    )
    r = parse_act(body, "Alterações ao contrato de sociedade(online)")
    assert r["counts"]["quotas"] == 2
    assert r["counts"]["orphans"] == 1
    # a órfã entra no total: é uma quota a sério, só não tem titular nomeado
    assert r["quota_total"] == 213684.21
    assert r["captable_eligible"] is False
    assert r["captable_linkable"] is True


def test_captable_sem_orfas_e_elegivel():
    for name in ("aumento_capital_4_titulares", "reducao_capital_quotas_titulares",
                 "constituicao_socio_gerente"):
        body, act = load(name)
        r = parse_act(body, act)
        assert r["counts"]["orphans"] == 0, name
        assert r["captable_eligible"] is True, name


def test_todas_as_fixtures_parseiam_sem_excecao():
    for name in INDEX:
        body, act = load(name)
        r = parse_act(body, act)
        assert r["act_class"] != ""
        assert isinstance(r["counts"]["quotas"], int)
