import uuid from datetime import date, timedelta from sqlmodel import Session from innercontext.api.llm_context import build_products_context_summary_list from innercontext.api.routines import ( _build_day_context, _build_grooming_context, _build_objectives_context, _build_recent_history, _build_skin_context, _build_upcoming_grooming_context, _contains_minoxidil_text, _ev, _extract_active_names, _extract_requested_product_ids, _filter_products_by_interval, _get_available_products, _get_latest_skin_snapshot_within_days, _get_recent_skin_snapshot, _is_minoxidil_product, build_product_details_tool_handler, ) from innercontext.models import ( GroomingSchedule, Product, Routine, RoutineStep, SkinConditionSnapshot, ) from innercontext.models.enums import BarrierState, OverallSkinState, SkinConcern def test_contains_minoxidil_text(): assert _contains_minoxidil_text(None) is False assert _contains_minoxidil_text("") is False assert _contains_minoxidil_text("some random text") is False assert _contains_minoxidil_text("contains MINOXIDIL here") is True assert _contains_minoxidil_text("minoksydyl 5%") is True def test_is_minoxidil_product(): # Setup product p = Product(id=uuid.uuid4(), name="Test", brand="Brand", is_medication=True) assert _is_minoxidil_product(p) is False p.name = "Minoxidil 5%" assert _is_minoxidil_product(p) is True p.name = "Test" p.brand = "Brand with minoksydyl" assert _is_minoxidil_product(p) is True p.brand = "Brand" p.line_name = "Minoxidil Line" assert _is_minoxidil_product(p) is True p.line_name = None p.inci = ["water", "minoxidil"] assert _is_minoxidil_product(p) is True p.inci = None p.actives = [{"name": "minoxidil", "strength": "5%"}] assert _is_minoxidil_product(p) is True # As Pydantic model representation isn't exactly a dict in db sometimes, we just test dict p.actives = [{"name": "Retinol", "strength": "1%"}] assert _is_minoxidil_product(p) is False def test_ev(): class DummyEnum: value = "dummy" assert _ev(None) == "" assert _ev(DummyEnum()) == "dummy" assert _ev("string") == "string" def test_build_skin_context(session: Session, current_user): # Empty reference_date = date(2026, 3, 10) assert ( _build_skin_context( session, target_user_id=current_user.user_id, reference_date=reference_date, ) == "SKIN CONDITION: no data\n" ) # With data snap = SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=reference_date, overall_state=OverallSkinState.GOOD, hydration_level=4, barrier_state=BarrierState.INTACT, active_concerns=[SkinConcern.ACNE, SkinConcern.DEHYDRATION], priorities=["hydration"], notes="Feeling good", ) session.add(snap) session.commit() ctx = _build_skin_context( session, target_user_id=current_user.user_id, reference_date=reference_date, ) assert "SKIN CONDITION (snapshot from" in ctx assert "Overall state: good" in ctx assert "Hydration: 4/5" in ctx assert "Barrier: intact" in ctx assert "Active concerns: acne, dehydration" in ctx assert "Priorities: hydration" in ctx assert "Notes: Feeling good" in ctx def test_build_skin_context_falls_back_to_recent_snapshot_within_14_days( session: Session, current_user, ): reference_date = date(2026, 3, 20) snap = SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=reference_date - timedelta(days=10), overall_state=OverallSkinState.FAIR, hydration_level=3, barrier_state=BarrierState.COMPROMISED, active_concerns=[SkinConcern.REDNESS], priorities=["barrier"], ) session.add(snap) session.commit() ctx = _build_skin_context( session, target_user_id=current_user.user_id, reference_date=reference_date, ) assert f"snapshot from {reference_date - timedelta(days=10)}" in ctx assert "Barrier: compromised" in ctx def test_build_skin_context_ignores_snapshot_older_than_14_days( session: Session, current_user ): reference_date = date(2026, 3, 20) snap = SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=reference_date - timedelta(days=15), overall_state=OverallSkinState.FAIR, hydration_level=3, barrier_state=BarrierState.INTACT, ) session.add(snap) session.commit() assert ( _build_skin_context( session, target_user_id=current_user.user_id, reference_date=reference_date, ) == "SKIN CONDITION: no data\n" ) def test_get_recent_skin_snapshot_prefers_window_match(session: Session, current_user): reference_date = date(2026, 3, 20) older = SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=reference_date - timedelta(days=10), overall_state=OverallSkinState.POOR, hydration_level=2, barrier_state=BarrierState.COMPROMISED, ) newer = SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=reference_date - timedelta(days=2), overall_state=OverallSkinState.GOOD, hydration_level=4, barrier_state=BarrierState.INTACT, ) session.add_all([older, newer]) session.commit() snapshot = _get_recent_skin_snapshot( session, target_user_id=current_user.user_id, reference_date=reference_date, ) assert snapshot is not None assert snapshot.id == newer.id def test_get_latest_skin_snapshot_within_days_uses_latest_within_14_days( session: Session, current_user, ): reference_date = date(2026, 3, 20) older = SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=reference_date - timedelta(days=10), overall_state=OverallSkinState.POOR, hydration_level=2, barrier_state=BarrierState.COMPROMISED, ) newer = SkinConditionSnapshot( id=uuid.uuid4(), user_id=current_user.user_id, snapshot_date=reference_date - timedelta(days=2), overall_state=OverallSkinState.GOOD, hydration_level=4, barrier_state=BarrierState.INTACT, ) session.add_all([older, newer]) session.commit() snapshot = _get_latest_skin_snapshot_within_days( session, target_user_id=current_user.user_id, reference_date=reference_date, ) assert snapshot is not None assert snapshot.id == newer.id def test_build_grooming_context(session: Session, current_user): assert ( _build_grooming_context(session, target_user_id=current_user.user_id) == "GROOMING SCHEDULE: none\n" ) sch = GroomingSchedule( id=uuid.uuid4(), user_id=current_user.user_id, day_of_week=0, action="shaving_oneblade", notes="Morning", ) session.add(sch) session.commit() ctx = _build_grooming_context(session, target_user_id=current_user.user_id) assert "GROOMING SCHEDULE:" in ctx assert "poniedziałek: shaving_oneblade (Morning)" in ctx # Test weekdays filter ctx2 = _build_grooming_context( session, target_user_id=current_user.user_id, weekdays=[1], ) # not monday assert "(no entries for specified days)" in ctx2 def test_build_upcoming_grooming_context(session: Session, current_user): assert ( _build_upcoming_grooming_context( session, target_user_id=current_user.user_id, start_date=date(2026, 3, 2), days=7, ) == "UPCOMING GROOMING (next 7 days): none\n" ) monday = GroomingSchedule( id=uuid.uuid4(), user_id=current_user.user_id, day_of_week=0, action="shaving_oneblade", notes="Morning", ) wednesday = GroomingSchedule( id=uuid.uuid4(), user_id=current_user.user_id, day_of_week=2, action="dermarolling", ) session.add_all([monday, wednesday]) session.commit() ctx = _build_upcoming_grooming_context( session, target_user_id=current_user.user_id, start_date=date(2026, 3, 2), days=7, ) assert "UPCOMING GROOMING (next 7 days):" in ctx assert "dzisiaj (2026-03-02, poniedziałek): shaving_oneblade (Morning)" in ctx assert "za 2 dni (2026-03-04, środa): dermarolling" in ctx def test_build_recent_history(session: Session, current_user): reference_date = date(2026, 3, 10) assert ( _build_recent_history( session, target_user_id=current_user.user_id, reference_date=reference_date, ) == "RECENT ROUTINES: none\n" ) r = Routine( id=uuid.uuid4(), user_id=current_user.user_id, routine_date=reference_date, part_of_day="am", ) session.add(r) p = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Cleanser", category="cleanser", brand="Test", recommended_time="both", leave_on=False, product_effect_profile={}, ) session.add(p) session.commit() s1 = RoutineStep( id=uuid.uuid4(), user_id=current_user.user_id, routine_id=r.id, order_index=1, product_id=p.id, ) s2 = RoutineStep( id=uuid.uuid4(), user_id=current_user.user_id, routine_id=r.id, order_index=2, action_type="shaving_razor", ) # Step with non-existent product s3 = RoutineStep( id=uuid.uuid4(), user_id=current_user.user_id, routine_id=r.id, order_index=3, product_id=uuid.uuid4(), ) session.add_all([s1, s2, s3]) session.commit() ctx = _build_recent_history( session, target_user_id=current_user.user_id, reference_date=reference_date, ) assert "RECENT ROUTINES:" in ctx assert "AM:" in ctx assert "cleanser [" in ctx assert "action: shaving_razor" in ctx assert "unknown [" in ctx def test_build_recent_history_uses_reference_window(session: Session, current_user): reference_date = date(2026, 3, 10) recent = Routine( id=uuid.uuid4(), user_id=current_user.user_id, routine_date=reference_date - timedelta(days=3), part_of_day="pm", ) old = Routine( id=uuid.uuid4(), user_id=current_user.user_id, routine_date=reference_date - timedelta(days=6), part_of_day="am", ) session.add_all([recent, old]) session.commit() ctx = _build_recent_history( session, target_user_id=current_user.user_id, reference_date=reference_date, ) assert str(recent.routine_date) in ctx assert str(old.routine_date) not in ctx def test_build_recent_history_excludes_future_routines(session: Session, current_user): reference_date = date(2026, 3, 10) future = Routine( id=uuid.uuid4(), user_id=current_user.user_id, routine_date=reference_date + timedelta(days=1), part_of_day="am", ) session.add(future) session.commit() assert ( _build_recent_history( session, target_user_id=current_user.user_id, reference_date=reference_date, ) == "RECENT ROUTINES: none\n" ) def test_build_products_context_summary_list(session: Session, current_user): p1 = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Regaine Minoxidil", category="serum", is_medication=True, brand="J&J", recommended_time="both", leave_on=True, product_effect_profile={}, user_id=current_user.user_id, ) p2 = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Sunscreen", category="spf", brand="Test", leave_on=True, recommended_time="am", pao_months=6, product_effect_profile={"hydration_immediate": 2, "exfoliation_strength": 0}, context_rules={"safe_after_shaving": False}, min_interval_hours=12, max_frequency_per_week=7, user_id=current_user.user_id, ) session.add_all([p1, p2]) session.commit() products_am = _get_available_products( session, current_user=current_user, time_filter="am", ) ctx = build_products_context_summary_list(products_am, {p2.id}) assert "Regaine Minoxidil" in ctx assert "Sunscreen" in ctx assert "[✓]" in ctx assert "hydration=2" in ctx assert "!post_shave" in ctx def test_build_objectives_context(): assert _build_objectives_context(False) == "" assert "improve beard" in _build_objectives_context(True) def test_build_day_context(): assert _build_day_context(None) == "" assert "Leaving home: yes" in _build_day_context(True) assert "Leaving home: no" in _build_day_context(False) def test_get_available_products_respects_filters(session: Session, current_user): regular_med = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Tretinoin", category="serum", is_medication=True, brand="Test", recommended_time="pm", leave_on=True, product_effect_profile={}, user_id=current_user.user_id, ) minoxidil_med = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Minoxidil 5%", category="serum", is_medication=True, brand="Test", recommended_time="both", leave_on=True, product_effect_profile={}, user_id=current_user.user_id, ) am_product = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="AM SPF", category="spf", brand="Test", recommended_time="am", leave_on=True, product_effect_profile={}, user_id=current_user.user_id, ) pm_product = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="PM Cream", category="moisturizer", brand="Test", recommended_time="pm", leave_on=True, product_effect_profile={}, user_id=current_user.user_id, ) session.add_all([regular_med, minoxidil_med, am_product, pm_product]) session.commit() am_available = _get_available_products( session, current_user=current_user, time_filter="am", ) am_names = {p.name for p in am_available} assert "Tretinoin" not in am_names assert "Minoxidil 5%" in am_names assert "AM SPF" in am_names assert "PM Cream" not in am_names def test_build_product_details_tool_handler_returns_only_available_ids( session: Session, ): available = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Available", category="serum", brand="Test", recommended_time="both", leave_on=True, inci=["Water", "Niacinamide"], product_effect_profile={}, ) unavailable = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Unavailable", category="serum", brand="Test", recommended_time="both", leave_on=True, inci=["Water", "Retinol"], product_effect_profile={}, ) handler = build_product_details_tool_handler([available]) payload = handler( { "product_ids": [ str(available.id), str(unavailable.id), str(available.id), 123, ] } ) assert "products" in payload products = payload["products"] assert len(products) == 1 assert products[0]["id"] == available.short_id assert products[0]["name"] == "Available" assert "actives" in products[0] assert "safety" in products[0] def test_extract_requested_product_ids_dedupes_and_limits(): ids = _extract_requested_product_ids( { "product_ids": [ "id-1", "id-2", "id-1", 3, "id-3", "id-4", ] }, max_ids=3, ) assert ids == ["id-1", "id-2", "id-3"] def test_extract_active_names_uses_compact_distinct_names(session: Session): p = Product( id=uuid.uuid4(), name="Test", category="serum", brand="Test", recommended_time="both", leave_on=True, actives=[ {"name": "Niacinamide", "percent": 10}, {"name": "Niacinamide", "percent": 5}, {"name": "Zinc PCA", "percent": 1}, ], product_effect_profile={}, ) names = _extract_active_names(p) assert names == ["Niacinamide", "Zinc PCA"] def test_get_available_products_excludes_minoxidil_when_flag_false( session: Session, current_user, ): minoxidil = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Minoxidil 5%", category="hair_treatment", is_medication=True, brand="Test", recommended_time="both", leave_on=True, product_effect_profile={}, user_id=current_user.user_id, ) regular = Product( id=uuid.uuid4(), short_id=str(uuid.uuid4())[:8], name="Cleanser", category="cleanser", brand="Test", recommended_time="both", leave_on=False, product_effect_profile={}, user_id=current_user.user_id, ) session.add_all([minoxidil, regular]) session.commit() # With flag True (default) - minoxidil included products = _get_available_products( session, current_user=current_user, include_minoxidil=True, ) names = {p.name for p in products} assert "Minoxidil 5%" in names assert "Cleanser" in names # With flag False - minoxidil excluded products = _get_available_products( session, current_user=current_user, include_minoxidil=False, ) names = {p.name for p in products} assert "Minoxidil 5%" not in names assert "Cleanser" in names def test_filter_products_by_interval(): today = date.today() p_no_interval = Product( id=uuid.uuid4(), name="No Interval", category="serum", brand="Test", recommended_time="both", leave_on=True, product_effect_profile={}, ) p_interval_72 = Product( id=uuid.uuid4(), name="Retinol", category="serum", brand="Test", recommended_time="pm", leave_on=True, min_interval_hours=72, product_effect_profile={}, ) p_interval_48 = Product( id=uuid.uuid4(), name="AHA", category="exfoliant", brand="Test", recommended_time="pm", leave_on=True, min_interval_hours=48, product_effect_profile={}, ) last_used = { str(p_interval_72.id): today - timedelta(days=1), # used yesterday -> need 3 days str(p_interval_48.id): today - timedelta(days=3), # used 3 days ago -> 48h ok } products = [p_no_interval, p_interval_72, p_interval_48] result = _filter_products_by_interval(products, today, last_used) result_names = {p.name for p in result} assert "No Interval" in result_names # always included assert "Retinol" not in result_names # used 1 day ago, needs 3 -> blocked assert "AHA" in result_names # used 3 days ago, needs 2 -> ok def test_filter_products_by_interval_never_used_passes(): today = date.today() p = Product( id=uuid.uuid4(), name="Retinol", category="serum", brand="Test", recommended_time="pm", leave_on=True, min_interval_hours=72, product_effect_profile={}, ) # no last_used entry -> should pass result = _filter_products_by_interval([p], today, {}) assert len(result) == 1 def test_product_details_tool_handler_returns_product_payloads(session: Session): p = Product( id=uuid.uuid4(), name="Detail Product", category="serum", brand="Test", recommended_time="both", leave_on=True, actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}], context_rules={"safe_after_shaving": True}, product_effect_profile={}, ) ids_payload = {"product_ids": [str(p.id)]} details_out = build_product_details_tool_handler([p])(ids_payload) assert details_out["products"][0]["actives"][0]["name"] == "Niacinamide" assert "context_rules" in details_out["products"][0] assert details_out["products"][0]["last_used_on"] is None