"""Юниты lookup контрагента по фамилии в plan_sync (чистая логика, без сети).

surname_index строит {фамилия: id} из имён контрагентов Финтабло («Фамилия Имя Отчество»),
resolve_pid перебирает слова имени из pay_config (порядок слов разный) и возвращает id по фамилии.
zp_ops проставляет partnerId ЗП-планкам ПО ЛЮДЯМ и None — агрегатам (администраторы) и педагогам.
"""
import plan_sync as ps


PARTNERS = [
    {"id": 101, "name": "Мухина Наталия Сергеевна"},
    {"id": 102, "name": "Поляков Михаил Викторович"},
    {"id": 103, "name": "Прохорова Ольга Сергеевна"},
    {"id": 104, "name": "Блинова Наталия Андреевна"},
    {"id": 105, "name": "Кревер Екатерина Ивановна"},
    {"id": 106, "name": "Хабриева Эльмира Шамилевна"},
    {"id": 107, "name": "Харитонов Александр Сергеевич"},
]
IDX = ps.surname_index(PARTNERS)


def test_surname_index_maps_first_word():
    assert IDX["мухина"] == 101
    assert IDX["харитонов"] == 107


def test_resolve_when_surname_first():
    assert ps.resolve_pid("Блинова Наталия", IDX) == 104
    assert ps.resolve_pid("Кревер Екатерина", IDX) == 105


def test_resolve_when_name_first():
    # порядок «Имя Фамилия» — перебираем слова, находим фамилию вторым словом
    assert ps.resolve_pid("Оля Прохорова", IDX) == 103
    assert ps.resolve_pid("Михаил Поляков", IDX) == 102
    assert ps.resolve_pid("Наташа Мухина", IDX) == 101


def test_resolve_with_parenthetical():
    assert ps.resolve_pid("Харитонов Александр (Саша)", IDX) == 107


def test_resolve_yo_normalized():
    idx = ps.surname_index([{"id": 9, "name": "Фёдорова Анна"}])
    assert ps.resolve_pid("Федорова Мария", idx) == 9   # ё == е


def test_resolve_not_found_returns_none():
    assert ps.resolve_pid("Селютина Софья", IDX) is None
    assert ps.resolve_pid("", IDX) is None


def test_zp_ops_sets_pid_for_people_none_for_aggregates():
    pid_for = lambda name: ps.resolve_pid(name, IDX)
    ops = ps.zp_ops(2026, 8, pid_for)          # (slug, desc, cat, amount, date, mb, pid)
    by_slug = {o[0]: o for o in ops}
    # педагоги — агрегат → None
    assert by_slug["teach:01"][6] is None and by_slug["teach:16"][6] is None
    # Харитонов — контрагент проставлен
    assert by_slug["zp:haritonov:vznos"][6] == 107
    # люди из pay_config получают partnerId; администраторы-агрегат — None
    admin_ops = [o for o in ops if "администратор" in o[1].lower()]
    assert admin_ops and all(o[6] is None for o in admin_ops)
    people_ops = [o for o in ops if o[6] is not None]
    assert people_ops                          # хоть кто-то из людей разрешился


def test_zp_ops_without_resolver_all_none():
    ops = ps.zp_ops(2026, 8)                    # без pid_for
    assert all(o[6] is None for o in ops)


def test_month_plan_pins_fixed_partner_id():
    # «% по займу» пиним явным id (в базе два тёзки-Назипова) — ставится без сети, даже без pid_for
    ops = ps.month_plan("09.2026")
    proc = [o for o in ops if o[0] == "proc_zaym"]
    assert proc and proc[0][6] == 4977253       # (slug, desc, cat, amt, date, mb, pid)


def test_month_plan_unmapped_fixed_partner_none():
    ops = ps.month_plan("09.2026")
    arenda = [o for o in ops if o[0] == "arenda"]
    assert arenda and arenda[0][6] is None      # аренды нет в FIXED_PARTNERS → None


# ── выбор суммы: факт прошлого месяца vs конфиг ──

ZAYM_CAT = ps.CAT["proc_zaym"]
BUH_CAT = ps.CAT["buh"]
ARENDA_CAT = ps.CAT["arenda"]


def test_prev_month():
    assert ps.prev_month("08.2026") == "07.2026"
    assert ps.prev_month("01.2026") == "12.2025"


