"""Stripe "All Activity" paste -> Wave ledger (services/stripe_activity.py).

The fixture `fixtures/all_activity_real.txt` is a REAL clipboard copy of the
activity table (19 transactions, all three types, header included), so the
parser is pinned against the thing it actually has to read rather than an
idealised sample.
"""

from datetime import date
from decimal import Decimal
from pathlib import Path

import pytest

from app.services.stripe_activity import (
    parse_activity,
    parse_money,
    parse_short_date,
    to_csv,
    to_ledger_rows,
)

REAL = (Path(__file__).parent / "fixtures" / "all_activity_real.txt").read_text()

# The copy was taken a few days after its newest row; anchoring "today" here
# keeps the year inference deterministic instead of drifting with the clock.
TODAY = date(2026, 9, 8)


def rows_of(text: str, *, today: date = TODAY) -> list[dict]:
    return to_ledger_rows(parse_activity(text, today=today))


def block(*fields: str) -> str:
    """One 9-field activity record as the clipboard emits it."""
    assert len(fields) == 9
    return "\n".join(fields)


CHARGE = block(
    "US$1,000.00", "-US$29.30", "US$970.70", "Charge", "Payments",
    "Invoice #2026087", "txn_abc", "Sep 4", "Sep 10",
)
PAYOUT = block(
    "-US$2,005.96", "—", "-US$2,005.96", "Payout", "Payments",
    "STRIPE PAYOUT", "txn_def", "Sep 3", "Sep 4",
)
STRIPE_FEE = block(
    "-US$14.58", "-US$1.75", "-US$16.33", "Stripe fee", "Payments",
    "Billing - Usage Fee (2026-09-01)", "txn_ghi", "Sep 1", "Sep 2",
)


# ------------------------------------------------------------ money parsing


@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("US$1,000.00", Decimal("1000.00")),
        ("-US$29.30", Decimal("-29.30")),
        ("US$0.00", Decimal("0.00")),
        ("-US$2,005.96", Decimal("-2005.96")),
        ("CA$12.34", Decimal("12.34")),  # symbols stripped, not validated
        ("—", None),   # em dash: Stripe's "no value"
        ("–", None),   # en dash
        ("-", None),
        ("", None),
        ("   ", None),
        ("not money", None),
        (None, None),
    ],
)
def test_parse_money(raw, expected):
    assert parse_money(raw) == expected


def test_parse_money_keeps_thousands_separators_out_of_the_value():
    assert parse_money("US$1,234,567.89") == Decimal("1234567.89")


# ------------------------------------------------------------- date parsing


@pytest.mark.parametrize(
    ("raw", "expected"),
    [("Sep 4", (9, 4)), ("Sept 4", (9, 4)), ("Jan 31", (1, 31)), ("Dec 1", (12, 1))],
)
def test_parse_short_date(raw, expected):
    assert parse_short_date(raw) == expected


@pytest.mark.parametrize("raw", ["", "Sep", "4 Sep", "Foo 4", "Sep 99", "2026-09-04"])
def test_parse_short_date_rejects_junk(raw):
    assert parse_short_date(raw) is None


# ----------------------------------------------------------------- records


def test_real_paste_yields_every_transaction_and_nothing_left_over():
    res = parse_activity(REAL, today=TODAY)
    assert len(res.activities) == 19
    assert res.leftover_lines == 0
    assert res.undated == 0
    assert res.unknown_types == []


def test_header_is_optional():
    """The same paste works whether or not the column titles were grabbed."""
    with_header = parse_activity(REAL, today=TODAY)
    without = parse_activity(REAL.split("Available on\n", 1)[1], today=TODAY)
    assert len(without.activities) == len(with_header.activities)


def test_blank_lines_between_rows_are_ignored():
    res = parse_activity(f"{CHARGE}\n\n\n{PAYOUT}\n", today=TODAY)
    assert len(res.activities) == 2


def test_a_truncated_paste_reports_its_leftover_lines():
    """A half-copied last row must be VISIBLE, not silently dropped."""
    res = parse_activity(f"{CHARGE}\nUS$5.00\n-US$1.00\n", today=TODAY)
    assert len(res.activities) == 1
    assert res.leftover_lines == 2


# ------------------------------------------------------------- the fee split


def test_charge_becomes_a_charge_line_plus_a_credit_card_fee():
    assert rows_of(CHARGE) == [
        {"date": "2026-09-04", "description": "Stripe Charge - Invoice #2026087",
         "amount": "1000.00"},
        {"date": "2026-09-04", "description": "Credit Card Fee", "amount": "-29.30"},
    ]


def test_payout_has_no_fee_line():
    """Stripe writes an em dash for a payout's fee — that line is dropped."""
    assert rows_of(PAYOUT) == [
        {"date": "2026-09-03", "description": "Stripe Payout", "amount": "-2005.96"},
    ]


def test_stripe_fee_row_becomes_two_stripe_fee_lines():
    assert rows_of(STRIPE_FEE) == [
        {"date": "2026-09-01", "description": "Stripe Fee", "amount": "-14.58"},
        {"date": "2026-09-01", "description": "Stripe Fee", "amount": "-1.75"},
    ]


