"""Pruebas de los catálogos del SAT, el catálogo de conceptos y los datos fiscales
del emisor (módulo fin).

Cubren: lectura de los 8 catálogos y su filtrado, que no acepten escritura, el CRUD de
conceptos con la relación 1:1 contra c_ClaveProdServ, el aislamiento multi-tenant, el
upsert del emisor y el amarre de las partidas de factura al catálogo de conceptos.

Los RFC de las pruebas son dummies (XAXX010101000): nunca datos reales.
"""
from decimal import Decimal

import pytest
import sqlalchemy as sa
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import ValidationError

from api.v1.modules.crm.accounts import service as accounts_service
from api.v1.modules.crm.accounts.dto import AccountCreate, AccountUpdate
from api.v1.modules.fin.catalogs.models import CfdiUse, ProductService, TaxObject, TaxRegime, UnitOfMeasure
from api.v1.modules.fin.catalogs.routes import router as catalogs_router
from api.v1.modules.fin.catalogs.seed_data import CATALOGS, sync_catalogs
from api.v1.modules.fin.concepts import service as concepts_service
from api.v1.modules.fin.concepts.dto import ConceptCreate, ConceptUpdate
from api.v1.modules.fin.invoices import service as invoices_service
from api.v1.modules.fin.invoices.dto import (
    InvoiceCreate,
    InvoiceItemCreate,
    InvoiceItemResponse,
    InvoiceItemUpdate,
)
from api.v1.modules.fin.issuer import service as issuer_service
from api.v1.modules.fin.issuer.dto import IssuerSettingsInput
from api.v1.modules.fin.issuer.models import IssuerSettings
from core.database import get_core_db
from core.security import get_current_user

T, C = 1, 1
OTHER_TENANT, OTHER_COMPANY = 2, 2
RFC_DUMMY = "XAXX010101000"


@pytest.fixture()
def client(db):
    """App mínima con solo el router de catálogos: evita levantar auth y permisos."""
    app = FastAPI()
    app.include_router(catalogs_router, prefix="/fin")
    app.dependency_overrides[get_core_db] = lambda: db
    app.dependency_overrides[get_current_user] = lambda: {"sub": "tester", "tenant_id": T}
    return TestClient(app)


def _product_service(db, code: str = "78101600") -> ProductService:
    return db.query(ProductService).filter(ProductService.code == code).one()


def _concept_payload(db, code: str = "FLETE-MAR", ps_code: str = "78101600") -> ConceptCreate:
    return ConceptCreate(
        code=code,
        description="Flete marítimo internacional",
        product_service_id=_product_service(db, ps_code).id,
        unit_of_measure_id=db.query(UnitOfMeasure).filter(UnitOfMeasure.code == "E48").one().id,
        tax_object_id=db.query(TaxObject).filter(TaxObject.code == "02").one().id,
        unit_price=Decimal("1500.00"),
    )


# ---------- Catálogos del SAT: lectura ----------

CATALOG_EXPECTATIONS = [
    ("tax-regimes", 19, "601"),
    ("taxes", 3, "002"),
    ("payment-forms", 22, "03"),
    ("units-of-measure", 21, "H87"),
    ("products-services", 11, "78101500"),
    ("voucher-types", 5, "I"),
    ("payment-methods", 2, "PUE"),
    ("tax-objects", 4, "02"),
    ("cfdi-uses", 24, "G03"),
]


@pytest.mark.parametrize("path,expected_count,sample_code", CATALOG_EXPECTATIONS)
def test_catalog_endpoints_return_seeded_rows(client, path, expected_count, sample_code):
    res = client.get(f"/fin/catalogs/{path}")
    assert res.status_code == 200
    rows = res.json()
    assert len(rows) == expected_count
    assert sample_code in [r["code"] for r in rows]


