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

Месячный итог шлётся 1-го числа и сам снимает доходные планки — проверяем, что без
--commit ничего не удаляется и что журнал пишется до мутаций.
"""
import datetime

import report_month as rm


# ── снятие планок: живые деньги, порядок важен ──

def test_dry_run_does_not_delete():
    """Без --commit планки не трогаем."""
    calls = []
    res = rm.retire(ops=[{"id": 1}], commit=False, delete=lambda i: calls.append(i))
    assert calls == []
    assert res["retired"] == 0 and res["dry_run"] is True


def test_commit_writes_journal_before_delete():
    """Журнал строго до удаления: упадём посреди — видно, что восстанавливать."""
    order = []
    rm.retire(ops=[{"id": 1}, {"id": 2}], commit=True,
              delete=lambda i: order.append(f"delete{i}"),
              journal=lambda ops: order.append("journal"))
    assert order == ["journal", "delete1", "delete2"]


def test_commit_reports_count():
    res = rm.retire(ops=[{"id": 1}, {"id": 2}], commit=True, delete=lambda i: None,
                    journal=lambda ops: None)
    assert res["retired"] == 2 and res["dry_run"] is False


# ── легаси-слаги: построчная Δ врёт, категории сходятся ──

def test_legacy_slugs_suppress_row_delta():
    """План июля на старых слагах, факт на новых: построчная Δ показала бы разгром
    −2 400 000 по абонементам и рост +2 907 064 по урокам на одних и тех же деньгах."""
    summary = {"abonementy": {"plan": 2_400_000, "fact": 0, "delta": -2_400_000},
               "uroki": {"plan": 0, "fact": 2_907_064, "delta": 2_907_064}}
    assert rm.has_legacy(summary) is True
    t = rm.income_vs_plan(summary)
    assert "1-1" not in t                    # построчной разбивки нет
    assert rm.money(-2_400_000) not in t     # мнимого провала по абонементам нет
    assert rm.money(507_064) in t            # только честная Δ категории
    assert "старых слагах" in t


def test_clean_slugs_show_row_delta():
    summary = {"uroki": {"plan": 4_000_000, "fact": 3_800_000, "delta": -200_000},
               "hor": {"plan": 200_000, "fact": 222_499, "delta": 22_499}}
    assert rm.has_legacy(summary) is False
    t = rm.income_vs_plan(summary)
    assert "1-1" in t and "Хор" in t


def test_by_category_sums_legacy_into_b2c():
    """Категории сходятся даже при легаси: abonementy и uroki — обе B2C."""
    summary = {"abonementy": {"plan": 2_400_000, "fact": 0},
               "uroki": {"plan": 0, "fact": 2_907_064},
               "b2b": {"plan": 307_000, "fact": 229_490}}
    cat = rm.by_category(summary)
    assert cat["B2C"] == {"plan": 2_400_000, "fact": 2_907_064}
    assert cat["B2B"] == {"plan": 307_000, "fact": 229_490}


def test_outcome_block_sorted_desc():
    t = rm.outcome_vs_plan({"ЗП педагогов": 724_600, "Аренда и помещение": 1_247_177})
    assert t.index("Аренда") < t.index("ЗП педагогов")


# ── «Как идём»: выводы по правилам, а не сочинение ──

REV = {"01.2025": 1_055_204, "05.2025": 2_275_936, "06.2025": 1_764_664, "07.2025": 1_626_328,
       "01.2026": 2_840_895, "05.2026": 4_206_890, "06.2026": 4_194_991, "07.2026": 4_348_094}


def test_plateau_detected():
    """Три месяца в пределах ±5% — плато, а не рост."""
    assert "плато" in rm.how_are_we_doing(REV, "07.2026", [], {})


def test_year_over_year():
    t = rm.how_are_we_doing(REV, "07.2026", [], {})
    assert "+167%" in t


def test_growth_detected():
    rev = dict(REV, **{"05.2026": 2_500_000, "06.2026": 3_000_000})
    t = rm.how_are_we_doing(rev, "07.2026", [], {})
    assert "Рост" in t and "плато" not in t


def test_decline_detected():
    rev = dict(REV, **{"05.2026": 6_000_000, "06.2026": 5_000_000})
    assert "Спад" in rm.how_are_we_doing(rev, "07.2026", [], {})


def test_expenses_share_of_revenue():
    """Не пересказываем блок расходов, а считаем чего в нём нет: долю выручки."""
    t = rm.how_are_we_doing(REV, "07.2026", [],
                            {"Проценты по кредитам/займам": 609_031, "Прочее": 3_531_089})
    assert "Расходы съели 95% выручки" in t


def test_debt_service_share_flagged():
    """609 031 процентов от выручки 4 348 094 — 14%, долговая нагрузка заметная."""
    t = rm.how_are_we_doing(REV, "07.2026", [],
                            {"Проценты по кредитам/займам": 609_031, "Прочее": 3_531_089})
    assert "обслуживание долга ушло 14% выручки" in t


def test_debt_service_silent_when_small():
    t = rm.how_are_we_doing(REV, "07.2026", [], {"Прочее": 4_140_120})
    assert "обслуживание долга" not in t


def test_plan_misses_listed():
    t = rm.how_are_we_doing(REV, "07.2026", [("Аренда", 517_455, 600_000)], {})
    assert "Аренда" in t and "второй месяц ниже плана" in t


# ── маржа: за один месяц шумит, берём три ──

def _pnl3(margin, revenue):
    return {m: {"margin": dict(margin), "revenue": dict(revenue)}
            for m in ("05.2026", "06.2026", "07.2026")}


def test_margin_block_shows_percent():
    """Маржинальность — это процент от выручки направления, а не рубли:
    «1-1 — 5 030 610» не говорит, эффективно ли направление."""
    t = rm.margin_block(_pnl3({"1-1": 700_000}, {"1-1": 1_000_000}),
                        ["05.2026", "06.2026", "07.2026"])
    assert "70%" in t


def test_margin_block_sorted_by_percent():
    """Сортировка по маржинальности, а не по объёму: мелкое, но прибыльное — выше."""
    pnl = _pnl3({"1-1": 700_000, "Аренда": 900_000},
                {"1-1": 1_000_000, "Аренда": 3_000_000})
    t = rm.margin_block(pnl, ["05.2026", "06.2026", "07.2026"])
    assert t.index("1-1") < t.index("Аренда")


def test_margin_block_keeps_amount_alongside():
    t = rm.margin_block(_pnl3({"1-1": 700_000}, {"1-1": 1_000_000}),
                        ["05.2026", "06.2026", "07.2026"])
    assert rm.money(2_100_000) in t          # сумма за три месяца тоже видна


def test_margin_block_handles_zero_revenue():
    """Направление без выручки за период не делит на ноль и не выпадает молча."""
    t = rm.margin_block(_pnl3({"Съёмки": -15_000}, {"Съёмки": 0}),
                        ["05.2026", "06.2026", "07.2026"])
    assert "Съёмки" in t


def test_margin_block_empty_when_no_data():
    assert rm.margin_block({}, ["05.2026"]) == ""


# ── сборка: порядок блоков фиксирован ──

def _data(**kw):
    base = dict(mm="07.2026", revenue=4_348_094.0, outcome=4_140_120.0,
                summary={"uroki": {"plan": 4_000_000.0, "fact": 3_800_000.0, "delta": -200_000.0}},
                outcome_lines={"Аренда и помещение": 1_247_177.0},
                revenue_by_month={"07.2025": 1_626_328.0, "07.2026": 4_348_094.0},
                pnl={}, months=["05.2026", "06.2026", "07.2026"],
                free=1_411_965.0, platform=598_887.0, plan_next=6_050_000.0,
                big_ahead=[("14.08", "Аренда и помещение", 1_146_000.0, "")],
                retired=6, plan_misses=[])
    base.update(kw)
    return base


def test_render_assembles_all_blocks():
    t = rm.render(_data())
    for marker in ("итоги июля", "Доходы: прогноз ↔ факт", "Расходы по строкам",
                   "Как идём", "С чем входим", "Снято"):
        assert marker in t, marker


def test_render_keeps_block_order():
    t = rm.render(_data())
    assert t.index("Доходы") < t.index("Расходы по строкам") < t.index("Как идём")


def test_render_shows_month_bottom_line():
    """Итог одной строкой: выручка, расходы, разница, год к году."""
    t = rm.render(_data())
    assert rm.money(4_348_094) in t and rm.money(207_974) in t
    assert "+167%" in t


def test_render_skips_margin_when_no_data():
    assert "Маржа" not in rm.render(_data(pnl={}))


def test_render_includes_margin_when_present():
    pnl = {m: {"margin": {"1-1": 700_000}} for m in ("05.2026", "06.2026", "07.2026")}
    assert "Маржа по направлениям" in rm.render(_data(pnl=pnl))


# ── план мог быть снят раньше срока: берём его из журнала ──

def test_plan_falls_back_to_journal(tmp_path, monkeypatch):
    """Планки июля сняли вручную 31.07 в 18:13 — живого плана уже нет, но журнал есть."""
    j = tmp_path / "income_retire_07-2026.json"
    j.write_text('{"month": "07.2026", "summary": {"uroki": {"plan": 2400000, "fact": 0}}}',
                 encoding="utf-8")
    monkeypatch.setattr(rm, "journal_path", lambda mm: str(j))
    summary = {"uroki": {"plan": 0.0, "fact": 2_907_064.0, "delta": 2_907_064.0}}
    merged = rm.with_journal_plan("07.2026", summary)
    assert merged["uroki"]["plan"] == 2_400_000
    assert merged["uroki"]["delta"] == 2_907_064.0 - 2_400_000


def test_plan_kept_when_alive(tmp_path, monkeypatch):
    """Живой план не затирается журналом."""
    monkeypatch.setattr(rm, "journal_path", lambda mm: str(tmp_path / "нет.json"))
    summary = {"uroki": {"plan": 4_000_000.0, "fact": 3_800_000.0, "delta": -200_000.0}}
    assert rm.with_journal_plan("07.2026", summary)["uroki"]["plan"] == 4_000_000.0


# ── тренд: «рост подряд» только если он действительно подряд ──

def test_wobble_is_not_growth():
    """4 206 969 → 4 197 958 → 4 441 859: второй месяц был спад, это не «рост подряд»."""
    rev = {"05.2026": 4_206_969, "06.2026": 4_197_958, "07.2026": 4_441_859}
    t = rm.how_are_we_doing(rev, "07.2026", [], {})
    assert "Рост третий месяц подряд" not in t


def test_monotonic_growth_is_growth():
    rev = {"05.2026": 3_000_000, "06.2026": 3_500_000, "07.2026": 4_441_859}
    assert "Рост третий месяц подряд" in rm.how_are_we_doing(rev, "07.2026", [], {})


def test_platform_hint_not_duplicated_across_rows():
    """598 887 на площадке покрывают только ближайшую ЗП, а не каждую из двух."""
    rows = rm.mark_platform([("03.08", "ЗП педагогов", 700_000.0, "Зарплаты педагогов"),
                             ("17.08", "ЗП педагогов", 700_000.0, "Зарплаты педагогов")],
                            598_887.0)
    assert rows[0][3] != "" and rows[1][3] == ""


def test_platform_hint_spreads_while_balance_lasts():
    rows = rm.mark_platform([("03.08", "ЗП педагогов", 300_000.0, "Зарплаты педагогов"),
                             ("17.08", "ЗП педагогов", 300_000.0, "Зарплаты педагогов")],
                            598_887.0)
    assert rows[0][3] != "" and rows[1][3] != ""


# ── заёмные деньги: не выручка, но именно они закрыли месяц ──

def test_loan_flagged_when_month_is_negative():
    """Июль 2026: операционно −290 412, но 15.07 пришёл кредит 1 500 000.
    Без него месяц было бы нечем закрывать — вывод обязан это назвать."""
    t = rm.how_are_we_doing(REV, "07.2026", [], {"Прочее": 4_732_270}, fin_in=1_500_000)
    assert rm.money(1_500_000) in t
    assert "заём" in t.lower() or "кредит" in t.lower()


def test_loan_mentioned_even_when_month_is_positive():
    """Кредит не выручка: если месяц в плюсе, всё равно проговариваем, что часть денег заёмная."""
    t = rm.how_are_we_doing(REV, "07.2026", [], {"Прочее": 3_000_000}, fin_in=1_500_000)
    assert rm.money(1_500_000) in t


def test_no_loan_line_without_borrowing():
    t = rm.how_are_we_doing(REV, "07.2026", [], {"Прочее": 3_000_000})
    assert "кредит" not in t.lower()


# ── «Крупное впереди»: только то, что реально предстоит платить ──

def test_big_upcoming_skips_paid_plan():
    """Планка 03.08 оплачена 31.07 и разнесена — в «Крупное впереди» ей не место."""
    ops = [{"date": "03.08.2026", "value": 592_150.0, "due": 0.0, "cat": "Зарплаты педагогов"},
           {"date": "14.08.2026", "value": 1_146_000.0, "due": 1_146_000.0,
            "cat": "Аренда и содержание здания"}]
    rows = rm.big_upcoming(ops, "08.2026")
    assert [r[1] for r in rows] == ["Аренда и содержание здания"]


def test_big_upcoming_uses_remaining_debt():
    """Показываем остаток долга, а не исходную оценку планки."""
    ops = [{"date": "03.08.2026", "value": 700_000.0, "due": 107_850.0,
            "cat": "Зарплаты педагогов"}]
    assert rm.big_upcoming(ops, "08.2026", threshold=100_000) == [
        ("03.08", "Зарплаты педагогов", 107_850.0, "Зарплаты педагогов")]


def test_big_upcoming_filters_other_months():
    ops = [{"date": "03.09.2026", "value": 700_000.0, "due": 700_000.0, "cat": "Зарплаты педагогов"}]
    assert rm.big_upcoming(ops, "08.2026") == []
