"""Garden (#118): deterministic growth from activities, daily caps, never shrinks, ignores answers.""" from collections.abc import AsyncIterator from datetime import date, datetime, timedelta import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy import select from app.config import Settings from app.core import clock from app.db import session from app.db.clinical import DailyCheckin, Dose from app.main import app from app.services.garden import compute, load_config from scripts.seed_demo import seed MEM = "sqlite+aiosqlite:///:memory:" NOW = datetime(2026, 10, 5, 14, 0) D = date(2026, 10, 5) def test_points_steps_and_daily_caps() -> None: cfg = load_config() one_diary = compute([("diary", D)], D) assert one_diary.points == cfg.activities["diary"].points # Five diary entries on one day count as one; on five days — five times. assert compute([("diary", D)] * 5, D).points == one_diary.points five_days = compute([("diary", D - timedelta(days=i)) for i in range(5)], D) assert five_days.points == 5 * one_diary.points assert five_days.steps == five_days.points // cfg.step_points assert five_days.points_in_step == five_days.points % cfg.step_points def test_never_shrinks_and_future_ignored() -> None: events = [("meds", D), ("diary", D), ("questionnaire", D)] now = compute(events, D) # A week without any activity takes nothing away. assert compute(events, D + timedelta(days=7)).points == now.points # Rows dated after the (demo) today are not counted yet. assert compute([*events, ("diary", D + timedelta(days=1))], D).points == now.points def test_plants_grow_one_after_another() -> None: cfg = load_config() per_plant = cfg.stages_per_plant g = compute([("questionnaire", D - timedelta(days=i)) for i in range(100)], D) stages = [p.stage for p in g.plants] assert stages == sorted(stages, reverse=True) and stages[0] == per_plant assert sum(stages) == min(g.steps, per_plant * cfg.max_plants) assert compute([], D).title == cfg.titles[0].title @pytest.fixture async def client(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[AsyncClient]: monkeypatch.setattr(clock, "get_settings", lambda: Settings(_env_file=None, demo_clock=True)) clock.set_now(NOW) await session.configure(identity_url=MEM, clinical_url=MEM) async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: yield c await session.configure() async def test_andrey_has_a_living_garden_that_grows_and_ignores_mood(client: AsyncClient) -> None: token = (await seed(12)).andrey_token url = f"/api/v1/me/{token}/garden" before = (await client.get(url)).json() assert before["steps"] >= 4 and before["plants"][0]["stage"] > 0 # Mood is an answer: a bad day and a good day grow the garden the same way. async with session.get_clinical_session() as s: rows = (await s.scalars(select(DailyCheckin).where(DailyCheckin.token == token))).all() for r in rows: r.mood = 1 assert (await client.get(url)).json()["points"] == before["points"] # Taking a dose today adds points. async with session.get_clinical_session() as s: dose = Dose(token=token, prescription_id=1, planned_at=clock.now(), status="taken", answered_at=clock.now()) s.add(dose) after = (await client.get(url)).json() assert after["points"] >= before["points"] assert any(i["activity"] == "diary" for i in after["today"]) async def test_unknown_patient_404(client: AsyncClient) -> None: await seed(0) assert (await client.get("/api/v1/me/nobody/garden")).status_code == 404