"""Pure parser/export fixtures — the CCE-* suite from the legacy
tests/cc-expenses.test.ts ported to the Python code of record
(app/services/cc/parse.py). Faithfulness against the legacy TS on REAL
data is proven separately by the byte-diff gate (test_parser_gate.py)."""

from decimal import Decimal

from app.services.cc.parse import (
    AssignedCcLineForPaste,
    AutoRuleMatch,
    find_auto_rule_match,
    find_highlight_match,
    format_cc_paste_block,
    parse_cc_line,
    parse_cc_paste,
    parse_currency,
    parse_expense_date,
)

# ------------------------------------------------------ parse_expense_date


def test_cce_d_001_long_format():
    assert parse_expense_date("May 20, 2026") == "2026-05-20"


def test_cce_d_002_iso_passthrough():
    assert parse_expense_date("2026-05-20") == "2026-05-20"


def test_cce_d_003_mixed_case_no_comma():
    assert parse_expense_date("may 20 2026") == "2026-05-20"
    assert parse_expense_date("MAY 20 2026") == "2026-05-20"


def test_cce_d_004_garbage_is_null():
    assert parse_expense_date("nonsense") is None
    assert parse_expense_date("") is None
    assert parse_expense_date("Foobar 20, 2026") is None
    assert parse_expense_date("13/13/13") is None


def test_cce_d_005_pads_single_digit_day():
    assert parse_expense_date("Jan 5, 2026") == "2026-01-05"


def test_cce_d_006_abbrev_with_period():
    # "Sept. 3, 2026" — abbreviation + trailing period, comma optional.
    assert parse_expense_date("Sept. 3, 2026") == "2026-09-03"


def test_cce_d_007_loose_validation_wart_carried():
    # WART carried verbatim for byte-exactness with the legacy parser
    # (no per-month calendar check). Fixing it would diverge the gate;
    # tracked as an open consideration in docs/features/cc-expenses.md.
    assert parse_expense_date("Feb 31, 2026") == "2026-02-31"


# ---------------------------------------------------------- parse_currency


def test_cce_c_001_strips_dollar_and_commas():
    assert parse_currency("$7.15") == Decimal("7.15")
    assert parse_currency("$3,856.56") == Decimal("3856.56")
    assert parse_currency("1234") == Decimal("1234")


def test_cce_c_002_null_on_empty_or_garbage():
    assert parse_currency("") is None
    assert parse_currency("   ") is None
    assert parse_currency("abc") is None


def test_cce_c_003_rounds_to_two_decimals_js_semantics():
    # JS Math.round(n*100)/100 on IEEE doubles (round half toward +Inf).
    assert parse_currency("$0.999") == Decimal("1")
    assert parse_currency("$10.234") == Decimal("10.23")
    assert parse_currency("7.155") == Decimal("7.16")
    assert parse_currency("2.675") == Decimal("2.68")


def test_cce_c_004_sign_quirks():
    assert parse_currency("-5") == Decimal("-5")
    assert parse_currency("$-5.00") == Decimal("-5")  # $ is leading, stripped
    assert parse_currency("-$5.00") is None  # $ not leading -> NaN -> None


# ------------------------------------------------------------- parse_cc_line


def test_cce_l_001_standard_line_trailing_tab():
    r = parse_cc_line("May 20, 2026\tFACEBK *N7VKLP9RX2\t$7.15\t\t$3,856.56\t")
    assert r is not None
    assert r.expense_date == "2026-05-20"
    assert r.description == "FACEBK *N7VKLP9RX2"
    assert r.amount == Decimal("7.15")
    assert r.balance == Decimal("3856.56")


def test_cce_l_002_credit_when_debit_blank():
    r = parse_cc_line("May 20, 2026\tREFUND\t\t$50.00\t$3,000.00")
    assert r is not None and r.amount == Decimal("50")


def test_cce_l_003_empty_line_null():
    assert parse_cc_line("") is None
    assert parse_cc_line("   ") is None


def test_cce_l_004_keeps_raw_line():
    raw = "May 20, 2026\tFOO\t$1.00"
    assert parse_cc_line(raw).raw_line == raw


def test_cce_l_005_partial_parse_keeps_other_fields():
    r = parse_cc_line("???\tDESC\t$5.00")
    assert r is not None
    assert r.expense_date is None
    assert r.description == "DESC"
    assert r.amount == Decimal("5")