def test_catalog_search_filters_by_code_or_description(client):
    by_code = client.get("/fin/catalogs/payment-forms", params={"search": "03"}).json()
    assert [r["code"] for r in by_code] == ["03"]

    by_description = client.get("/fin/catalogs/payment-forms", params={"search": "transferencia"}).json()
    assert [r["code"] for r in by_description] == ["03"]

    prodserv = client.get("/fin/catalogs/products-services", params={"search": "marítimo"}).json()
    assert [r["code"] for r in prodserv] == ["78101600"]


def test_tax_regimes_person_type_excludes_individual_only(client):
    moral = client.get("/fin/catalogs/tax-regimes", params={"person_type": "moral"}).json()
    codes = [r["code"] for r in moral]
    assert "601" in codes  # General de Ley Personas Morales
    assert "605" not in codes  # Sueldos y Salarios: solo persona física
    assert all(r["applies_to_legal_entity"] for r in moral)

    fisica = client.get("/fin/catalogs/tax-regimes", params={"person_type": "fisica"}).json()
    fisica_codes = [r["code"] for r in fisica]
    assert "605" in fisica_codes and "601" not in fisica_codes


def test_products_services_limit_caps_results(client):
    assert len(client.get("/fin/catalogs/products-services", params={"limit": 3}).json()) == 3
    assert client.get("/fin/catalogs/products-services", params={"limit": 500}).status_code == 422


def test_catalogs_are_read_only(client):
    """Los catálogos del SAT no exponen métodos de escritura."""
    for method, path in [
        ("post", "/fin/catalogs/payment-forms"),
        ("put", "/fin/catalogs/tax-regimes"),
        ("patch", "/fin/catalogs/units-of-measure"),
        ("delete", "/fin/catalogs/products-services"),
    ]:
        res = client.request(method.upper(), path, json={"code": "XX", "description": "Inventado"})
        assert res.status_code == 405, f"{method.upper()} {path} no debería aceptarse"


def _catalog_counts(db) -> dict[str, int]:
    return {
        table.name: db.execute(sa.select(sa.func.count()).select_from(table)).scalar()
        for table, _ in CATALOGS
    }


def test_sync_catalogs_is_idempotent(db):
    """Volver a correrla no duplica ni borra filas."""
    before = _catalog_counts(db)
    inserted = sync_catalogs(db.connection())  # el fixture ya sembró los catálogos
    db.commit()
    assert sum(inserted.values()) == 0
    assert _catalog_counts(db) == before


# ---------- Conceptos ----------

def test_concept_crud(db):
    created = concepts_service.create_concept(db, _concept_payload(db), T, C, "tester")
    assert created.code == "FLETE-MAR" and created.currency == "MXN" and created.is_active

    fetched = concepts_service.get_concept(db, created.id, T, C)
    assert fetched.product_service.code == "78101600"  # catálogo resuelto sin N+1

    updated = concepts_service.update_concept(
        db, created.id, ConceptUpdate(description="Flete marítimo FCL", is_active=False), T, C, "tester"
    )
    assert updated.description == "Flete marítimo FCL" and updated.is_active is False

    assert concepts_service.get_concepts(db, T, C, active_only=False) == [updated]
    assert concepts_service.get_concepts(db, T, C, active_only=True) == []

    concepts_service.delete_concept(db, created.id, T, C)
    assert concepts_service.get_concepts(db, T, C) == []
    with pytest.raises(HTTPException) as exc:
        concepts_service.get_concept(db, created.id, T, C)
    assert exc.value.status_code == 404


def test_duplicate_product_service_in_same_company_conflicts(db):
    concepts_service.create_concept(db, _concept_payload(db), T, C)
    with pytest.raises(HTTPException) as exc:
        concepts_service.create_concept(db, _concept_payload(db, code="OTRO-CODIGO"), T, C)
    assert exc.value.status_code == 409
    assert "producto/servicio" in exc.value.detail


def test_duplicate_concept_code_in_same_company_conflicts(db):
    concepts_service.create_concept(db, _concept_payload(db), T, C)
    with pytest.raises(HTTPException) as exc:
        concepts_service.create_concept(db, _concept_payload(db, ps_code="78101500"), T, C)
    assert exc.value.status_code == 409
    assert "clave 'FLETE-MAR'" in exc.value.detail