def test_fact_amount_by_partner_sums_payments():
    # займ Алмаза (partnerId 4977253): один платёж 82 192 → сумма планки = факт
    facts = [{"partnerId": 4977253, "categoryId": ZAYM_CAT, "value": 82_192.0},
             {"partnerId": 999, "categoryId": ZAYM_CAT, "value": 5_000.0}]
    amt, basis = ps.fact_amount(facts, 4977253, ZAYM_CAT, 1)
    assert amt == 82_192.0 and basis == "контрагент"


def test_fact_amount_partner_multiple_payments_summed():
    facts = [{"partnerId": 7, "categoryId": BUH_CAT, "value": 30_000.0},
             {"partnerId": 7, "categoryId": BUH_CAT, "value": 3_000.0}]
    amt, _ = ps.fact_amount(facts, 7, BUH_CAT, 1)
    assert amt == 33_000.0


def test_fact_amount_by_category_when_single_plank():
    # нет partnerId, но в статье одна планка → берём факт статьи
    facts = [{"partnerId": None, "categoryId": BUH_CAT, "value": 33_000.0}]
    amt, basis = ps.fact_amount(facts, None, BUH_CAT, 1)
    assert amt == 33_000.0 and basis == "статья"


def test_fact_amount_category_ambiguous_returns_none():
    # в статье несколько планок (аренда/пожарка/вода/…) → неоднозначно, факт по статье НЕ берём
    facts = [{"partnerId": None, "categoryId": ARENDA_CAT, "value": 1_146_000.0},
             {"partnerId": None, "categoryId": ARENDA_CAT, "value": 7_000.0}]
    amt, basis = ps.fact_amount(facts, None, ARENDA_CAT, 4)
    assert amt is None and basis is None


def test_fact_amount_no_fact_returns_none():
    amt, basis = ps.fact_amount([], 4977253, ZAYM_CAT, 1)
    assert amt is None and basis == "контрагент"


def test_month_plan_uses_fact_for_fixed_keeps_config_without_fact():
    prior = [{"partnerId": 4977253, "categoryId": ZAYM_CAT, "value": 82_192.0}]
    ops = ps.month_plan("08.2026", pid_for=None, prior_facts=prior)
    proc = [o for o in ops if o[0] == "proc_zaym"][0]
    assert proc[3] == 82_192.0                  # сумма из факта, не конфигных 85 000
    # у аренды факта в prior нет → остаётся конфигная сумма
    arenda = [o for o in ops if o[0] == "arenda"][0]
    assert arenda[3] == 1_146_000


def test_month_plan_without_prior_facts_uses_config():
    ops = ps.month_plan("08.2026")
    proc = [o for o in ops if o[0] == "proc_zaym"][0]
    assert proc[3] == 85_000                    # без prior_facts — конфиг


def test_fixed_proposal_rows_shows_delta():
    prior = [{"partnerId": 4977253, "categoryId": ZAYM_CAT, "value": 82_192.0}]
    rows = ps.fixed_proposal_rows("08.2026", "07.2026", pid_for=None, prior_facts=prior)
    joined = " | ".join(rows)
    assert "% по займу" in joined and "82 192" in joined
    assert "факт июля" in joined and "конфиг 85 000" in joined


def test_fixed_proposal_rows_empty_when_fact_equals_config():
    # факт == конфиг → строки нет (нечего показывать)
    prior = [{"partnerId": 4977253, "categoryId": ZAYM_CAT, "value": 85_000.0}]
    rows = ps.fixed_proposal_rows("08.2026", "07.2026", pid_for=None, prior_facts=prior)
    assert rows == []


# ── сверка ставок ЗП с фактом (порог 10%) ──

def test_zp_rate_warning_when_fact_off_by_more_than_10pct():
    pid_for = lambda name: ps.resolve_pid(name, IDX)   # Мухина → 101
    # ставка Наташи Мухиной по pay_config = 30 000; факт июля 12 000 (>10% отклонение) → предупреждение
    prior = [{"partnerId": 101, "categoryId": 1, "value": 12_000.0}]
    warns = ps.zp_rate_warnings(2026, 8, "07.2026", prior, pid_for)
    muh = [w for w in warns if "Мухина" in w]
    assert muh and "факт июля" in muh[0] and "12 000" in muh[0]


