"""Tests for ledgerly.models (sanitizers, Month, read models).""" from dataclasses import FrozenInstanceError from datetime import date import pytest from ledgerly.errors import ValidationError from ledgerly.models import ( BudgetLine, Category, Month, MonthSummary, clean_name, clean_note, parse_date, validate_kind, ) from ledgerly.money import Money def test_clean_name_ok(): assert clean_name(" Rent ") == "Rent" def test_clean_name_rejects_blank(): with pytest.raises(ValidationError): clean_name(" ") with pytest.raises(ValidationError): clean_name(None) def test_clean_name_rejects_long(): with pytest.raises(ValidationError, match="50"): clean_name("x" * 51) def test_clean_note(): assert clean_note(None) is None assert clean_note(" ") is None assert clean_note("a note") == "a note" def test_clean_note_rejects_long(): with pytest.raises(ValidationError, match="200"): clean_note("n" * 201) def test_parse_date_ok(): assert parse_date("2026-10-26") == date(2026, 10, 26) def test_parse_date_rejects_bad(): for bad in ("2026-13-01", "2026-02-30", "25-10-01", "not-a-date", 123): with pytest.raises(ValidationError): parse_date(bad) def test_parse_date_rejects_out_of_range(): with pytest.raises(ValidationError): parse_date("1999-12-31") with pytest.raises(ValidationError): parse_date("2101-01-01") def test_validate_kind(): assert validate_kind("income") == "income" with pytest.raises(ValidationError): validate_kind("asset") def test_month_from_str_ok(): month = Month.from_str("2026-10") assert (month.year, month.month) == (2026, 10) assert month.label == "2026-10" assert month.first_day == date(2026, 10, 1) assert month.last_day == date(2026, 10, 31) def test_month_from_str_rejects_bad(): for bad in ("2026", "26-10", "2026-13", "2026-", "2026-1-3", "garbage", 2026): with pytest.raises(ValidationError): Month.from_str(bad) def test_month_constructor_validation(): with pytest.raises(ValidationError): Month(2026, 13) with pytest.raises(ValidationError): Month(1999, 5) def test_month_range_leap_year(): month = Month(year=2024, month=2) assert month.range == ("2024-02-01", "2024-02-29") def test_month_now_is_sane(): month = Month.now() assert 1 <= month.month <= 12 def test_month_summary_net(): summary = MonthSummary(month=Month.now(), income=Money(3000), expenses=Money(3100)) assert summary.net == Money(-100) def test_budget_line_over_and_under(): over = BudgetLine(category="Rent", budget=Money(1000), spent=Money(1200)) assert over.is_over assert over.remaining == Money(-200) under = BudgetLine(category="Rent", budget=Money(1000), spent=Money(800)) assert not under.is_over assert under.remaining == Money(200) def test_entities_are_frozen(): cat = Category(name="Rent", kind="expense") with pytest.raises(FrozenInstanceError): cat.name = "Other"