def test_same_product_service_allowed_in_another_company(db):
    concepts_service.create_concept(db, _concept_payload(db), T, C)
    other = concepts_service.create_concept(db, _concept_payload(db), T, OTHER_COMPANY)
    assert other.company_id == OTHER_COMPANY
    assert other.product_service_id == _product_service(db).id


def test_soft_deleted_concept_frees_its_product_service(db):
    first = concepts_service.create_concept(db, _concept_payload(db), T, C)
    concepts_service.delete_concept(db, first.id, T, C)
    reused = concepts_service.create_concept(db, _concept_payload(db), T, C)
    assert reused.id != first.id
    assert reused.product_service_id == first.product_service_id


def test_concept_is_isolated_by_tenant(db):
    other_tenant_concept = concepts_service.create_concept(db, _concept_payload(db), OTHER_TENANT, C)
    assert concepts_service.get_concepts(db, T, C) == []
    with pytest.raises(HTTPException) as exc:
        concepts_service.get_concept(db, other_tenant_concept.id, T, C)
    assert exc.value.status_code == 404
    with pytest.raises(HTTPException) as exc:
        concepts_service.update_concept(
            db, other_tenant_concept.id, ConceptUpdate(description="Ajeno"), T, C
        )
    assert exc.value.status_code == 404


def test_concept_rejects_unknown_sat_key(db):
    payload = _concept_payload(db)
    payload.product_service_id = 999999
    with pytest.raises(HTTPException) as exc:
        concepts_service.create_concept(db, payload, T, C)
    assert exc.value.status_code == 422


# ---------- Datos fiscales del emisor ----------

def _issuer_payload(db, legal_name: str = "Empresa Demo SA de CV") -> IssuerSettingsInput:
    regime = db.query(TaxRegime).filter(TaxRegime.code == "601").one()
    return IssuerSettingsInput(
        legal_name=legal_name, rfc=RFC_DUMMY, tax_regime_id=regime.id, zip_code="64000"
    )


def test_issuer_settings_upsert_keeps_one_row_per_company(db):
    created = issuer_service.save_issuer_settings(db, _issuer_payload(db), T, C, "tester")
    assert created.rfc == RFC_DUMMY

    updated = issuer_service.save_issuer_settings(
        db, _issuer_payload(db, legal_name="Empresa Demo Renombrada SA de CV"), T, C, "tester"
    )
    assert updated.id == created.id
    assert updated.legal_name == "Empresa Demo Renombrada SA de CV"

    rows = db.query(IssuerSettings).filter(
        IssuerSettings.tenant_id == T, IssuerSettings.company_id == C, IssuerSettings.deleted_at.is_(None)
    ).all()
    assert len(rows) == 1


def test_issuer_settings_missing_returns_404(db):
    with pytest.raises(HTTPException) as exc:
        issuer_service.get_issuer_settings(db, T, C)
    assert exc.value.status_code == 404


def test_issuer_rfc_is_validated_and_normalized(db):
    regime = db.query(TaxRegime).filter(TaxRegime.code == "601").one()
    with pytest.raises(ValidationError):
        IssuerSettingsInput(legal_name="Demo", rfc="RFC-INVALIDO", tax_regime_id=regime.id)
    with pytest.raises(ValidationError):
        IssuerSettingsInput(legal_name="Demo", rfc=RFC_DUMMY, tax_regime_id=regime.id, zip_code="123")

    normalized = IssuerSettingsInput(
        legal_name="Demo", rfc=" xaxx010101000 ", tax_regime_id=regime.id
    )
    assert normalized.rfc == RFC_DUMMY


def test_issuer_rejects_unknown_tax_regime(db):
    payload = _issuer_payload(db)
    payload.tax_regime_id = 999999
    with pytest.raises(HTTPException) as exc:
        issuer_service.save_issuer_settings(db, payload, T, C)
    assert exc.value.status_code == 422


# ---------- Amarre con las facturas ----------

