"""Smoke-test the ORM schema — every table must round-trip a row.""" from datetime import date from decimal import Decimal from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from fire_planner.db import ( AccountSnapshot, LifeEvent, McPath, McRun, ProjectionYearly, RetirementGoal, Scenario, ScenarioSummary, ) async def test_account_snapshot_roundtrip(session: AsyncSession) -> None: snap = AccountSnapshot( external_id="wealthfolio:account-1:2026-04-25", snapshot_date=date(2026, 4, 25), account_id="account-1", account_name="ISA", account_type="ISA", currency="GBP", market_value=Decimal("123456.78"), market_value_gbp=Decimal("123456.78"), ) session.add(snap) await session.commit() result = await session.execute(select(AccountSnapshot)) rows = result.scalars().all() assert len(rows) == 1 assert rows[0].external_id == "wealthfolio:account-1:2026-04-25" async def test_scenario_roundtrip(session: AsyncSession) -> None: scen = Scenario( external_id="cyprus-vpw-leave-y3-glide-rising", jurisdiction="cyprus", strategy="vpw", leave_uk_year=3, glide_path="rising", spending_gbp=Decimal("100000"), nw_seed_gbp=Decimal("1000000"), savings_per_year_gbp=Decimal("100000"), config_json={"horizon_years": 60}, ) session.add(scen) await session.commit() result = await session.execute(select(Scenario)) rows = result.scalars().all() assert len(rows) == 1 assert rows[0].jurisdiction == "cyprus" async def test_mc_run_roundtrip(session: AsyncSession) -> None: run = McRun( scenario_id=1, n_paths=10000, seed=42, success_rate=Decimal("0.9412"), p10_ending_gbp=Decimal("250000"), p50_ending_gbp=Decimal("3500000"), p90_ending_gbp=Decimal("12000000"), median_lifetime_tax_gbp=Decimal("750000"), elapsed_seconds=Decimal("42.351"), ) session.add(run) await session.commit() result = await session.execute(select(McRun)) rows = result.scalars().all() assert len(rows) == 1 assert rows[0].n_paths == 10000 async def test_remaining_tables_smoke(session: AsyncSession) -> None: session.add( McPath(mc_run_id=1, path_idx=0, bucket="median", year_idx=0, portfolio_gbp=Decimal("1000000"), withdrawal_gbp=Decimal("100000"), tax_paid_gbp=Decimal("0"), real_portfolio_gbp=Decimal("1000000"))) session.add( ProjectionYearly(mc_run_id=1, year_idx=0, p10_portfolio_gbp=Decimal("800000"), p25_portfolio_gbp=Decimal("900000"), p50_portfolio_gbp=Decimal("1000000"), p75_portfolio_gbp=Decimal("1100000"), p90_portfolio_gbp=Decimal("1200000"), p50_withdrawal_gbp=Decimal("100000"), p50_tax_gbp=Decimal("0"), survival_rate=Decimal("1"))) session.add( ScenarioSummary(scenario_id=1, mc_run_id=1, jurisdiction="uk", strategy="trinity", leave_uk_year=0, glide_path="static", spending_gbp=Decimal("100000"), success_rate=Decimal("0.95"), p10_ending_gbp=Decimal("200000"), p50_ending_gbp=Decimal("3000000"), p90_ending_gbp=Decimal("10000000"), median_lifetime_tax_gbp=Decimal("800000"))) await session.commit() async def test_user_scenario_with_clone(session: AsyncSession) -> None: """User-defined scenarios can declare a parent (cloned-from) scenario.""" base = Scenario( external_id="cyprus-vpw-leave-y3-glide-rising", kind="cartesian", jurisdiction="cyprus", strategy="vpw", leave_uk_year=3, glide_path="rising", spending_gbp=Decimal("100000"), nw_seed_gbp=Decimal("1500000"), savings_per_year_gbp=Decimal("0"), config_json={"horizon_years": 60}, ) session.add(base) await session.commit() user = Scenario( external_id="user-aggressive-fire", kind="user", name="Aggressive FIRE", description="Same as Cyprus baseline but spending £80k", parent_scenario_id=base.id, jurisdiction="cyprus", strategy="vpw", leave_uk_year=3, glide_path="rising", spending_gbp=Decimal("80000"), nw_seed_gbp=Decimal("1500000"), savings_per_year_gbp=Decimal("0"), config_json={"horizon_years": 60, "floor": 60000}, ) session.add(user) await session.commit() rows = (await session.execute(select(Scenario).where(Scenario.kind == "user"))).scalars().all() assert len(rows) == 1 assert rows[0].name == "Aggressive FIRE" assert rows[0].parent_scenario_id == base.id async def test_life_event_roundtrip(session: AsyncSession) -> None: ev = LifeEvent( scenario_id=1, kind="retirement", name="Retire at 50", year_start=15, year_end=15, delta_gbp_per_year=Decimal("0"), ) childcare = LifeEvent( scenario_id=1, kind="expense_range", name="Childcare", year_start=2, year_end=20, delta_gbp_per_year=Decimal("-12000"), ) inheritance = LifeEvent( scenario_id=1, kind="one_time_income", name="Inheritance", year_start=22, year_end=22, one_time_amount_gbp=Decimal("250000"), ) session.add_all([ev, childcare, inheritance]) await session.commit() rows = (await session.execute( select(LifeEvent).where(LifeEvent.scenario_id == 1))).scalars().all() assert len(rows) == 3 by_kind = {r.kind: r for r in rows} assert by_kind["retirement"].year_start == 15 assert by_kind["expense_range"].delta_gbp_per_year == Decimal("-12000") assert by_kind["one_time_income"].one_time_amount_gbp == Decimal("250000") async def test_life_event_default_enabled(session: AsyncSession) -> None: ev = LifeEvent(scenario_id=1, kind="retirement", name="Retire", year_start=10) session.add(ev) await session.commit() fetched = (await session.execute(select(LifeEvent))).scalars().first() assert fetched is not None assert fetched.enabled is True async def test_retirement_goal_roundtrip(session: AsyncSession) -> None: target_nw = RetirementGoal( scenario_id=1, kind="target_nw", name="≥ £2M at age 50", target_amount_gbp=Decimal("2000000"), target_year=15, comparator=">=", success_threshold=Decimal("0.90"), ) never_run_out = RetirementGoal( scenario_id=1, kind="never_run_out", name="Last to age 95", target_year=65, comparator=">=", success_threshold=Decimal("0.95"), ) inheritance = RetirementGoal( scenario_id=1, kind="inheritance", name="Leave £500k", target_amount_gbp=Decimal("500000"), target_year=65, comparator=">=", success_threshold=Decimal("0.50"), ) session.add_all([target_nw, never_run_out, inheritance]) await session.commit() rows = (await session.execute(select(RetirementGoal))).scalars().all() assert len(rows) == 3 kinds = {r.kind for r in rows} assert kinds == {"target_nw", "never_run_out", "inheritance"} async def test_scenario_kind_default_cartesian(session: AsyncSession) -> None: """Existing Cartesian scenarios omit `kind` — default applies.""" scen = Scenario( external_id="uk-trinity-leave-y0-glide-static", jurisdiction="uk", strategy="trinity", leave_uk_year=0, glide_path="static", spending_gbp=Decimal("60000"), nw_seed_gbp=Decimal("1500000"), savings_per_year_gbp=Decimal("0"), config_json={}, ) session.add(scen) await session.commit() fetched = (await session.execute(select(Scenario))).scalars().first() assert fetched is not None assert fetched.kind == "cartesian"