"""Юниты enter_olya_plan (чистая логика, без сети).

Покрыто: парсинг суммы (формат «р.10\xa0638»), детерминизм externalId, маппинг категории
по «Статья расхода», фильтр по месяцу и статусу, сборка операций и разбивка сумм.
"""
import enter_olya_plan as e


# name→id для outcome-категорий (как отдаёт outcome_cats_by_name)
CATS = {
    'расходы на внутренние ивенты': 1543713,
    'расходы на съемки': 1543714,      # в Финтабло имя с хвостовым пробелом, norm его срежет
    'расходы на b2b-ивенты': 1543717,
    'сервисы': 1543704,
}


def row(art='Концерты / квартирники', date='25.08.2026', summa='р.20\xa0000',
        naznach='Оплата гитаристу', status='Не оплачено'):
    return {'Статья расхода': art, 'Дата платежа': date, 'Сумма': summa,
            'Назначение': naznach, 'Статус': status}


# ---- parse_sum ----

def test_parse_sum_olya_format():
    assert e.parse_sum('р.10\xa0638') == 10638.0
    assert e.parse_sum('р.20\xa0000') == 20000.0

def test_parse_sum_plain_and_comma():
    assert e.parse_sum('15000') == 15000.0
    assert e.parse_sum('1 234,5') == 1234.5

def test_parse_sum_empty():
    assert e.parse_sum('') is None
    assert e.parse_sum(None) is None


# ---- row_month / is_target_row ----

def test_row_month():
    assert e.row_month('25.08.2026') == '08.2026'
    assert e.row_month('01.12.2026') == '12.2026'
    assert e.row_month('') is None
    assert e.row_month('нет даты') is None

def test_is_target_status_and_month():
    assert e.is_target_row(row(status='Не оплачено', date='03.08.2026'), '08.2026')
    # оплачено — не берём (это факт, не план)
    assert not e.is_target_row(row(status='Оплачено', date='03.08.2026'), '08.2026')
    # другой месяц — не берём
    assert not e.is_target_row(row(status='Не оплачено', date='03.09.2026'), '08.2026')

def test_is_target_status_variants():
    # «Не оплачено» пишется по-разному — фильтр по префиксу «не оплач»
    assert e.is_target_row(row(status='не оплачено'), '08.2026')
    assert e.is_target_row(row(status='Не оплачен'), '08.2026')


# ---- ext_id детерминизм ----

def test_ext_id_deterministic():
    a = e.ext_id('25.08.2026', 20000.0, 'Оплата гитаристу')
    b = e.ext_id('25.08.2026', 20000.0, 'Оплата гитаристу')
    assert a == b
    assert a.startswith('plansync:olya:')

def test_ext_id_distinguishes_fields():
    base = e.ext_id('25.08.2026', 20000.0, 'Оплата гитаристу')
    assert base != e.ext_id('26.08.2026', 20000.0, 'Оплата гитаристу')   # дата
    assert base != e.ext_id('25.08.2026', 21000.0, 'Оплата гитаристу')   # сумма
    assert base != e.ext_id('25.08.2026', 20000.0, 'Оплата басисту')     # назначение


# ---- map_category ----

def test_map_category_by_statya():
    cid, name = e.map_category(row(art='Концерты / квартирники'), CATS)
    assert cid == 1543713 and name == 'расходы на внутренние ивенты'
    cid, name = e.map_category(row(art='Съемки'), CATS)
    assert cid == 1543714
    cid, name = e.map_category(row(art='Лекции'), CATS)
    assert cid == 1543717
    cid, name = e.map_category(row(art='Сервисы'), CATS)
    assert cid == 1543704

def test_map_category_unknown():
    cid, name = e.map_category(row(art='Что-то новое'), CATS)
    assert cid is None and name is None

def test_map_category_missing_in_fintablo():
    # статья маппится, но такой категории нет в переданном словаре -> id None
    cid, name = e.map_category(row(art='Съемки'), {'сервисы': 1543704})
    assert cid is None and name == 'расходы на съемки'


# ---- build_ops / разбивка ----