def test_invoice_item_inherits_concept_description(db):
    concept = concepts_service.create_concept(db, _concept_payload(db), T, C)
    invoice = invoices_service.create_invoice(db, InvoiceCreate(reference="F-SAT-1"), T, C)
    item = invoices_service.create_item(
        db,
        InvoiceItemCreate(invoice_id=invoice.id, concept_id=concept.id, quantity=1, unit_amount=1500),
        T,
        C,
    )
    assert item.concept == concept.description  # copiada del catálogo para el PDF
    assert item.concept_id == concept.id

    # La respuesta expone las claves fiscales: el frontend etiqueta la partida con ellas.
    payload = InvoiceItemResponse.model_validate(item).model_dump()
    assert payload["concept_id"] == concept.id
    assert payload["concept"] == concept.description
    assert {"product_service_id", "unit_of_measure_id", "tax_object_id"} <= payload.keys()

    # Si el cliente sí manda el texto, se respeta tal cual.
    explicit = invoices_service.create_item(
        db,
        InvoiceItemCreate(
            invoice_id=invoice.id, concept_id=concept.id, concept="Flete a la medida", unit_amount=100
        ),
        T,
        C,
    )
    assert explicit.concept == "Flete a la medida"


def test_invoice_item_without_concept_or_catalog_is_rejected(db):
    invoice = invoices_service.create_invoice(db, InvoiceCreate(reference="F-SAT-2"), T, C)
    with pytest.raises(HTTPException) as exc:
        invoices_service.create_item(db, InvoiceItemCreate(invoice_id=invoice.id, unit_amount=10), T, C)
    assert exc.value.status_code == 422


def test_invoice_item_rejects_concept_from_another_company(db):
    concept = concepts_service.create_concept(db, _concept_payload(db), T, OTHER_COMPANY)
    invoice = invoices_service.create_invoice(db, InvoiceCreate(reference="F-SAT-3"), T, C)
    with pytest.raises(HTTPException) as exc:
        invoices_service.create_item(
            db, InvoiceItemCreate(invoice_id=invoice.id, concept_id=concept.id, unit_amount=10), T, C
        )
    assert exc.value.status_code == 422


def test_invoice_item_inherits_sat_keys_from_concept(db):
    """La partida hereda las claves fiscales del concepto para quedar completa (CFDI)."""
    concept = concepts_service.create_concept(db, _concept_payload(db), T, C)
    invoice = invoices_service.create_invoice(db, InvoiceCreate(reference="F-SAT-4"), T, C)

    item = invoices_service.create_item(
        db, InvoiceItemCreate(invoice_id=invoice.id, concept_id=concept.id, unit_amount=1500), T, C
    )
    assert item.product_service_id == concept.product_service_id
    assert item.unit_of_measure_id == concept.unit_of_measure_id
    assert item.tax_object_id == concept.tax_object_id


def test_invoice_item_sat_keys_sent_by_client_win_over_concept(db):
    """Lo que el cliente envía manda: permite facturar con otra unidad de medida."""
    concept = concepts_service.create_concept(db, _concept_payload(db), T, C)
    invoice = invoices_service.create_invoice(db, InvoiceCreate(reference="F-SAT-5"), T, C)
    other_unit = db.query(UnitOfMeasure).filter(UnitOfMeasure.code == "KGM").one()

    item = invoices_service.create_item(
        db,
        InvoiceItemCreate(
            invoice_id=invoice.id, concept_id=concept.id, unit_of_measure_id=other_unit.id, unit_amount=10
        ),
        T,
        C,
    )
    assert item.unit_of_measure_id == other_unit.id
    assert item.product_service_id == concept.product_service_id  # el resto sí se hereda


