from datetime import UTC, date, datetime from math import exp from types import SimpleNamespace from test_app import login from app.extensions import db from app.fitness import effort, history from app.heart_rate import zones from app.models import Activity, User pytest_plugins = ("test_app",) def run(seconds=600, histogram=None): return SimpleNamespace( sport="running", duration_s=seconds, analysis_summary={"intervals": [{"hr_seconds": histogram or {}}]}, ) def test_effort_boundaries_weights_and_coverage(): scored = effort(run(histogram={"120": 200, "140": 200, "160": 200}), [140, 160]) assert scored["score"] == 20 assert scored["coverage"] == 100 assert effort(run(histogram={"140": 479}), [140, 160])["score"] is None assert effort(run(histogram={"140": 480}), [140, 160])["score"] == 16 assert effort(run(histogram={"140": 600}), None)["score"] is None assert effort(run(histogram={"140": 600}), None)["coverage"] == 100 assert effort(run(histogram={"250": 600}), [140, 160])["score"] is None five = effort( run(histogram={"121": 120, "122": 120, "152": 120, "167": 120, "181": 120}), [122, 152, 167, 181], ) assert five["score"] == 30 assert five["zone_minutes"] == [2, 2, 2, 2, 2] def test_automatic_zones_update_on_birthday_manual_overrides(): user = SimpleNamespace( timezone="Australia/Sydney", hr_zones={"mode": "auto", "birthday": "1990-10-22"} ) assert zones(user, date(2026, 10, 21))["max_hr"] == 185 assert zones(user, date(2026, 10, 22))["max_hr"] == 184 user.hr_zones = {"mode": "max", "max_hr": 195, "birthday": "1990-10-22"} assert zones(user, date(2026, 10, 22))["max_hr"] == 195 user.hr_zones = None assert zones(user)["max_hr"] == 190 user.hr_zones = {"mode": "custom", "boundaries": [122, 152, 167, 181]} assert zones(user)["labels"] == ["0–121", "122–151", "152–166", "167–180", "181+"] def test_history_local_days_sum_decay_missing_and_owner_scope(app): with app.app_context(): db.session.delete(db.session.get(Activity, 1)) user = db.session.get(User, 1) user.hr_zones = {"mode": "custom", "boundaries": [140, 160, 180, 200]} for key, owner, hour, histogram in [ ("a", 1, 13, {"140": 600}), ("b", 1, 14, {"140": 600}), ("other", 2, 14, {"160": 600}), ("missing", 1, 15, {}), ]: row = Activity( owner_id=owner, source_key=key, title=key, sport="running", started_at=datetime(2026, 1, 1, hour, tzinfo=UTC), duration_s=600, analysis_summary=run(histogram=histogram).analysis_summary, ) db.session.add(row) db.session.commit() data = history(user, date(2026, 1, 3)) assert len(data["runs"]) == 3 assert data["series"][0]["day"] == "2026-01-02" first, second = data["series"] assert first["load"] is None assert first["missing"] == 1 assert first["fitness"] == round(40 * (1 - exp(-1 / 42)), 2) assert first["fatigue"] == round(40 * (1 - exp(-1 / 7)), 2) assert first["form"] == round(40 * (exp(-1 / 7) - exp(-1 / 42)), 2) assert second["load"] == 0 assert second["fitness"] == round(40 * (1 - exp(-1 / 42)) * exp(-1 / 42), 2) assert second["provisional"] assert history(user, date(2026, 1, 3)) == data user.hr_zones = {"mode": "custom", "boundaries": [160, 180, 200, 220]} recalculated = history(user, date(2026, 1, 3)) assert recalculated["series"][0]["fitness"] == round(20 * (1 - exp(-1 / 42)), 2) assert history(user, date(2026, 1, 1))["series"] == [] def test_fitness_page_private_and_renders_history(app): client = app.test_client() assert client.get("/fitness").status_code == 302 assert client.get("/fitness/data").status_code == 302 login(client) response = client.get("/fitness") assert response.status_code == 200 assert len(client.get("/fitness/data").json["runs"]) == 1 assert b"A quiet morning run" in response.data with app.app_context(): db.session.get(User, 1).hr_bands = [140, 160] db.session.commit() assert b"fitness-chart" in client.get("/fitness").data assert b"Run effort:" in client.get("/activities/1").data def test_zone_settings_validation_persistence_and_privacy(app): client = app.test_client() assert client.post("/training/zones", data={"mode": "auto"}).status_code == 302 login(client) valid = {"mode": "custom", "z2": "122", "z3": "152", "z4": "167", "z5": "181"} assert client.post("/training/zones", data=valid).status_code == 302 assert client.post("/training/zones", data={**valid, "z3": "100"}).status_code == 400 assert client.post("/training/zones", data={"mode": "max", "max_hr": "999"}).status_code == 400 assert ( client.post("/training/zones", data={"mode": "auto", "birthday": "2099-01-01"}).status_code == 400 ) with app.app_context(): assert db.session.get(User, 1).hr_zones["boundaries"] == [122, 152, 167, 181] assert db.session.get(User, 2).hr_zones is None assert b"Manual zone boundaries" in client.get("/settings").data