from datetime import date from types import SimpleNamespace from app.crud.subject import should_generate_visits_after_subject_update from app.crud.visit import build_visit_schedule_dates, sort_visits_for_display from app.schemas.study import StudyUpdate def test_build_visit_schedule_dates_uses_per_visit_windows(): baseline_date = date(2026, 5, 8) visits = build_visit_schedule_dates( [ { "visit_code": "基线访视", "baseline_offset_days": 0, "window_before_days": 0, "window_after_days": 0, }, { "visit_code": "V1", "baseline_offset_days": 7, "window_before_days": 2, "window_after_days": 2, }, { "visit_code": "V2", "baseline_offset_days": 15, "window_before_days": 2, "window_after_days": 2, }, { "visit_code": "V3", "baseline_offset_days": 23, "window_before_days": 3, "window_after_days": 3, }, ], baseline_date, ) assert [ (visit["visit_code"], visit["baseline_offset_days"], visit["planned_date"], visit["window_start"], visit["window_end"]) for visit in visits ] == [ ("基线访视", 0, date(2026, 5, 8), date(2026, 5, 8), date(2026, 5, 8)), ("V1", 7, date(2026, 5, 15), date(2026, 5, 13), date(2026, 5, 17)), ("V2", 15, date(2026, 5, 23), date(2026, 5, 21), date(2026, 5, 25)), ("V3", 23, date(2026, 5, 31), date(2026, 5, 28), date(2026, 6, 3)), ] def test_build_visit_schedule_dates_keeps_visit_structure_without_baseline_date(): visits = build_visit_schedule_dates( [ { "visit_code": "基线访视", "baseline_offset_days": 0, "window_before_days": 0, "window_after_days": 0, }, { "visit_code": "V1", "baseline_offset_days": 7, "window_before_days": 2, "window_after_days": 2, }, ], None, ) assert [ (visit["visit_code"], visit["planned_date"], visit["window_start"], visit["window_end"]) for visit in visits ] == [ ("基线访视", None, None, None), ("V1", None, None, None), ] def test_study_update_allows_multiple_visits_with_same_baseline_offset(): payload = StudyUpdate( visit_schedule=[ { "visit_code": "筛选访视", "baseline_offset_days": 0, "window_before_days": 0, "window_after_days": 0, }, { "visit_code": "基线访视", "baseline_offset_days": 0, "window_before_days": 0, "window_after_days": 0, }, ] ) assert [item.visit_code for item in payload.visit_schedule or []] == ["筛选访视", "基线访视"] def test_sort_visits_for_display_follows_configured_visit_order_without_dates(): visits = [ SimpleNamespace(visit_code="V1", planned_date=None), SimpleNamespace(visit_code="V2", planned_date=None), SimpleNamespace(visit_code="基线访视", planned_date=None), ] visit_schedule = [ {"visit_code": "基线访视"}, {"visit_code": "V1"}, {"visit_code": "V2"}, ] assert [visit.visit_code for visit in sort_visits_for_display(visits, visit_schedule)] == ["基线访视", "V1", "V2"] def test_sort_visits_for_display_does_not_infer_business_order(): visits = [ SimpleNamespace(visit_code="基线访视", planned_date=None), SimpleNamespace(visit_code="V1", planned_date=None), SimpleNamespace(visit_code="V2", planned_date=None), ] visit_schedule = [ {"visit_code": "V1"}, {"visit_code": "V2"}, {"visit_code": "基线访视"}, ] assert [visit.visit_code for visit in sort_visits_for_display(visits, visit_schedule)] == ["V1", "V2", "基线访视"] def test_should_generate_visits_when_baseline_date_is_set_or_changed(): assert should_generate_visits_after_subject_update( previous_baseline_date=None, next_baseline_date=date(2026, 5, 8), ) assert should_generate_visits_after_subject_update( previous_baseline_date=date(2026, 5, 8), next_baseline_date=date(2026, 5, 9), ) assert not should_generate_visits_after_subject_update( previous_baseline_date=None, next_baseline_date=None, )