def test_changing_item_concept_reinherits_keys(db):
    """Cambiar el concepto de una partida revalida y vuelve a heredar del nuevo."""
    first = concepts_service.create_concept(db, _concept_payload(db), T, C)
    second = concepts_service.create_concept(
        db, _concept_payload(db, code="DESPACHO", ps_code="78141600"), T, C
    )
    invoice = invoices_service.create_invoice(db, InvoiceCreate(reference="F-SAT-6"), T, C)
    item = invoices_service.create_item(
        db, InvoiceItemCreate(invoice_id=invoice.id, concept_id=first.id, unit_amount=100), T, C
    )

    updated = invoices_service.update_item(
        db, item.id, InvoiceItemUpdate(concept_id=second.id), T, C
    )
    assert updated.concept_id == second.id
    assert updated.product_service_id == second.product_service_id
    assert updated.concept == second.description


def test_updating_item_rejects_concept_from_another_tenant(db):
    """El PATCH valida la referencia igual que el alta: no cruza tenants."""
    mine = concepts_service.create_concept(db, _concept_payload(db), T, C)
    alien = concepts_service.create_concept(db, _concept_payload(db), OTHER_TENANT, C)
    invoice = invoices_service.create_invoice(db, InvoiceCreate(reference="F-SAT-7"), T, C)
    item = invoices_service.create_item(
        db, InvoiceItemCreate(invoice_id=invoice.id, concept_id=mine.id, unit_amount=100), T, C
    )

    with pytest.raises(HTTPException) as exc:
        invoices_service.update_item(db, item.id, InvoiceItemUpdate(concept_id=alien.id), T, C)
    assert exc.value.status_code == 422


# ---------- Claves fiscales del receptor (crm.accounts) ----------

def test_account_accepts_sat_fiscal_keys(db):
    regime = db.query(TaxRegime).filter(TaxRegime.code == "601").one()
    cfdi_use = db.query(CfdiUse).filter(CfdiUse.code == "G03").one()

    account = accounts_service.create_account(
        db,
        AccountCreate(name="Cliente fiscal", tax_regime_id=regime.id, cfdi_use_id=cfdi_use.id),
        T,
        C,
    )
    assert account.tax_regime_id == regime.id and account.cfdi_use_id == cfdi_use.id


def test_account_rejects_unknown_sat_fiscal_keys(db):
    with pytest.raises(HTTPException) as exc:
        accounts_service.create_account(db, AccountCreate(name="Cliente malo", cfdi_use_id=999999), T, C)
    assert exc.value.status_code == 422

    account = accounts_service.create_account(db, AccountCreate(name="Cliente ok"), T, C)
    with pytest.raises(HTTPException) as exc:
        accounts_service.update_account(db, account.id, AccountUpdate(tax_regime_id=999999), T, C)
    assert exc.value.status_code == 422


def test_account_free_text_fiscal_fields_are_preserved(db):
    """El texto libre previo se conserva: las FK lo complementan, no lo sustituyen."""
    account = accounts_service.create_account(
        db, AccountCreate(name="Cliente heredado", tax_regime="601", cfdi_use="G03"), T, C
    )
    assert account.tax_regime == "601" and account.cfdi_use == "G03"
    assert account.tax_regime_id is None and account.cfdi_use_id is None


def test_legacy_invoices_keep_working_without_sat_fields(db, monkeypatch):
    """Las facturas previas, sin claves del SAT, siguen listándose y generando PDF."""
    stored = {}
    monkeypatch.setattr(
        "core.storage_s3.put_object_bytes",
        lambda key, body, content_type="": stored.update({"key": key, "len": len(body)}),
    )
    account = accounts_service.create_account(db, AccountCreate(name="Cliente heredado"), T, C)
    invoice = invoices_service.create_invoice(
        db, InvoiceCreate(reference="F-LEGACY", account_id=account.id, tax_rate=Decimal("16")), T, C
    )
    invoices_service.create_item(
        db, InvoiceItemCreate(invoice_id=invoice.id, concept="flete_internacional", unit_amount=1000), T, C
    )
    assert invoice.voucher_type_id is None and invoice.payment_form_id is None

    listed = invoices_service.get_invoices(db, T, C)
    assert invoice.id in [i.id for i in listed]

    sent = invoices_service.send_invoice(db, invoice.id, T, C)
    assert sent.status == "enviada" and stored["len"] > 0