def SHEET():
    return [
        row(art='Концерты / квартирники', date='22.08.2026', summa='р.30\xa0000', naznach='Аренда Анлок'),
        row(art='Концерты / квартирники', date='25.08.2026', summa='р.20\xa0000', naznach='Гитарист Саша'),
        row(art='Съемки', date='03.08.2026', summa='р.15\xa0000', naznach='Софа cover'),
        row(art='Лекции', date='28.08.2026', summa='р.10\xa0000', naznach='ВДНХ гитарник'),
        row(art='Сервисы', date='05.08.2026', summa='р.500', naznach='VPN'),
        # мусор: другой месяц, оплачено, без суммы, неизвестная статья
        row(art='Съемки', date='03.09.2026', summa='р.99\xa0000', naznach='сентябрь'),
        row(art='Съемки', date='03.08.2026', summa='р.15\xa0000', naznach='оплачено', status='Оплачено'),
        row(art='Съемки', date='04.08.2026', summa='', naznach='без суммы'),
        row(art='Мимо', date='06.08.2026', summa='р.700', naznach='непонятно'),
    ]

def test_build_ops_selects_and_skips():
    ops, skipped = e.build_ops(SHEET(), '08.2026', CATS)
    assert len(ops) == 5                       # 5 валидных за август
    # 2 пропуска за август: без суммы + неизвестная статья (сент/оплачено отфильтрованы раньше)
    reasons = sorted(s['reason'] for s in skipped)
    assert reasons == ['нет категории', 'нет суммы']

def test_build_ops_total_and_breakdown():
    ops, _ = e.build_ops(SHEET(), '08.2026', CATS)
    total = sum(o['amount'] for o in ops.values())
    assert total == 30000 + 20000 + 15000 + 10000 + 500
    bd = e.category_breakdown(ops)
    assert bd['расходы на внутренние ивенты'] == (2, 50000.0)
    assert bd['расходы на съемки'] == (1, 15000.0)
    assert bd['расходы на b2b-ивенты'] == (1, 10000.0)
    assert bd['сервисы'] == (1, 500.0)

def test_build_ops_bucket_flag():
    ops, _ = e.build_ops(SHEET(), '08.2026', CATS)
    buckets = {o['catname']: o['bucket'] for o in ops.values()}
    assert buckets['расходы на внутренние ивенты'] == 'события (Оля)'
    assert buckets['расходы на съемки'] == 'события (Оля)'
    assert buckets['расходы на b2b-ивенты'] == 'события (Оля)'
    assert buckets['сервисы'] != 'события (Оля)'          # сервисы в свой бакет не идут


# ---- clip_desc ----

def test_clip_desc():
    assert e.clip_desc('  hi  ') == 'hi'
    assert len(e.clip_desc('x' * 500)) == e.DESC_MAX


# ── сироты: план сносим только под подтверждённый факт ──────────────────────
# Баг 27.07.2026: Оля помечает строку «Оплачено» — enter_olya_plan удаляет плановую
# операцию по одному статусу. Пока деньги не ушли и акт не занесён, расход не виден
# нигде, и отчёт показывает запас больше настоящего.

def _srow(summa, date, naznach, status, statya='Концерты / квартирники'):
    return {'Статья расхода': statya, 'Дата платежа': date, 'Сумма': summa,
            'Назначение': naznach, 'Статус': status, 'Способ оплаты': 'Точка', 'Проект': ''}


def test_month_exts_counts_paid_rows_too():
    """Индекс «строка вообще есть в таблице» не смотрит на статус."""
    rows = [_srow('20 000,00', '27.07.2026', 'Оплата Насте (клавиши)', 'Оплачено')]
    ext = e.ext_id('27.07.2026', 20_000.0, 'Оплата Насте (клавиши)')
    assert e.month_exts(rows, '07.2026') == {ext}
    assert e.month_exts(rows, '08.2026') == set()


def test_row_removed_from_table_is_deleted():
    """Строку убрали из таблицы (событие отменили) — обязательства нет, планку сносим."""
    orphans = {'plansync:olya:deadbeef': {'id': 1, 'value': 15_000.0, 'desc': 'отменённый концерт'}}
    dele, keep = e.split_orphans(orphans, still_in_table=set(), confirmed_paid=set())
    assert list(dele) == ['plansync:olya:deadbeef'] and keep == {}


def test_paid_in_table_without_fact_is_kept():
    """«Оплачено» в таблице, а списания не видно — планку ОСТАВЛЯЕМ, иначе расход исчезнет."""
    ext = 'plansync:olya:cafe0001'
    orphans = {ext: {'id': 2, 'value': 20_000.0, 'desc': 'Оплата Насте (клавиши) за концерт 25.07'}}
    dele, keep = e.split_orphans(orphans, still_in_table={ext}, confirmed_paid=set())
    assert dele == {} and list(keep) == [ext]


