from __future__ import annotations

from io import BytesIO
from zipfile import ZipFile, ZipInfo

import pytest

from jarvis_finance.imports import postfinance_documents as parser


def overview_text() -> str:
    lines = [
        "Portfolio  Stand 27/07/2026 09:14:30",
        "Offene Aufträge 0",
        "EUR  1.0000  100.00  x  x  100.00  x",
        "USD  1.0000  100.00  x  x  100.00  x",
        "Gesamt CHF  200.00  2'200.00  2'400.00",
        "Produkt  Anzahl  Einstandskurs  Einstandswert  Differenz  Kurs  x  Wert CHF  Anteil %",
        "Aktien",
    ]
    for index in range(11):
        lines.append(f"Synthetic Asset {index + 1:02d}  1  50  50  -  100 CHF  x  100  4.17")
    lines.extend(["Zwischensumme Aktien in CHF  0  0  1'100.00", "ETFs"])
    for index in range(11, 22):
        lines.append(f"Synthetic Asset {index + 1:02d}  1  50  50  -  100 CHF  x  100  4.17")
    lines.append("Zwischensumme ETFs in CHF  0  0  1'100.00")
    return "\n".join(lines)


def trade_text() -> str:
    return """Transaktionsbeleg
Börsentransaktion: Kauf
Unsere Referenz: 100
Kontonummer: 123456
Ausführungsdatum: 01.07.2026
Valutadatum: 03.07.2026
Synthetic Asset 01 ISIN: CH0000000000
Anzahl  Preis  Betrag
1  50  CHF  50
Total CHF 50
"""


def account_statement_text() -> str:
    return """Ihr Kontoauszug
Kontoauszug vom 01.07.2026 bis 31.07.2026
Kontonummer: 123456
Kontoauszug in CHF
DATUM           INFORMATION                                   REFERENZ   BELASTUNG   GUTSCHRIFT   VALUTA-DATUM          SALDO (CHF)
01.07.2026 Anfangsbestand                                                                                                       0.00
01.07.2026 Depotgebühren                                    1129157288 18.00                        01.07.2026                -18.00
           Depotgebühren
01.07.2026 Automatisierter Währungstausch                   1129515219                18.00         01.07.2026                  0.00
           EUR - CHF
           1 CHF = 1.09889 EUR
31.07.2026 Schlussbilanz                                                                                                        0.00
Kontoauszug in USD
DATUM           INFORMATION                                   REFERENZ   BELASTUNG   GUTSCHRIFT   VALUTA-DATUM          SALDO (USD)
01.07.2026 Anfangsbestand                                                                                                  9'187.40
15.07.2026 Dividende                                        1136755240                47.31         13.07.2026             9'234.71
           SYNTHETIC ADR
           Anzahl: 46
           Betrag: USD 48.30
           Taxen: USD 0.00
           Total: USD 47.31
21.07.2026 Dividende                                        1140661666                0.84          21.07.2026             9'235.55
           SYNTHETIC ORD
           Anzahl: 8
           Betrag: USD 1.20
           Taxen: USD 0.18
           Total: USD 0.84
31.07.2026 Schlussbilanz                                                                                                   9'235.55
Kontoauszug in EUR
DATUM           INFORMATION                                   REFERENZ   BELASTUNG   GUTSCHRIFT   VALUTA-DATUM            SALDO (EUR)
01.07.2026 Anfangsbestand                                                                                                  21'713.05
01.07.2026 Automatisierter Währungstausch                   1129515219 19.78                        01.07.2026             21'693.27
           EUR - CHF
           1 EUR = 0.91001 CHF
31.07.2026 Schlussbilanz                                                                                                   21'693.27
"""


def zip_bytes(name: str = "20260701_Borsenabrechnung_100.pdf", raw: bytes = b"%PDF-trade") -> bytes:
    output = BytesIO()
    with ZipFile(output, "w") as archive:
        archive.writestr(name, raw)
    return output.getvalue()


def test_synthetic_zip_pdf_bundle_is_parsed_and_reconciled(monkeypatch):
    monkeypatch.setattr(parser, "_pdf_text", lambda raw: (trade_text(), 1) if raw == b"%PDF-trade" else (overview_text(), 5))
    result = parser.parse_postfinance_bundle(zip_bytes(), b"%PDF-overview")
    assert len(result.documents) == 1
    assert len(result.events) == 1 and result.events[0].event_type == "buy"
    assert len(result.snapshot.positions) == 22 and len(result.snapshot.cash) == 2
    assert result.snapshot.total_value_chf == result.snapshot.securities_total_chf + result.snapshot.cash_total_chf
    assert result.snapshot.open_orders == 0


def test_multicurrency_account_statement_emits_fee_dividend_and_paired_fx_events(monkeypatch):
    monkeypatch.setattr(
        parser,
        "_pdf_text",
        lambda raw: (account_statement_text(), 4)
        if raw == b"%PDF-statement"
        else (overview_text(), 5),
    )
    bundle = parse_bundle = parser.parse_postfinance_bundle(
        zip_bytes("260826_Postfinance_Kontoauszug.pdf", b"%PDF-statement"),
        b"%PDF-overview",
    )

    assert len(parse_bundle.documents) == 1
    assert [event.event_type for event in bundle.events] == [
        "fee",
        "fx",
        "dividend",
        "dividend",
        "fx",
    ]
    assert [event.currency for event in bundle.events] == ["CHF", "CHF", "USD", "USD", "EUR"]
    assert [event.net_original for event in bundle.events] == [
        parser.Decimal("18.00"),
        parser.Decimal("18.00"),
        parser.Decimal("47.31"),
        parser.Decimal("0.84"),
        parser.Decimal("19.78"),
    ]
    fx_events = [event for event in bundle.events if event.event_type == "fx"]
    assert {event.direction for event in fx_events} == {"in", "out"}
    assert len({event.internal_transfer_group for event in fx_events}) == 1
    assert all("internal_fx_not_external_cashflow" in event.reason_codes for event in fx_events)


def test_zip_path_traversal_and_symlink_are_rejected(monkeypatch):
    monkeypatch.setattr(parser, "_pdf_text", lambda _raw: (overview_text(), 1))
    with pytest.raises(ValueError, match="unsafe"):
        parser.parse_postfinance_bundle(zip_bytes("../escape.pdf"), b"%PDF-overview")
    output = BytesIO()
    with ZipFile(output, "w") as archive:
        info = ZipInfo("link.pdf")
        info.external_attr = (0o120777 << 16)
        archive.writestr(info, b"%PDF-link")
    with pytest.raises(ValueError, match="unsafe"):
        parser.parse_postfinance_bundle(output.getvalue(), b"%PDF-overview")


def test_preview_parser_rejects_incomplete_or_non_pdf_sources(monkeypatch):
    monkeypatch.setattr(parser, "_pdf_text", lambda _raw: (overview_text(), 5))
    with pytest.raises(ValueError, match="unsupported entry"):
        parser.parse_postfinance_bundle(zip_bytes("note.txt"), b"%PDF-overview")
    monkeypatch.setattr(parser, "_pdf_text", lambda raw: (trade_text(), 1) if raw == b"%PDF-trade" else ("27/07/2026 09:14:30\nno positions", 1))
    with pytest.raises(ValueError, match="complete snapshot"):
        parser.parse_postfinance_bundle(zip_bytes(), b"%PDF-overview")