def test_cce_l_006_debit_wins_when_both_set():
    r = parse_cc_line("May 20, 2026\tBOTH\t$10.00\t$99.00\t$0.00")
    assert r is not None and r.amount == Decimal("10")


# ------------------------------------------------------------ parse_cc_paste


def test_cce_p_001_splits_newlines_skips_blanks_order():
    text = "May 20, 2026\tA\t$1\n\nMay 19, 2026\tB\t$2\n   \nMay 18, 2026\tC\t$3"
    rows = parse_cc_paste(text)
    assert len(rows) == 3
    assert [r.description for r in rows] == ["A", "B", "C"]


def test_cce_p_002_crlf_endings():
    text = "May 20, 2026\tA\t$1\r\nMay 19, 2026\tB\t$2"
    assert len(parse_cc_paste(text)) == 2


# --------------------------------------------------------- find_highlight


def test_cce_h_001_substring_contains():
    kws = ["UBER", "FACEBK"]
    assert find_highlight_match("UBER *EATS", kws) == "UBER"
    assert find_highlight_match("FACEBK *N7VKLP9RX2", kws) == "FACEBK"


def test_cce_h_002_case_insensitive():
    assert find_highlight_match("uber *eats", ["UBER"]) == "UBER"


def test_cce_h_003_null_on_no_match_or_empty():
    assert find_highlight_match("GOOGLE *ADS", ["UBER"]) is None
    assert find_highlight_match(None, ["UBER"]) is None
    assert find_highlight_match("", ["UBER"]) is None


def test_cce_h_004_first_in_list_wins():
    assert find_highlight_match("UBER FACEBK", ["UBER", "FACEBK"]) == "UBER"


def test_cce_h_005_empty_keyword_skipped():
    assert find_highlight_match("FACEBK", ["", "FACEBK"]) == "FACEBK"


# --------------------------------------------------------- find_auto_rule


def _rule(**kw):
    base = dict(
        id="r1", keyword="ADS9772464823", project_id="p1",
        assignment_description="Google", category="Client Advertising Spend",
    )
    base.update(kw)
    return AutoRuleMatch(**base)


def test_cce_r_001_returns_matched_rule():
    rule = _rule()
    assert find_auto_rule_match("GOOGLE *ADS9772464823", [rule]) == rule


def test_cce_r_002_case_insensitive():
    r = find_auto_rule_match("google *ads9772464823", [_rule()])
    assert r is not None and r.id == "r1"


def test_cce_r_003_null_when_no_match():
    assert find_auto_rule_match("FACEBK *XYZ", [_rule()]) is None


def test_cce_r_004_first_match_wins_newest_precedence():
    # Caller passes rules created_at DESC -> first-match == newest wins.
    r2 = _rule(id="r2", keyword="GOOGLE")
    r = find_auto_rule_match("GOOGLE *ADS9772464823", [r2, _rule()])
    assert r is not None and r.id == "r2"


def test_cce_r_005_null_empty_description():
    assert find_auto_rule_match(None, [_rule()]) is None
    assert find_auto_rule_match("", [_rule()]) is None


# ------------------------------------------------------- format_cc_paste_block


def _assigned(**kw):
    base = dict(
        client="Tuscany", project="Monthly Services",
        assignment_description="Google", amount=Decimal("750"),
        expense_date="2026-05-18", category="Client Advertising Spend",
    )
    base.update(kw)
    return AssignedCcLineForPaste(**base)


def test_cce_x_001_empty_input():
    assert format_cc_paste_block([]) == ""


def test_cce_x_002_seven_column_shape():
    out = format_cc_paste_block([_assigned()])
    assert out.split("\t") == [
        "Tuscany : Monthly Services",
        "Google",
        "750.00",
        "2026-05-18",
        "",
        "Bowden Works",
        "Client Advertising Spend",
    ]


def test_cce_x_003_amount_two_decimals_no_symbol():
    out = format_cc_paste_block([_assigned(amount=Decimal("1234.5"))])
    assert "\t1234.50\t" in out
    assert "$" not in out


def test_cce_x_004_multiple_lines_joined_no_trailing_newline():
    out = format_cc_paste_block([_assigned(), _assigned(client="Venetian")])
    assert len(out.split("\n")) == 2
    assert not out.endswith("\n")


def test_cce_x_005_operator_label_is_tenant_setting():
    # Column 6 is a tenant setting (M6 parity); a non-default renders.
    out = format_cc_paste_block([_assigned()], operator_label="Acme Co")
    assert out.split("\t")[5] == "Acme Co"
