import uuid from datetime import date from unittest.mock import patch from innercontext.models import Routine, SkinConditionSnapshot from innercontext.models.enums import BarrierState, OverallSkinState, PartOfDay # --------------------------------------------------------------------------- # Routines # --------------------------------------------------------------------------- def test_create_routine_minimal(client): r = client.post( "/routines", json={"routine_date": "2026-02-26", "part_of_day": "am"} ) assert r.status_code == 201 data = r.json() assert "id" in data assert data["routine_date"] == "2026-02-26" assert data["part_of_day"] == "am" def test_create_routine_invalid_part_of_day(client): r = client.post( "/routines", json={"routine_date": "2026-02-26", "part_of_day": "noon"} ) assert r.status_code == 422 def test_list_routines_empty(client): r = client.get("/routines") assert r.status_code == 200 assert r.json() == [] def test_list_filter_date_range(client): client.post("/routines", json={"routine_date": "2026-01-01", "part_of_day": "am"}) client.post("/routines", json={"routine_date": "2026-06-01", "part_of_day": "am"}) r = client.get("/routines?from_date=2026-05-01&to_date=2026-12-31") assert r.status_code == 200 data = r.json() assert len(data) == 1 assert data[0]["routine_date"] == "2026-06-01" def test_list_filter_part_of_day(client): client.post("/routines", json={"routine_date": "2026-02-01", "part_of_day": "am"}) client.post("/routines", json={"routine_date": "2026-02-02", "part_of_day": "pm"}) r = client.get("/routines?part_of_day=pm") assert r.status_code == 200 data = r.json() assert len(data) == 1 assert data[0]["part_of_day"] == "pm" def test_get_routine(client, created_routine): rid = created_routine["id"] r = client.get(f"/routines/{rid}") assert r.status_code == 200 assert r.json()["id"] == rid def test_get_routine_not_found(client): r = client.get(f"/routines/{uuid.uuid4()}") assert r.status_code == 404 def test_update_routine_notes(client, created_routine): rid = created_routine["id"] r = client.patch(f"/routines/{rid}", json={"notes": "Felt great today"}) assert r.status_code == 200 assert r.json()["notes"] == "Felt great today" def test_update_routine_not_found(client): r = client.patch(f"/routines/{uuid.uuid4()}", json={"notes": "x"}) assert r.status_code == 404 def test_delete_routine(client, created_routine): rid = created_routine["id"] r = client.delete(f"/routines/{rid}") assert r.status_code == 204 r2 = client.get(f"/routines/{rid}") assert r2.status_code == 404 # --------------------------------------------------------------------------- # RoutineStep # --------------------------------------------------------------------------- def test_add_step_action_only(client, created_routine): rid = created_routine["id"] r = client.post( f"/routines/{rid}/steps", json={"order_index": 0, "action_notes": "Cleanse face gently"}, ) assert r.status_code == 201 data = r.json() assert data["order_index"] == 0 assert data["action_notes"] == "Cleanse face gently" assert data["product_id"] is None def test_add_step_with_product(client, created_routine, created_product): rid = created_routine["id"] pid = created_product["id"] r = client.post( f"/routines/{rid}/steps", json={"order_index": 1, "product_id": pid}, ) assert r.status_code == 201 data = r.json() assert data["product_id"] == pid assert data["order_index"] == 1 def test_add_step_routine_not_found(client): r = client.post( f"/routines/{uuid.uuid4()}/steps", json={"order_index": 0}, ) assert r.status_code == 404 def test_update_step(client, created_routine): rid = created_routine["id"] r = client.post( f"/routines/{rid}/steps", json={"order_index": 0, "action_notes": "Initial"}, ) step_id = r.json()["id"] r2 = client.patch(f"/routines/steps/{step_id}", json={"action_notes": "Updated"}) assert r2.status_code == 200 assert r2.json()["action_notes"] == "Updated" def test_update_step_not_found(client): r = client.patch(f"/routines/steps/{uuid.uuid4()}", json={"action_notes": "x"}) assert r.status_code == 404 def test_delete_step(client, created_routine): rid = created_routine["id"] r = client.post( f"/routines/{rid}/steps", json={"order_index": 0}, ) step_id = r.json()["id"] r2 = client.delete(f"/routines/steps/{step_id}") assert r2.status_code == 204 def test_delete_step_not_found(client): r = client.delete(f"/routines/steps/{uuid.uuid4()}") assert r.status_code == 404 # --------------------------------------------------------------------------- # GroomingSchedule # --------------------------------------------------------------------------- def test_list_grooming_schedule_empty(client): r = client.get("/routines/grooming-schedule") assert r.status_code == 200 assert r.json() == [] def test_create_grooming_schedule(client): r = client.post( "/routines/grooming-schedule", json={"day_of_week": 1, "action": "shaving_razor"}, ) assert r.status_code == 201 data = r.json() assert data["day_of_week"] == 1 assert data["action"] == "shaving_razor" def test_list_grooming_schedule_returns_entry(client): client.post( "/routines/grooming-schedule", json={"day_of_week": 3, "action": "dermarolling"}, ) r = client.get("/routines/grooming-schedule") assert r.status_code == 200 assert len(r.json()) >= 1 def test_update_grooming_schedule(client): r = client.post( "/routines/grooming-schedule", json={"day_of_week": 0, "action": "shaving_oneblade"}, ) entry_id = r.json()["id"] r2 = client.patch( f"/routines/grooming-schedule/{entry_id}", json={"notes": "Use cold water"}, ) assert r2.status_code == 200 assert r2.json()["notes"] == "Use cold water" def test_delete_grooming_schedule(client): r = client.post( "/routines/grooming-schedule", json={"day_of_week": 5, "action": "shaving_razor"}, ) entry_id = r.json()["id"] r2 = client.delete(f"/routines/grooming-schedule/{entry_id}") assert r2.status_code == 204 def test_delete_grooming_schedule_not_found(client): r = client.delete(f"/routines/grooming-schedule/{uuid.uuid4()}") assert r.status_code == 404 def test_suggest_routine(client, session, current_user): with patch( "innercontext.api.routines.call_gemini_with_function_tools" ) as mock_gemini: session.add( SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=date(2026, 2, 22), overall_state=OverallSkinState.GOOD, hydration_level=4, barrier_state=BarrierState.INTACT, priorities=["hydration"], ) ) session.commit() # Mock the Gemini response mock_response = type( "Response", (), { "text": '{"steps": [{"product_id": null, "action_type": "shaving_razor"}], "reasoning": "because"}' }, ) mock_gemini.return_value = (mock_response, None) r = client.post( "/routines/suggest", json={ "routine_date": "2026-03-03", "part_of_day": "am", "notes": "Testing", "include_minoxidil_beard": True, }, ) assert r.status_code == 200 data = r.json() assert len(data["steps"]) == 1 assert data["steps"][0]["action_type"] == "shaving_razor" assert data["reasoning"] == "because" kwargs = mock_gemini.call_args.kwargs assert "USER PROFILE:" in kwargs["contents"] assert "UPCOMING GROOMING (next 7 days):" in kwargs["contents"] assert "snapshot from 2026-02-22" in kwargs["contents"] assert "RECENT ROUTINES: none" in kwargs["contents"] assert "function_handlers" in kwargs assert "get_product_details" in kwargs["function_handlers"] def test_suggest_batch(client, session, current_user): with patch("innercontext.api.routines.call_gemini") as mock_gemini: session.add( Routine( id=uuid.uuid4(), user_id=current_user.user_id, routine_date=date(2026, 2, 27), part_of_day=PartOfDay.PM, ) ) session.add( SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=date(2026, 2, 20), overall_state=OverallSkinState.GOOD, hydration_level=4, barrier_state=BarrierState.INTACT, ) ) session.commit() # Mock the Gemini response mock_response = type( "Response", (), { "text": '{"days": [{"date": "2026-03-03", "am_steps": [], "pm_steps": [], "reasoning": "none"}], "overall_reasoning": "batch test"}' }, ) mock_gemini.return_value = (mock_response, None) r = client.post( "/routines/suggest-batch", json={ "from_date": "2026-03-03", "to_date": "2026-03-04", "minimize_products": True, }, ) assert r.status_code == 200 data = r.json() assert len(data["days"]) == 1 assert data["days"][0]["date"] == "2026-03-03" assert data["overall_reasoning"] == "batch test" kwargs = mock_gemini.call_args.kwargs assert "USER PROFILE:" in kwargs["contents"] assert "2026-02-27 PM:" in kwargs["contents"] assert "snapshot from 2026-02-20" in kwargs["contents"] def test_suggest_batch_invalid_date_range(client): r = client.post( "/routines/suggest-batch", json={"from_date": "2026-03-04", "to_date": "2026-03-03"}, ) assert r.status_code == 400 def test_suggest_batch_too_long(client): r = client.post( "/routines/suggest-batch", json={"from_date": "2026-03-01", "to_date": "2026-03-20"}, ) assert r.status_code == 400