def test_zp_rate_no_warning_within_10pct():
    pid_for = lambda name: ps.resolve_pid(name, IDX)
    # Блинова ставка 85 000; факт 84 000 (в пределах 10%) → без предупреждения
    prior = [{"partnerId": 104, "categoryId": 1, "value": 84_000.0}]
    warns = ps.zp_rate_warnings(2026, 8, "07.2026", prior, pid_for)
    assert not any("Блинова" in w for w in warns)


def test_zp_rate_no_warning_when_no_fact():
    pid_for = lambda name: ps.resolve_pid(name, IDX)
    warns = ps.zp_rate_warnings(2026, 8, "07.2026", [], pid_for)
    assert warns == []


# ── постоянка: контрагент пинится, чтобы матч не зависел от суммы ──────────
# 27.07.2026: планка «Вода» 14 560 висела «просрочено», хотя ЭКОТЕХНОЛОГИИ заплатили
# 10 650 ещё 21.07. partnerId не был проставлен → шаг A мимо, сумма не сошлась → мимо всё.

def test_recurring_vendors_are_pinned_to_partner():
    """У однозначной постоянки должен стоять partnerId — тогда сумма перестаёт быть критерием."""
    lines = {slug: pid for slug, _d, _c, _a, _dt, _mb, pid in ps.fixed_lines('08.2026')}
    for slug in ('voda', 'pozharka', 'internet', 'bot', 'buhgalteriya'):
        assert lines.get(slug), f"у постоянки «{slug}» не проставлен контрагент"


def test_water_tariff_follows_fact():
    """Тариф ЭКОТЕХНОЛОГИИ упал до 10 650 в мае и держится — канон обязан это знать."""
    amt = {slug: a for slug, _d, _c, a, _dt, _mb, _p in ps.fixed_lines('08.2026')}
    assert amt['voda'] == 10_650


def test_partner_pin_survives_amount_drift():
    """Планка с контрагентом гасится его фактом, даже когда сумма разошлась."""
    import matching, datetime
    plan = [{'ext': 'plansync:sync:07.2026:voda', 'date': '25.07.2026', 'factMonth': '07.2026',
             'catId': 1325377, 'cat': 'Аренда и содержание здания (ком.услуги)', 'value': 14_560.0,
             'partnerId': 4594669, 'desc': 'Вода (аренда автоматов)', 'matched': False}]
    facts = [{'date': '21.07.2026', 'catId': 1325377, 'cat': 'Аренда и содержание здания (ком.услуги)',
              'factMonth': '07.2026', 'value': 10_650.0, 'partnerId': 4594669,
              'desc': 'Оплата по счету №ТЕХ-607000000171', 'partner_name': 'ООО "ЭКОТЕХНОЛОГИЯ"'}]
    matching.match_cascade(plan, facts, datetime.date(2026, 7, 27))
    assert plan[0]['matched'] and plan[0]['paid'] == 10_650.0


# ── даты: зарплату можно в понедельник, аренду — нельзя ──

import datetime


def test_duedate_moves_back_from_weekend():
    """Аренда, коммуналка, кредиты, налоги: опаздывать нельзя, платим до выходных.
    15.08.2026 — суббота, значит 14.08 (пятница)."""
    assert ps.duedate(2026, 8, 15) == datetime.date(2026, 8, 14)   # сб → пт
    assert ps.duedate(2026, 8, 16) == datetime.date(2026, 8, 14)   # вс → пт
    assert ps.duedate(2026, 8, 17) == datetime.date(2026, 8, 17)   # пн — не трогаем
    # зарплаты двигаются в другую сторону — payday остаётся как был
    assert ps.payday(2026, 8, 15) == datetime.date(2026, 8, 17)


def test_is_mandatory_by_category_id():
    assert ps.is_mandatory(ps.CAT["arenda"])
    assert ps.is_mandatory(ps.CAT["nalog"])
    assert ps.is_mandatory(ps.CAT["proc_kredit"])
    assert ps.is_mandatory(ps.CAT["ndfl"])
    assert not ps.is_mandatory(ps.CAT["zp_ped"])
    assert not ps.is_mandatory(ps.CAT["servisy"])


def test_fixed_lines_puts_rent_before_weekend():
    """Аренда 15.08.2026 (сб) уезжает на 14.08, а не на 17.08."""
    lines = {s: d for s, _, _, _, d, _, _ in ps.fixed_lines("08.2026")}
    assert lines["arenda"] == datetime.date(2026, 8, 14)
    assert lines["vodootv"] == datetime.date(2026, 8, 14)