def test_paid_in_table_with_fact_is_deleted():
    """Факт нашёлся — обязательство закрыто, планку убираем."""
    ext = 'plansync:olya:cafe0002'
    orphans = {ext: {'id': 3, 'value': 20_000.0, 'desc': 'Оплата Олегу (кахон)'}}
    dele, keep = e.split_orphans(orphans, still_in_table={ext}, confirmed_paid={ext})
    assert list(dele) == [ext] and keep == {}


# ── колонки «Контрагент» и «Проект»: точный человек и событие за строкой плана ──
# В «Назначении» Оля пишет человека как придётся («Вове (бас)», «Саше Б»), поэтому
# в планку кладём ФИО из «Контрагента», а событие из «Проекта» — по ним потом матчится факт.

def krow(**kw):
    r = row(**{k: v for k, v in kw.items() if k in
              ('art', 'date', 'summa', 'naznach', 'status')})
    r['Контрагент'] = kw.get('kontr', 'СЗ Барышников Владимир')
    r['Проект'] = kw.get('project', 'концерт ДЕПО 11.07')
    return r


def test_build_ops_keeps_kontr_and_project():
    ops, _ = e.build_ops([krow(naznach='Оплата Вове (бас) за ДЕПО 11.07')], '08.2026', CATS)
    o = list(ops.values())[0]
    assert o['kontr'] == 'СЗ Барышников Владимир'
    assert o['project'] == 'концерт ДЕПО 11.07'


def test_plan_description_carries_naznach_and_project():
    """Описание планки = назначение + проект: по нему видно и за что, и на каком событии."""
    d = e.plan_description('Оплата Вове (бас) за ДЕПО 11.07', 'концерт ДЕПО 11.07')
    assert 'Вове' in d and 'концерт ДЕПО 11.07' in d


def test_plan_description_does_not_duplicate_project():
    """Проект уже назван в назначении — не дублируем."""
    d = e.plan_description('Оплата за концерт ДЕПО 11.07', 'концерт ДЕПО 11.07')
    assert d.count('ДЕПО') == 1


def test_plan_description_without_project():
    assert e.plan_description('Оплата типографии', '') == 'Оплата типографии'


def test_ext_id_ignores_project_and_kontr():
    """externalId считается от (дата|сумма|назначение) — добавление колонок не ломает дедуп."""
    a = e.ext_id('27.07.2026', 15000.0, 'Оплата Вове (бас) за ДЕПО 11.07')
    ops, _ = e.build_ops([krow(date='27.07.2026', summa='р.15 000',
                               naznach='Оплата Вове (бас) за ДЕПО 11.07')], '07.2026', CATS)
    assert list(ops) == [a]


# ---- резолв контрагента в Финтабло ----

PARTNERS = {101: 'БАРЫШНИКОВ ВЛАДИМИР СЕРГЕЕВИЧ', 102: 'Булгаков Александр Николаевич',
            103: 'Русманов Александр Сергеевич'}


def test_resolve_partner_by_surname_and_first_name():
    assert e.resolve_partner('СЗ Барышников Владимир', PARTNERS) == 101


def test_resolve_partner_rejects_namesake():
    """«Александр» есть у двоих — по одному имени контрагента не выбираем."""
    assert e.resolve_partner('СЗ Александров Александр', PARTNERS) is None


def test_resolve_partner_unknown_person():
    assert e.resolve_partner('СЗ Козлов Олег Владимирович', PARTNERS) is None


def test_resolve_partner_prefers_full_card_when_person_duplicated():
    """В Финтабло у людей задвоены карточки («Кокорев Юрий» и «Кокорев Юрий Сергеевич») —
    это один человек, берём самую полную запись, а не отказываемся от резолва."""
    partners = {201: 'Кокорев Юрий', 202: 'Кокорев Юрий Сергеевич'}
    assert e.resolve_partner('СЗ КОКОРЕВ ЮРИЙ СЕРГЕЕВИЧ', partners) == 202


def test_resolve_partner_reversed_order_in_sheet():
    assert e.resolve_partner('СЗ Ксения Хохлова', {301: 'Хохлова Ксения Игоревна'}) == 301