def test_a_zero_fee_is_dropped_like_a_missing_one():
    zero_fee = block(
        "US$100.00", "US$0.00", "US$100.00", "Charge", "Payments",
        "Invoice #1", "txn_z", "Sep 4", "Sep 10",
    )
    assert [r["description"] for r in rows_of(zero_fee)] == ["Stripe Charge - Invoice #1"]


def test_the_description_is_carried_through_verbatim():
    """Stripe's own wording, casing and stray '#' included — rian's call, so
    the ledger line matches what he sees in Stripe."""
    odd = block(
        "US$322.00", "-US$9.64", "US$312.36", "Charge", "Payments",
        "Quarterly Invoice # 2026073", "txn_q", "Aug 24", "Aug 28",
    )
    assert rows_of(odd)[0]["description"] == "Stripe Charge - Quarterly Invoice # 2026073"


def test_an_unknown_type_falls_back_to_its_description_and_is_reported():
    refund = block(
        "-US$50.00", "US$1.45", "-US$48.55", "Refund", "Payments",
        "Refund for #2026087", "txn_r", "Sep 2", "Sep 3",
    )
    res = parse_activity(refund, today=TODAY)
    assert res.unknown_types == ["Refund"]
    assert to_ledger_rows(res)[0]["description"] == "Refund for #2026087"


# ------------------------------------------------------------ year recovery


def test_the_year_comes_from_the_current_year_when_nothing_crosses_new_year():
    assert all(r["date"].startswith("2026-") for r in rows_of(REAL))


def test_a_month_jumping_up_going_down_the_list_steps_back_a_year():
    """Newest-first, so months only rise when the list crossed New Year."""
    jan = block("US$10.00", "—", "US$10.00", "Charge", "Payments",
                "Jan invoice", "txn_1", "Jan 5", "Jan 6")
    dec = block("US$20.00", "—", "US$20.00", "Charge", "Payments",
                "Dec invoice", "txn_2", "Dec 28", "Dec 29")
    rows = rows_of(f"{jan}\n{dec}", today=date(2026, 1, 10))
    assert rows[0]["date"] == "2026-01-05"
    assert rows[1]["date"] == "2025-12-28"


def test_a_list_pasted_after_new_year_anchors_to_last_year():
    """Every row is December; pasted in January it belongs to the year just
    gone, not eleven months in the future."""
    dec = block("US$20.00", "—", "US$20.00", "Charge", "Payments",
                "Dec invoice", "txn_2", "Dec 28", "Dec 29")
    assert rows_of(dec, today=date(2026, 1, 3))[0]["date"] == "2025-12-28"


def test_a_few_days_into_the_future_does_not_trigger_a_rollback():
    """Clock/timezone skew must not silently rewrite the year."""
    assert rows_of(CHARGE, today=date(2026, 9, 1))[0]["date"] == "2026-09-04"


def test_an_impossible_day_is_reported_not_raised():
    feb30 = block("US$10.00", "—", "US$10.00", "Charge", "Payments",
                  "Bad date", "txn_b", "Feb 30", "Mar 1")
    res = parse_activity(feb30, today=date(2026, 3, 1))
    assert res.undated == 1
    assert to_ledger_rows(res)[0]["date"] == ""  # visible, not dropped


# ------------------------------------------------------------ reconciliation


def test_every_row_reconciles_to_stripes_own_total():
    """The split must not invent or lose money: amount + fees == Stripe's
    total on every row, and the emitted lines sum to the same grand total."""
    res = parse_activity(REAL, today=TODAY)
    for a in res.activities:
        assert (a.amount or 0) + (a.fees or 0) == a.total, a.description
    emitted = sum(Decimal(r["amount"]) for r in to_ledger_rows(res))
    assert emitted == sum((a.total or Decimal(0)) for a in res.activities)
    assert emitted == Decimal("2426.90")


def test_the_real_paste_produces_the_expected_row_count():
    """19 transactions -> 32 lines: 13 with a fee, 6 payouts without."""
    assert len(rows_of(REAL)) == 32


# -------------------------------------------------------------------- csv


def test_csv_header_and_shape():
    csv_text = to_csv(rows_of(CHARGE))
    lines = csv_text.strip().splitlines()
    assert lines[0] == "Date,Description,Amount"
    assert lines[1] == "2026-09-04,Stripe Charge - Invoice #2026087,1000.00"
    assert lines[2] == "2026-09-04,Credit Card Fee,-29.30"


def test_csv_quotes_a_description_containing_a_comma():
    comma = block("US$10.00", "—", "US$10.00", "Charge", "Payments",
                  "Invoice #1, second half", "txn_c", "Sep 4", "Sep 10")
    assert '"Stripe Charge - Invoice #1, second half"' in to_csv(rows_of(comma))


def test_empty_paste_is_an_empty_ledger_not_an_error():
    assert rows_of("") == []
    assert to_csv([]).strip() == "Date,Description,Amount"
