from datetime import date from types import SimpleNamespace from fastapi import HTTPException from app.api.v1.studies import ( _apply_project_publish_snapshot_to_study, _build_default_setup_config_from_study, _resolve_setup_project_snapshot, _validate_setup_data, ) from app.schemas.study_setup_config import ProjectPublishSnapshot, StudySetupConfigData def test_setup_config_data_keeps_project_info_draft(): draft = StudySetupConfigData.model_validate( { "projectInfo": { "code": "PRJ-002", "name": "草稿项目", "plan_start_date": "2026-06-01", "plan_end_date": "2026-12-31", "planned_enrollment_count": 120, "status": "ACTIVE", "visit_schedule": [ { "visit_code": "V1", "baseline_offset_days": 7, "window_before_days": 1, "window_after_days": 2, } ], } } ) assert draft.projectInfo.code == "PRJ-002" assert draft.projectInfo.name == "草稿项目" assert draft.projectInfo.planned_enrollment_count == 120 assert draft.projectInfo.visit_schedule[0].visit_code == "V1" def test_draft_schema_accepts_incomplete_project_snapshot_rows(): draft = StudySetupConfigData.model_validate( { "projectInfo": { "code": "PRJ-002", "name": "草稿项目", "visit_schedule": [ { "visit_code": "", "baseline_offset_days": 0, "window_before_days": 0, "window_after_days": 0, } ], }, "projectMilestones": [ { "id": "row-1", "name": "", "planDate": "", "startDate": "", "endDate": "", "durationDays": 1, "owner": "", "remark": "", "status": "", } ], "centerConfirm": [ { "id": "row-1", "siteId": "", "siteName": "", "confirmer": "", "confirmStatus": "", "confirmDate": "", "note": "", } ], } ) assert draft.projectInfo.visit_schedule[0].visit_code == "" assert draft.projectMilestones[0].status == "" assert draft.centerConfirm[0].confirmStatus == "" def test_draft_save_allows_incomplete_setup_steps(): draft = StudySetupConfigData.model_validate( { "projectInfo": { "code": "PRJ-002", "name": "草稿项目", "plan_start_date": "2026-05-01", "plan_end_date": "2027-05-31", }, "enrollmentPlan": { "totalTarget": 180, "startDate": "", "endDate": "", "monthlyGoalNote": "", "stageBreakdown": "", }, } ) _validate_setup_data( draft, {}, project_plan_start=date(2026, 5, 1), project_plan_end=date(2027, 5, 31), strict_required=False, ) def test_publish_validation_still_requires_complete_setup_steps(): draft = StudySetupConfigData.model_validate( { "projectInfo": { "code": "PRJ-002", "name": "草稿项目", "plan_start_date": "2026-05-01", "plan_end_date": "2027-05-31", }, "enrollmentPlan": { "totalTarget": 180, "startDate": "", "endDate": "", "monthlyGoalNote": "", "stageBreakdown": "", }, } ) try: _validate_setup_data( draft, {}, project_plan_start=date(2026, 5, 1), project_plan_end=date(2027, 5, 31), ) except HTTPException as exc: assert exc.status_code == 422 else: raise AssertionError("publish validation should reject incomplete setup steps") def test_project_info_is_part_of_setup_config_payload(): draft = StudySetupConfigData.model_validate( { "projectInfo": { "code": "PRJ-VERSION", "name": "版本内项目信息", "project_full_name": "版本内项目全称", "plan_start_date": "2026-06-01", "plan_end_date": "2026-12-31", "planned_site_count": 8, "planned_enrollment_count": 120, "status": "ACTIVE", }, "enrollmentPlan": { "totalTarget": 120, "startDate": "2026-06-01", "endDate": "2026-12-31", "monthlyGoalNote": "", "stageBreakdown": "", }, } ) payload = draft.model_dump(mode="json") assert payload["projectInfo"]["code"] == "PRJ-VERSION" assert payload["projectInfo"]["planned_enrollment_count"] == 120 assert payload["enrollmentPlan"]["totalTarget"] == 120 def test_default_setup_config_includes_project_info_snapshot(): study = SimpleNamespace( code="PRJ-DEFAULT", name="默认项目", project_full_name="默认项目全称", sponsor="申办方", protocol_no="PROTO-DEFAULT", lead_unit="组长单位", principal_investigator="PI", main_pm="PM", research_analysis="III期", research_product="研究药物", control_product="对照药物", indication="适应症", research_population="研究人群", research_design="随机双盲", plan_start_date=date(2026, 6, 1), plan_end_date=date(2026, 12, 31), planned_site_count=8, planned_enrollment_count=120, enrollment_monthly_goal_note="", enrollment_stage_breakdown="", status="ACTIVE", visit_schedule=[ { "visit_code": "V1", "baseline_offset_days": 7, "window_before_days": 1, "window_after_days": 2, } ], ) draft = _build_default_setup_config_from_study(study, []) assert draft.projectInfo.code == "PRJ-DEFAULT" assert draft.projectInfo.name == "默认项目" assert draft.projectInfo.planned_enrollment_count == 120 assert draft.projectInfo.visit_schedule[0].visit_code == "V1" def test_empty_project_info_does_not_fallback_to_study_snapshot(): study = SimpleNamespace( code="PRJ-MASTER", name="主表项目", project_full_name="", sponsor="", protocol_no="", lead_unit="", principal_investigator="", main_pm="", research_analysis="", research_product="", control_product="", indication="", research_population="", research_design="", plan_start_date=None, plan_end_date=None, planned_site_count=None, planned_enrollment_count=None, status="DRAFT", visit_schedule=[], ) draft = StudySetupConfigData() snapshot = _resolve_setup_project_snapshot(draft, study) assert snapshot.code == "" assert snapshot.name == "" def test_apply_project_publish_snapshot_updates_formal_study_fields(): study = SimpleNamespace( code="PRJ-001", name="原项目", project_full_name=None, sponsor=None, protocol_no=None, lead_unit=None, principal_investigator=None, main_pm=None, research_analysis=None, research_product=None, control_product=None, indication=None, research_population=None, research_design=None, plan_start_date=None, plan_end_date=None, planned_site_count=None, planned_enrollment_count=10, status="DRAFT", visit_schedule=[], ) snapshot = ProjectPublishSnapshot( code="PRJ-002", name="发布项目", project_full_name="发布项目全称", sponsor="申办方", plan_start_date="2026-06-01", plan_end_date="2026-12-31", planned_site_count=8, planned_enrollment_count=120, status="ACTIVE", visit_schedule=[ { "visit_code": "V1", "baseline_offset_days": 7, "window_before_days": 1, "window_after_days": 2, } ], ) changed = _apply_project_publish_snapshot_to_study(study, snapshot) assert changed is True assert study.code == "PRJ-002" assert study.name == "发布项目" assert study.project_full_name == "发布项目全称" assert study.sponsor == "申办方" assert study.plan_start_date == date(2026, 6, 1) assert study.plan_end_date == date(2026, 12, 31) assert study.planned_site_count == 8 assert study.planned_enrollment_count == 120 assert study.status == "ACTIVE" assert study.visit_schedule[0]["visit_code"] == "V1"