Skip to main content

haste_testscript_runner/
lib.rs

1use haste_fhir_client::{
2    FHIRClient,
3    request::{
4        DeleteRequest, FHIRCreateRequest, FHIRDeleteInstanceRequest, FHIRDeleteSystemRequest,
5        FHIRDeleteTypeRequest, FHIRHistoryInstanceRequest, FHIRHistorySystemRequest,
6        FHIRHistoryTypeRequest, FHIRInvokeInstanceRequest, FHIRInvokeSystemRequest,
7        FHIRInvokeTypeRequest, FHIRReadRequest, FHIRRequest, FHIRResponse, FHIRTransactionRequest,
8        FHIRUpdateInstanceRequest, FHIRVersionReadRequest, HistoryRequest, HistoryResponse,
9        InvocationRequest, InvokeResponse, Operation, SearchResponse, UpdateRequest,
10    },
11    url::ParsedParameters,
12};
13use haste_fhir_model::r4::generated::{
14    resources::{
15        Resource, ResourceType, TestReport, TestReportSetup, TestReportSetupAction,
16        TestReportSetupActionAssert, TestReportSetupActionOperation, TestReportTeardown,
17        TestReportTeardownAction, TestReportTest, TestReportTestAction, TestScript,
18        TestScriptFixture, TestScriptSetup, TestScriptSetupAction, TestScriptSetupActionAssert,
19        TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
20        TestScriptTest, TestScriptTestAction, TestScriptVariable,
21    },
22    terminology::{
23        AssertDirectionCodes, AssertOperatorCodes, BundleType, IssueType, ReportActionResultCodes,
24        ReportResultCodes, ReportStatusCodes, TestscriptOperationCodes,
25    },
26    types::{FHIRId, FHIRMarkdown, FHIRString, Reference},
27};
28use haste_fhir_operation_error::OperationOutcomeError;
29use haste_pointer::{Key, TypedPointer};
30use haste_reflect::MetaValue;
31use regex::Regex;
32use std::{
33    any::Any,
34    collections::HashMap,
35    sync::{Arc, LazyLock},
36    time::Duration,
37};
38use tokio::sync::Mutex;
39
40use crate::conversion::ConvertedValue;
41
42mod conversion;
43
44#[derive(Debug)]
45pub enum TestScriptError {
46    ExecutionError(String),
47    ValidationError(String),
48    FixtureNotFound,
49    InvalidFixture,
50    OperationError(OperationOutcomeError),
51}
52
53#[derive(Debug, Clone)]
54enum Response {
55    FHIRResponse(FHIRResponse),
56    OperationError(Arc<OperationOutcomeError>),
57}
58
59#[derive(Debug)]
60enum Fixtures {
61    Resource(Resource),
62    Request(FHIRRequest),
63    Response(Response),
64}
65
66// Internal structure to hold current test result and testing fixtures.
67struct TestState {
68    fp_engine: haste_fhirpath::FPEngine,
69    fixtures: HashMap<String, Fixtures>,
70    latest_request: Option<FHIRRequest>,
71    latest_response: Option<Response>,
72    result: ReportResultCodes,
73}
74
75impl TestState {
76    fn new() -> Self {
77        TestState {
78            fp_engine: haste_fhirpath::FPEngine::new(),
79            fixtures: HashMap::new(),
80            latest_request: None,
81            latest_response: None,
82            result: ReportResultCodes::Pending(None),
83        }
84    }
85    fn resolve_fixture<'a>(
86        &'a self,
87        fixture_id: &str,
88    ) -> Result<&'a dyn MetaValue, TestScriptError> {
89        let fixture = self
90            .fixtures
91            .get(fixture_id)
92            .ok_or(TestScriptError::FixtureNotFound)?;
93
94        match fixture {
95            Fixtures::Resource(res) => Ok(res),
96            Fixtures::Request(req) => {
97                request_to_meta_value(req).ok_or_else(|| TestScriptError::InvalidFixture)
98            }
99            Fixtures::Response(response) => {
100                response_to_meta_value(response).ok_or_else(|| TestScriptError::InvalidFixture)
101            }
102        }
103    }
104}
105
106struct TestResult<T> {
107    pub state: Arc<Mutex<TestState>>,
108    pub value: T,
109}
110
111fn response_to_meta_value<'a>(response: &'a Response) -> Option<&'a dyn MetaValue> {
112    match response {
113        Response::FHIRResponse(fhir_response) => match fhir_response {
114            FHIRResponse::Create(res) => Some(&res.resource),
115            FHIRResponse::Read(res) => Some(&res.resource),
116            FHIRResponse::VersionRead(res) => Some(&res.resource),
117            FHIRResponse::Update(res) => Some(&res.resource),
118            FHIRResponse::Patch(res) => Some(&res.resource),
119            FHIRResponse::Batch(res) => Some(&res.resource),
120            FHIRResponse::Transaction(res) => Some(&res.resource),
121
122            FHIRResponse::Capabilities(res) => Some(&res.capabilities),
123            FHIRResponse::Search(res) => match res {
124                SearchResponse::Type(res) => Some(&res.bundle),
125                SearchResponse::System(res) => Some(&res.bundle),
126            },
127            FHIRResponse::History(res) => match res {
128                HistoryResponse::Instance(res) => Some(&res.bundle),
129                HistoryResponse::Type(res) => Some(&res.bundle),
130                HistoryResponse::System(res) => Some(&res.bundle),
131            },
132            FHIRResponse::Invoke(res) => match res {
133                InvokeResponse::Instance(res) => Some(&res.resource),
134                InvokeResponse::Type(res) => Some(&res.resource),
135                InvokeResponse::System(res) => Some(&res.resource),
136            },
137
138            FHIRResponse::Delete(_) => None,
139        },
140        Response::OperationError(op_error) => {
141            let outcome = op_error.outcome();
142            Some(outcome)
143        }
144    }
145}
146
147fn request_to_meta_value<'a>(request: &'a FHIRRequest) -> Option<&'a dyn MetaValue> {
148    match request {
149        FHIRRequest::Create(req) => Some(&req.resource),
150
151        FHIRRequest::Update(update_request) => match update_request {
152            UpdateRequest::Conditional(req) => Some(&req.resource),
153            UpdateRequest::Instance(req) => Some(&req.resource),
154        },
155
156        FHIRRequest::Batch(req) => Some(&req.resource),
157        FHIRRequest::Transaction(req) => Some(&req.resource),
158        FHIRRequest::Invocation(req) => match req {
159            haste_fhir_client::request::InvocationRequest::Instance(req) => Some(&req.parameters),
160            haste_fhir_client::request::InvocationRequest::Type(req) => Some(&req.parameters),
161            haste_fhir_client::request::InvocationRequest::System(req) => Some(&req.parameters),
162        },
163        FHIRRequest::Read(_)
164        | FHIRRequest::VersionRead(_)
165        | FHIRRequest::Compartment(_)
166        | FHIRRequest::Patch(_)
167        | FHIRRequest::Delete(_)
168        | FHIRRequest::Capabilities
169        | FHIRRequest::Search(_)
170        | FHIRRequest::History(_) => None,
171    }
172}
173
174fn associate_request_response_variables(
175    state: &mut TestState,
176    operation: &TestScriptSetupActionOperation,
177    request: FHIRRequest,
178    response: Response,
179) {
180    if let Some(request_var) = operation
181        .requestId
182        .as_ref()
183        .and_then(|id| id.value.as_ref())
184    {
185        // Associate request variable in state
186        state
187            .fixtures
188            .insert(request_var.clone(), Fixtures::Request(request.clone()));
189    }
190
191    if let Some(response_var) = operation
192        .responseId
193        .as_ref()
194        .and_then(|id| id.value.as_ref())
195    {
196        // Associate response variable in state
197        state
198            .fixtures
199            .insert(response_var.clone(), Fixtures::Response(response.clone()));
200    }
201
202    state.latest_request = Some(request);
203    state.latest_response = Some(response);
204}
205
206/// Derive the resource type from operation or from the metavalue if not present on operation.
207fn derive_resource_type(
208    operation: &TestScriptSetupActionOperation,
209    target: Option<&dyn MetaValue>,
210    path: &str,
211) -> Result<ResourceType, TestScriptError> {
212    if let Some(operation_resource_type) = operation.resource.as_ref() {
213        let string_type: Option<String> = operation_resource_type.as_ref().into();
214        ResourceType::try_from(string_type.unwrap_or_default()).map_err(|_| {
215            TestScriptError::ExecutionError(format!(
216                "Unsupported resource type '{:?}' for operation at '{}'.",
217                operation_resource_type.as_ref(),
218                path
219            ))
220        })
221    } else if let Some(target) = target {
222        ResourceType::try_from(target.typename()).map_err(|_| {
223            TestScriptError::ExecutionError(format!(
224                "Unsupported resource type '{}' for operation at '{}'.",
225                target.typename(),
226                path
227            ))
228        })
229    } else {
230        Err(TestScriptError::ExecutionError(format!(
231            "Failed to derive resource type for operation at '{}'.",
232            path
233        )))
234    }
235}
236
237static EXPRESSION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]*)\}").unwrap());
238
239async fn get_variable(
240    state: &TestState,
241    variables: &Vec<TestScriptVariable>,
242    variable_id: &str,
243) -> Result<ConvertedValue, TestScriptError> {
244    let Some(variable) = variables
245        .iter()
246        .find(|v| v.name.value.as_ref().map(|s| s.as_str()) == Some(variable_id))
247    else {
248        return Err(TestScriptError::ExecutionError(format!(
249            "Variable with id '{}' not found.",
250            variable_id
251        )));
252    };
253
254    if let Some(expression) = variable
255        .expression
256        .as_ref()
257        .and_then(|exp| exp.value.as_ref())
258    {
259        let values =
260            if let Some(source_id) = variable.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
261                let source = state.resolve_fixture(source_id)?;
262                vec![source]
263            } else {
264                vec![]
265            };
266
267        let eval_result = state
268            .fp_engine
269            .evaluate(expression, values)
270            .await
271            .map_err(|e| {
272                TestScriptError::ExecutionError(format!(
273                    "Failed to evaluate FHIRPath expression for variable '{}': {}",
274                    variable_id, e
275                ))
276            })?;
277
278        let converted_values = eval_result
279            .iter()
280            .map(|d| {
281                conversion::convert_meta_value(d).ok_or_else(|| {
282                    TestScriptError::ExecutionError(format!(
283                        "Failed to convert comparison fixture value '{}'.",
284                        d.typename()
285                    ))
286                })
287            })
288            .collect::<Result<Vec<_>, TestScriptError>>()?;
289
290        if converted_values.len() == 1 {
291            Ok(converted_values.into_iter().next().unwrap())
292        } else {
293            Err(TestScriptError::ExecutionError(format!(
294                "Variable '{}' evaluation returned multiple values; only single value supported.",
295                variable_id
296            )))
297        }
298    } else {
299        return Err(TestScriptError::ExecutionError(format!(
300            "Only support variable with expression for variable id '{}'.",
301            variable_id
302        )));
303    }
304}
305
306async fn evaluate_variable(
307    state: &TestState,
308    pointer: TypedPointer<TestScript, TestScript>,
309    value: &str,
310) -> Result<String, TestScriptError> {
311    let mut result = value.to_string();
312    let variable_pointer =
313        pointer.descend::<Vec<TestScriptVariable>>(&Key::Field("variable".to_string()));
314    let default_variables = vec![];
315
316    let variables = if let Some(pointer) = variable_pointer.as_ref() {
317        pointer.value().unwrap_or(&default_variables)
318    } else {
319        &default_variables
320    };
321
322    for reg_match in EXPRESSION_REGEX.captures_iter(value) {
323        let full_match = reg_match.get(0).map(|m| m.as_str()).unwrap_or("");
324        let Some(variable_id) = reg_match.get(1).map(|m| m.as_str()) else {
325            return Err(TestScriptError::ExecutionError(format!(
326                "Invalid variable expression in '{}'.",
327                value
328            )));
329        };
330
331        let variable = get_variable(state, variables, variable_id).await?;
332        result = result.replace(full_match, variable.to_string().as_str());
333    }
334
335    Ok(result)
336}
337
338async fn testscript_operation_to_fhir_request(
339    state: &TestState,
340    pointer: &TypedPointer<TestScript, TestScriptSetupActionOperation>,
341) -> Result<FHIRRequest, TestScriptError> {
342    let operation = pointer.value().ok_or_else(|| {
343        TestScriptError::ExecutionError(format!(
344            "Failed to retrieve TestScript operation at '{}'.",
345            pointer.path()
346        ))
347    })?;
348
349    let operation_type = operation
350        .type_
351        .as_ref()
352        .and_then(|t| t.code.as_ref())
353        .and_then(|c| c.value.clone());
354
355    if operation_type == (&TestscriptOperationCodes::Read(None)).into() {
356        let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
357            return Err(TestScriptError::ExecutionError(format!(
358                "Read operation requires targetId at '{}'.",
359                pointer.path()
360            )));
361        };
362
363        let target = state.resolve_fixture(target_id)?;
364
365        Ok(FHIRRequest::Read(FHIRReadRequest {
366            resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
367            id: target
368                .get_field("id")
369                .ok_or_else(|| {
370                    TestScriptError::ExecutionError(format!(
371                        "Target fixture '{}' does not have an 'id' field.",
372                        target_id
373                    ))
374                })?
375                .as_any()
376                .downcast_ref::<String>()
377                .cloned()
378                .unwrap_or_default(),
379        }))
380    } else if operation_type == (&TestscriptOperationCodes::Vread(None)).into() {
381        let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
382            return Err(TestScriptError::ExecutionError(format!(
383                "Version Read operation requires targetId at '{}'.",
384                pointer.path()
385            )));
386        };
387        let target = state.resolve_fixture(target_id)?;
388
389        let id = target
390            .get_field("id")
391            .ok_or_else(|| {
392                TestScriptError::ExecutionError(format!(
393                    "Target fixture '{}' does not have an 'id' field.",
394                    target_id
395                ))
396            })?
397            .as_any()
398            .downcast_ref::<String>()
399            .cloned()
400            .unwrap_or_default();
401        let version_id = target
402            .get_field("meta")
403            .and_then(|meta| meta.get_field("versionId"))
404            .ok_or_else(|| {
405                TestScriptError::ExecutionError(format!(
406                    "Target fixture '{}' does not have an 'versionId' field.",
407                    target_id
408                ))
409            })?
410            .as_any()
411            .downcast_ref::<Box<FHIRId>>()
412            .cloned()
413            .and_then(|v| v.value)
414            .ok_or_else(|| {
415                TestScriptError::ExecutionError(format!(
416                    "Target fixture '{}' does not have an 'versionId' field.",
417                    target_id
418                ))
419            })?;
420
421        Ok(FHIRRequest::VersionRead(FHIRVersionReadRequest {
422            resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
423            id: id,
424            version_id: version_id.into(),
425        }))
426    } else if operation_type == (&TestscriptOperationCodes::Search(None)).into() {
427        let query_string = operation
428            .params
429            .as_ref()
430            .and_then(|p| p.value.as_ref())
431            .cloned()
432            .unwrap_or_default();
433
434        let processed_query = ParsedParameters::try_from(
435            evaluate_variable(state, pointer.root(), &query_string)
436                .await?
437                .as_str(),
438        )
439        .map_err(|e| {
440            TestScriptError::ExecutionError(format!(
441                "Failed to parse parameters for History operation at '{}': {}",
442                pointer.path(),
443                e
444            ))
445        })?;
446
447        if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
448            Ok(FHIRRequest::Search(
449                haste_fhir_client::request::SearchRequest::Type(
450                    haste_fhir_client::request::FHIRSearchTypeRequest {
451                        resource_type,
452                        parameters: processed_query,
453                    },
454                ),
455            ))
456        } else {
457            Ok(FHIRRequest::Search(
458                haste_fhir_client::request::SearchRequest::System(
459                    haste_fhir_client::request::FHIRSearchSystemRequest {
460                        parameters: processed_query,
461                    },
462                ),
463            ))
464        }
465    } else if operation_type == (&TestscriptOperationCodes::History(None)).into() {
466        let query_string = operation
467            .params
468            .as_ref()
469            .and_then(|p| p.value.as_ref())
470            .cloned()
471            .unwrap_or_default();
472
473        let processed_query = ParsedParameters::try_from(
474            evaluate_variable(state, pointer.root(), &query_string)
475                .await?
476                .as_str(),
477        )
478        .map_err(|e| {
479            TestScriptError::ExecutionError(format!(
480                "Failed to parse parameters for History operation at '{}': {}",
481                pointer.path(),
482                e
483            ))
484        })?;
485
486        if let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) {
487            let target = state.resolve_fixture(target_id)?;
488
489            return Ok(FHIRRequest::History(HistoryRequest::Instance(
490                FHIRHistoryInstanceRequest {
491                    resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
492                    id: target
493                        .get_field("id")
494                        .ok_or_else(|| {
495                            TestScriptError::ExecutionError(format!(
496                                "Target fixture '{}' does not have an 'id' field.",
497                                target_id
498                            ))
499                        })?
500                        .as_any()
501                        .downcast_ref::<String>()
502                        .cloned()
503                        .unwrap_or_default(),
504                    parameters: processed_query,
505                },
506            )));
507        } else if operation.resource.is_some() {
508            let resource_type = derive_resource_type(operation, None, pointer.path())?;
509            return Ok(FHIRRequest::History(HistoryRequest::Type(
510                FHIRHistoryTypeRequest {
511                    resource_type,
512                    parameters: processed_query,
513                },
514            )));
515        } else {
516            return Ok(FHIRRequest::History(HistoryRequest::System(
517                FHIRHistorySystemRequest {
518                    parameters: processed_query,
519                },
520            )));
521        }
522    } else if operation_type == (&TestscriptOperationCodes::Transaction(None)).into() {
523        let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
524            return Err(TestScriptError::ExecutionError(format!(
525                "Transaction operation requires sourceId at '{}'.",
526                pointer.path()
527            )));
528        };
529
530        let source = state.resolve_fixture(source_id)?;
531        let resource = (source as &dyn Any)
532            .downcast_ref::<Resource>()
533            .cloned()
534            .ok_or_else(|| {
535                TestScriptError::ExecutionError(format!(
536                    "Target fixture '{}' is not a Resource.",
537                    source_id
538                ))
539            })?;
540
541        match resource {
542            Resource::Bundle(bundle) => {
543                if !matches!(bundle.type_.as_ref(), BundleType::Transaction(_)) {
544                    return Err(TestScriptError::ExecutionError(format!(
545                        "Fixture must be a transaction bundle for transaction operations for sourceId '{}'.",
546                        source_id
547                    )));
548                }
549
550                Ok(FHIRRequest::Transaction(FHIRTransactionRequest {
551                    resource: bundle,
552                }))
553            }
554
555            _ => Err(TestScriptError::ExecutionError(format!(
556                "Fixture '{}' is not a transaction Bundle resource.",
557                source_id
558            ))),
559        }
560    } else if operation_type == (&TestscriptOperationCodes::Create(None)).into() {
561        let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
562            return Err(TestScriptError::ExecutionError(format!(
563                "Create operation requires sourceId at '{}'.",
564                pointer.path()
565            )));
566        };
567
568        let source = state.resolve_fixture(source_id)?;
569        let resource = (source as &dyn Any)
570            .downcast_ref::<Resource>()
571            .cloned()
572            .ok_or_else(|| {
573                TestScriptError::ExecutionError(format!(
574                    "Target fixture '{}' is not a Resource.",
575                    source_id
576                ))
577            })?;
578
579        Ok(FHIRRequest::Create(FHIRCreateRequest {
580            resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
581            resource: resource,
582        }))
583    } else if operation_type == (&TestscriptOperationCodes::Update(None)).into() {
584        let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
585            return Err(TestScriptError::ExecutionError(format!(
586                "Update operation requires sourceId at '{}'.",
587                pointer.path()
588            )));
589        };
590        let source = state.resolve_fixture(source_id)?;
591        let resource = (source as &dyn Any)
592            .downcast_ref::<Resource>()
593            .cloned()
594            .ok_or_else(|| {
595                TestScriptError::ExecutionError(format!(
596                    "Source fixture '{}' is not a Resource.",
597                    source_id
598                ))
599            })?;
600
601        let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
602            return Err(TestScriptError::ExecutionError(format!(
603                "Update operation requires targetId at '{}'.",
604                pointer.path()
605            )));
606        };
607
608        let target = state.resolve_fixture(target_id)?;
609        let target_resource = (target as &dyn Any)
610            .downcast_ref::<Resource>()
611            .cloned()
612            .ok_or_else(|| {
613                TestScriptError::ExecutionError(format!(
614                    "Source fixture '{}' is not a Resource.",
615                    source_id
616                ))
617            })?;
618
619        Ok(FHIRRequest::Update(UpdateRequest::Instance(
620            FHIRUpdateInstanceRequest {
621                resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
622                id: target_resource
623                    .get_field("id")
624                    .ok_or_else(|| {
625                        TestScriptError::ExecutionError(format!(
626                            "Source fixture '{}' does not have an 'id' field.",
627                            source_id
628                        ))
629                    })?
630                    .as_any()
631                    .downcast_ref::<String>()
632                    .cloned()
633                    .unwrap_or_default(),
634                resource: resource,
635            },
636        )))
637    } else if operation_type == (&TestscriptOperationCodes::Delete(None)).into() {
638        let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
639            return Err(TestScriptError::ExecutionError(format!(
640                "Delete operation requires targetId at '{}'.",
641                pointer.path()
642            )));
643        };
644
645        let target = state.resolve_fixture(target_id)?;
646
647        Ok(FHIRRequest::Delete(DeleteRequest::Instance(
648            FHIRDeleteInstanceRequest {
649                resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
650                id: target
651                    .get_field("id")
652                    .ok_or_else(|| {
653                        TestScriptError::ExecutionError(format!(
654                            "Target fixture '{}' does not have an 'id' field.",
655                            target_id
656                        ))
657                    })?
658                    .as_any()
659                    .downcast_ref::<String>()
660                    .cloned()
661                    .unwrap_or_default(),
662            },
663        )))
664    } else if operation_type == (&TestscriptOperationCodes::DeleteCondMultiple(None)).into() {
665        let delete_parameters = ParsedParameters::try_from(
666            operation
667                .params
668                .as_ref()
669                .and_then(|p| p.value.as_ref())
670                .cloned()
671                .unwrap_or("".to_string())
672                .as_str(),
673        )
674        .map_err(|e| {
675            TestScriptError::ExecutionError(format!(
676                "Failed to parse parameters for DeleteCondMultiple operation at '{}': {}",
677                pointer.path(),
678                e
679            ))
680        })?;
681        if operation.resource.is_some() {
682            Ok(FHIRRequest::Delete(DeleteRequest::Type(
683                FHIRDeleteTypeRequest {
684                    resource_type: derive_resource_type(operation, None, pointer.path())?,
685                    parameters: delete_parameters,
686                },
687            )))
688        } else {
689            Ok(FHIRRequest::Delete(DeleteRequest::System(
690                FHIRDeleteSystemRequest {
691                    parameters: delete_parameters,
692                },
693            )))
694        }
695    } else if operation_type == Some("invoke".to_string()) {
696        let Some(op_code) = operation.url.as_ref().and_then(|u| u.value.as_ref()) else {
697            return Err(TestScriptError::ExecutionError(format!(
698                "Invoke operation requires url at '{}' which is used for the operation code.",
699                pointer.path()
700            )));
701        };
702
703        let op_code = Operation::new(op_code).map_err(|_e| {
704            TestScriptError::ExecutionError(format!(
705                "Invalid operation code for invoke operation '{}'",
706                op_code
707            ))
708        })?;
709
710        let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
711            return Err(TestScriptError::ExecutionError(format!(
712                "Invoke operation requires sourceId at '{}'.",
713                pointer.path()
714            )));
715        };
716        let source = state.resolve_fixture(source_id)?;
717        let Resource::Parameters(parameters) = (source as &dyn Any)
718            .downcast_ref::<Resource>()
719            .cloned()
720            .ok_or_else(|| {
721                TestScriptError::ExecutionError(format!(
722                    "Source fixture '{}' is not a Resource.",
723                    source_id
724                ))
725            })?
726        else {
727            return Err(TestScriptError::ExecutionError(format!(
728                "Source fixture '{}' is not a Parameters resource.",
729                source_id
730            )));
731        };
732
733        if let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) {
734            let target = state.resolve_fixture(target_id)?;
735            let target_resource = (target as &dyn Any)
736                .downcast_ref::<Resource>()
737                .cloned()
738                .ok_or_else(|| {
739                    TestScriptError::ExecutionError(format!(
740                        "Source fixture '{}' is not a Resource.",
741                        source_id
742                    ))
743                })?;
744            let target_id = target_resource
745                .get_field("id")
746                .ok_or_else(|| {
747                    TestScriptError::ExecutionError(format!(
748                        "Source fixture '{}' does not have an 'id' field.",
749                        source_id
750                    ))
751                })?
752                .as_any()
753                .downcast_ref::<String>()
754                .cloned()
755                .unwrap_or_default();
756            let resource_type = derive_resource_type(operation, Some(target), pointer.path())?;
757
758            Ok(FHIRRequest::Invocation(InvocationRequest::Instance(
759                FHIRInvokeInstanceRequest {
760                    operation: op_code,
761                    resource_type,
762                    id: target_id,
763                    parameters,
764                },
765            )))
766        } else if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
767            Ok(FHIRRequest::Invocation(InvocationRequest::Type(
768                FHIRInvokeTypeRequest {
769                    operation: op_code,
770                    resource_type,
771                    parameters,
772                },
773            )))
774        } else {
775            Ok(FHIRRequest::Invocation(InvocationRequest::System(
776                FHIRInvokeSystemRequest {
777                    operation: op_code,
778                    parameters,
779                },
780            )))
781        }
782    } else {
783        Err(TestScriptError::ExecutionError(format!(
784            "Unsupported TestScript operation type: {:?} at '{}'.",
785            operation_type,
786            pointer.path()
787        )))
788    }
789}
790
791async fn run_operation<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
792    client: &Client,
793    ctx: CTX,
794    state: Arc<Mutex<TestState>>,
795    pointer: TypedPointer<TestScript, TestScriptSetupActionOperation>,
796    options: Arc<TestRunnerOptions>,
797) -> Result<TestResult<TestReportSetupActionOperation>, TestScriptError> {
798    let operation = pointer.value().ok_or_else(|| {
799        TestScriptError::ExecutionError(format!(
800            "Failed to retrieve TestScript operation at '{}'.",
801            pointer.path()
802        ))
803    })?;
804
805    let mut state_guard = state.lock().await;
806    let fhir_request = testscript_operation_to_fhir_request(&state_guard, &pointer).await?;
807    let fhir_response = client.request(ctx, fhir_request.clone()).await;
808    if let Some(wait_duration) = options.wait_between_operations {
809        tokio::time::sleep(wait_duration).await;
810    }
811
812    match fhir_response {
813        Ok(fhir_response) => {
814            associate_request_response_variables(
815                &mut state_guard,
816                operation,
817                fhir_request,
818                Response::FHIRResponse(fhir_response),
819            );
820
821            drop(state_guard);
822
823            Ok(TestResult {
824                state: state.clone(),
825                value: TestReportSetupActionOperation {
826                    result: Box::new(ReportActionResultCodes::Pass(None)),
827                    ..Default::default()
828                },
829            })
830        }
831        Err(op_error) => {
832            let op_error = Arc::new(op_error);
833            tracing::warn!("Operation at '{}' failed: {}", pointer.path(), op_error);
834            associate_request_response_variables(
835                &mut state_guard,
836                operation,
837                fhir_request,
838                Response::OperationError(op_error.clone()),
839            );
840
841            Ok(TestResult {
842                state: state.clone(),
843                value: TestReportSetupActionOperation {
844                    result: Box::new(ReportActionResultCodes::Warning(None)),
845                    message: Some(Box::new(FHIRMarkdown {
846                        value: Some(format!("Operation failed: {}", op_error)),
847                        ..Default::default()
848                    })),
849                    ..Default::default()
850                },
851            })
852        }
853    }
854}
855
856static DEFAULT_DIRECTION: LazyLock<Box<AssertDirectionCodes>> =
857    LazyLock::new(|| Box::new(AssertDirectionCodes::Response(None)));
858
859async fn get_source<'a>(
860    state: &'a TestState,
861    assertion: &TestScriptSetupActionAssert,
862) -> Result<Option<&'a dyn MetaValue>, TestScriptError> {
863    if let Some(source_id) = assertion.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
864        let source = state.resolve_fixture(source_id)?;
865        Ok(Some(source))
866    } else {
867        match assertion
868            .direction
869            .as_ref()
870            .unwrap_or(&DEFAULT_DIRECTION)
871            .as_ref()
872        {
873            AssertDirectionCodes::Request(_) => {
874                if let Some(request) = state.latest_request.as_ref() {
875                    request_to_meta_value(request)
876                        .ok_or_else(|| TestScriptError::InvalidFixture)
877                        .map(Some)
878                } else {
879                    Ok(None)
880                }
881            }
882            AssertDirectionCodes::Response(_) => {
883                if let Some(response) = state.latest_response.as_ref() {
884                    response_to_meta_value(response)
885                        .ok_or_else(|| TestScriptError::InvalidFixture)
886                        .map(Some)
887                } else {
888                    Ok(None)
889                }
890            }
891            AssertDirectionCodes::Null(_) => Err(TestScriptError::ExecutionError(
892                "Assert direction cannot be 'null' when sourceId is not provided.".to_string(),
893            )),
894        }
895    }
896}
897
898fn evaluate_operator(
899    operator: &Box<AssertOperatorCodes>,
900    a: &Vec<conversion::ConvertedValue>,
901    b: &Vec<conversion::ConvertedValue>,
902) -> bool {
903    match operator.as_ref() {
904        AssertOperatorCodes::Equals(_) | AssertOperatorCodes::Null(_) => a == b,
905        AssertOperatorCodes::NotEquals(_) => !(a == b),
906
907        AssertOperatorCodes::Contains(_) => {
908            if a.len() != 1 || b.len() != 1 {
909                return false;
910            }
911
912            match (&a[0], &b[0]) {
913                (ConvertedValue::String(a_str), ConvertedValue::String(b_str)) => {
914                    a_str.contains(b_str)
915                }
916                _ => false,
917            }
918        }
919        AssertOperatorCodes::Empty(_) => todo!("Empty operator not implemented"),
920        AssertOperatorCodes::Eval(_) => todo!("Eval operator not implemented"),
921        AssertOperatorCodes::GreaterThan(_) => todo!("GreaterThan operator not implemented"),
922        AssertOperatorCodes::In(_) => todo!("In operator not implemented"),
923        AssertOperatorCodes::LessThan(_) => todo!("LessThan operator not implemented"),
924        AssertOperatorCodes::NotContains(_) => todo!("NotContains operator not implemented"),
925        AssertOperatorCodes::NotEmpty(_) => todo!("NotEmpty operator not implemented"),
926        AssertOperatorCodes::NotIn(_) => todo!("NotIn operator not implemented"),
927    }
928    // a == b
929}
930
931static DEFAULT_EQUAL_OPERATOR: LazyLock<Box<AssertOperatorCodes>> =
932    LazyLock::new(|| Box::new(AssertOperatorCodes::Equals(None)));
933
934async fn derive_comparison_to(
935    state: &TestState,
936    assertion: &TestScriptSetupActionAssert,
937) -> Result<Vec<ConvertedValue>, TestScriptError> {
938    if let Some(comparision_fixture_id) = assertion
939        .compareToSourceId
940        .as_ref()
941        .and_then(|c| c.value.as_ref())
942    {
943        let comparison_fixture = state.resolve_fixture(comparision_fixture_id)?;
944
945        let Some(comparison_expression) = assertion
946            .compareToSourceExpression
947            .as_ref()
948            .and_then(|exp| exp.value.as_ref())
949        else {
950            return Err(TestScriptError::ExecutionError(
951                "compareToSourceExpression is required when compareToSourceId is provided."
952                    .to_string(),
953            ));
954        };
955
956        let result = state
957            .fp_engine
958            .evaluate(comparison_expression, vec![comparison_fixture])
959            .await
960            .map_err(|e| {
961                TestScriptError::ExecutionError(format!(
962                    "FHIRPath evaluation error for comparison fixture '{}': {}",
963                    comparision_fixture_id, e
964                ))
965            })?;
966
967        result
968            .iter()
969            .map(|d| {
970                conversion::convert_meta_value(d).ok_or_else(|| {
971                    TestScriptError::ExecutionError(format!(
972                        "Failed to convert comparison fixture value '{}'.",
973                        d.typename()
974                    ))
975                })
976            })
977            .collect::<Result<Vec<_>, TestScriptError>>()
978    } else if let Some(value) = assertion.value.as_ref().and_then(|v| v.value.as_ref())
979        && let Some(converted_value) = conversion::convert_string_value(value.as_ref())
980    {
981        Ok(vec![converted_value])
982    } else {
983        Err(TestScriptError::ExecutionError(
984            "Failed to derive comparison value for assertion.".to_string(),
985        ))
986    }
987}
988
989fn get_id<T: MetaValue>(pointer: &TypedPointer<TestScript, T>) -> String {
990    pointer
991        .root()
992        .value()
993        .and_then(|t| t.id.clone())
994        .unwrap_or_default()
995}
996
997/// Assertions are what determine the testreports ultimate pass/fail status.
998/// So set that within state here depending on assertion success/failure.
999async fn run_assertion(
1000    state: Arc<Mutex<TestState>>,
1001    pointer: TypedPointer<TestScript, TestScriptSetupActionAssert>,
1002) -> Result<TestResult<TestReportSetupActionAssert>, TestScriptError> {
1003    let assertion = pointer.value().ok_or_else(|| {
1004        TestScriptError::ExecutionError(format!(
1005            "Failed to retrieve TestScript assertion at '{}'.",
1006            pointer.path()
1007        ))
1008    })?;
1009
1010    let mut state_guard = state.lock().await;
1011
1012    let Some(source) = get_source(&*state_guard, assertion).await? else {
1013        return Err(TestScriptError::ExecutionError(format!(
1014            "Failed to resolve source for assertion at '{}'.",
1015            pointer.path()
1016        )));
1017    };
1018
1019    let operator = assertion
1020        .operator
1021        .as_ref()
1022        .unwrap_or(&*DEFAULT_EQUAL_OPERATOR);
1023
1024    if assertion.resource.is_some() {
1025        let resource_string = assertion
1026            .resource
1027            .as_ref()
1028            .and_then(|r| {
1029                let string_type: Option<String> = r.as_ref().into();
1030                string_type
1031            })
1032            .unwrap_or("".to_string());
1033
1034        let operation_evaluation_result = evaluate_operator(
1035            operator,
1036            &vec![conversion::ConvertedValue::String(resource_string.clone())],
1037            &vec![conversion::ConvertedValue::String(
1038                source.typename().to_string(),
1039            )],
1040        );
1041        if !operation_evaluation_result {
1042            tracing::error!(
1043                "{} Assertion at '{}' failed: resource type '{}' does not match '{}'.",
1044                get_id(&pointer),
1045                pointer.path(),
1046                resource_string,
1047                source.typename()
1048            );
1049
1050            state_guard.result = ReportResultCodes::Fail(None);
1051            return Ok(TestResult {
1052                state: state.clone(),
1053                value: TestReportSetupActionAssert {
1054                    result: Box::new(ReportActionResultCodes::Fail(None)),
1055                    ..Default::default()
1056                },
1057            });
1058        }
1059    }
1060    if let Some(expression) = assertion.expression.as_ref().and_then(|e| e.value.as_ref()) {
1061        let comparison_to = derive_comparison_to(&state_guard, assertion).await?;
1062
1063        let Ok(result) = state_guard
1064            .fp_engine
1065            .evaluate(expression, vec![source])
1066            .await
1067        else {
1068            tracing::error!(
1069                "{} Assertion at '{}' failed: FHIRPath expression '{}' failed to evaluate.",
1070                get_id(&pointer),
1071                expression,
1072                pointer.path()
1073            );
1074
1075            state_guard.result = ReportResultCodes::Fail(None);
1076            return Err(TestScriptError::ExecutionError(format!(
1077                "FHIRPath failed to evaluate at '{}' error.",
1078                pointer.path()
1079            )));
1080        };
1081
1082        let converted_values = result
1083            .iter()
1084            .filter_map(|v| conversion::convert_meta_value(v))
1085            .collect::<Vec<_>>();
1086
1087        let operation_evaluation_result =
1088            evaluate_operator(operator, &converted_values, &comparison_to);
1089
1090        if !operation_evaluation_result {
1091            tracing::error!(
1092                "{} Assertion at '{}' failed: '{:?}' {:?} '{:?}'.",
1093                get_id(&pointer),
1094                pointer.path(),
1095                converted_values,
1096                operator,
1097                comparison_to
1098            );
1099
1100            state_guard.result = ReportResultCodes::Fail(None);
1101            return Ok(TestResult {
1102                state: state.clone(),
1103                value: TestReportSetupActionAssert {
1104                    result: Box::new(ReportActionResultCodes::Fail(None)),
1105                    ..Default::default()
1106                },
1107            });
1108        }
1109    }
1110
1111    return Ok(TestResult {
1112        state: state.clone(),
1113        value: TestReportSetupActionAssert {
1114            result: Box::new(ReportActionResultCodes::Pass(None)),
1115            ..Default::default()
1116        },
1117    });
1118}
1119
1120async fn run_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1121    client: &Client,
1122    ctx: CTX,
1123    state: Arc<Mutex<TestState>>,
1124    pointer: TypedPointer<TestScript, TestScriptTestAction>,
1125    options: Arc<TestRunnerOptions>,
1126) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1127    tracing::info!("Running TestScript action at path: {}", pointer.path());
1128    let action = pointer.value().ok_or_else(|| {
1129        TestScriptError::ExecutionError(format!(
1130            "Failed to retrieve TestScript action at '{}'.",
1131            pointer.path()
1132        ))
1133    })?;
1134
1135    // Should be either an operation or an assert.
1136    // Both should not exist at the same time.
1137    if action.operation.is_some() {
1138        let Some(operation_pointer) =
1139            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1140        else {
1141            return Err(TestScriptError::ExecutionError(format!(
1142                "Failed to retrieve TestScript operation at '{}'.",
1143                pointer.path()
1144            )));
1145        };
1146
1147        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1148
1149        Ok(TestResult {
1150            state: result.state,
1151            value: TestReportSetupAction {
1152                operation: Some(result.value),
1153                ..Default::default()
1154            },
1155        })
1156    } else if action.assert.is_some() {
1157        let Some(assertion_pointer) =
1158            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1159        else {
1160            return Err(TestScriptError::ExecutionError(format!(
1161                "Failed to retrieve TestScript assertion at '{}'.",
1162                pointer.path()
1163            )));
1164        };
1165
1166        let assertion = run_assertion(state, assertion_pointer).await?;
1167
1168        Ok(TestResult {
1169            state: assertion.state,
1170            value: TestReportSetupAction {
1171                assert: Some(assertion.value),
1172                ..Default::default()
1173            },
1174        })
1175    } else {
1176        Err(TestScriptError::ExecutionError(format!(
1177            "TestScript action must have either an operation or an assert at '{}'.",
1178            pointer.path()
1179        )))
1180    }
1181}
1182
1183async fn run_setup_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1184    client: &Client,
1185    ctx: CTX,
1186    state: Arc<Mutex<TestState>>,
1187    pointer: TypedPointer<TestScript, TestScriptSetupAction>,
1188    options: Arc<TestRunnerOptions>,
1189) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1190    let action = pointer.value().ok_or_else(|| {
1191        TestScriptError::ExecutionError(format!(
1192            "Failed to retrieve TestScript action at '{}'.",
1193            pointer.path()
1194        ))
1195    })?;
1196
1197    tracing::info!("Running TestScript action at path: {}", pointer.path());
1198
1199    // Should be either an operation or an assert.
1200    // Both should not exist at the same time.
1201    if action.operation.is_some() {
1202        let Some(operation_pointer) =
1203            pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1204        else {
1205            return Err(TestScriptError::ExecutionError(format!(
1206                "Failed to retrieve TestScript operation at '{}'.",
1207                pointer.path()
1208            )));
1209        };
1210
1211        let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1212
1213        Ok(TestResult {
1214            state: result.state,
1215            value: TestReportSetupAction {
1216                operation: Some(result.value),
1217                ..Default::default()
1218            },
1219        })
1220    } else if action.assert.is_some() {
1221        let Some(assertion_pointer) =
1222            pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1223        else {
1224            return Err(TestScriptError::ExecutionError(format!(
1225                "Failed to retrieve TestScript assertion at '{}'.",
1226                pointer.path()
1227            )));
1228        };
1229
1230        let assertion = run_assertion(state, assertion_pointer).await?;
1231
1232        Ok(TestResult {
1233            state: assertion.state,
1234            value: TestReportSetupAction {
1235                assert: Some(assertion.value),
1236                ..Default::default()
1237            },
1238        })
1239    } else {
1240        Err(TestScriptError::ExecutionError(format!(
1241            "TestScript action must have either an operation or an assert at '{}'.",
1242            pointer.path()
1243        )))
1244    }
1245}
1246
1247async fn setup_fixtures<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1248    client: &Client,
1249    ctx: CTX,
1250    state: Arc<Mutex<TestState>>,
1251    pointer: TypedPointer<TestScript, TestScript>,
1252    _options: Arc<TestRunnerOptions>,
1253) -> Result<Arc<Mutex<TestState>>, OperationOutcomeError> {
1254    let mut state_lock = state.lock().await;
1255
1256    let Some(fixtures_pointer) =
1257        pointer.descend::<Vec<TestScriptFixture>>(&Key::Field("fixture".to_string()))
1258    else {
1259        return Ok(state.clone());
1260    };
1261
1262    let Some(fixtures) = fixtures_pointer.value() else {
1263        return Ok(state.clone());
1264    };
1265
1266    for fixture in fixtures.iter() {
1267        if let Some(reference_string) = fixture
1268            .resource
1269            .as_ref()
1270            .and_then(|r| r.reference.as_ref())
1271            .and_then(|refe| refe.value.as_ref())
1272        {
1273            let resolved_resource = if reference_string.starts_with('#')
1274                && let Some(contained) =
1275                    pointer.descend::<Vec<Box<Resource>>>(&Key::Field("contained".to_string()))
1276                && let Some(contained) = contained.value()
1277            {
1278                let local_id = &reference_string[1..];
1279                let Some(resource) = contained.iter().find(|res| {
1280                    if let Some(id) = res.get_field("id")
1281                        && let Some(id) = id.as_any().downcast_ref::<String>()
1282                    {
1283                        id.as_str() == local_id
1284                    } else {
1285                        false
1286                    }
1287                }) else {
1288                    return Err(OperationOutcomeError::error(
1289                        IssueType::NotFound(None),
1290                        format!("Contained resource with id '{}' not found.", local_id),
1291                    ));
1292                };
1293
1294                resource.as_ref().clone()
1295            } else {
1296                let parts = reference_string.split("/").collect::<Vec<&str>>();
1297                if parts.len() != 2 {
1298                    return Err(OperationOutcomeError::error(
1299                        IssueType::Invalid(None),
1300                        format!("Invalid fixture reference: {}", reference_string),
1301                    ));
1302                }
1303
1304                let resource_type = parts[0];
1305                let id = parts[1];
1306
1307                let Some(remote_resource) = client
1308                    .read(
1309                        ctx.clone(),
1310                        ResourceType::try_from(resource_type).map_err(|_| {
1311                            OperationOutcomeError::error(
1312                                IssueType::Invalid(None),
1313                                format!(
1314                                    "Invalid resource type in fixture reference: '{}'",
1315                                    resource_type
1316                                ),
1317                            )
1318                        })?,
1319                        id.to_string(),
1320                    )
1321                    .await?
1322                else {
1323                    return Err(OperationOutcomeError::error(
1324                        IssueType::NotFound(None),
1325                        format!("Resource '{}' with id '{}' not found.", resource_type, id),
1326                    ));
1327                };
1328
1329                remote_resource
1330            };
1331
1332            state_lock.fixtures.insert(
1333                fixture.id.clone().unwrap_or_default(),
1334                Fixtures::Resource(resolved_resource),
1335            );
1336        }
1337    }
1338
1339    drop(state_lock);
1340
1341    Ok(state)
1342}
1343
1344async fn run_setup<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1345    client: &Client,
1346    ctx: CTX,
1347    state: Arc<Mutex<TestState>>,
1348    pointer: TypedPointer<TestScript, TestScriptSetup>,
1349    options: Arc<TestRunnerOptions>,
1350) -> Result<TestResult<TestReportSetup>, TestScriptError> {
1351    let mut cur_state = state;
1352
1353    let mut setup_results = TestReportSetup {
1354        action: vec![],
1355        ..Default::default()
1356    };
1357
1358    let Some(setup) = pointer.value() else {
1359        return Ok(TestResult {
1360            state: cur_state,
1361            value: setup_results,
1362        });
1363    };
1364
1365    for action in setup.action.iter().enumerate() {
1366        let action_pointer = pointer
1367            .descend::<Vec<TestScriptSetupAction>>(&Key::Field("action".to_string()))
1368            .and_then(|p| p.descend::<TestScriptSetupAction>(&Key::Index(action.0)));
1369
1370        let action_pointer = action_pointer.ok_or_else(|| {
1371            TestScriptError::ExecutionError(format!(
1372                "Failed to retrieve TestScript action at index {}.",
1373                action.0
1374            ))
1375        })?;
1376
1377        let result = run_setup_action(
1378            client,
1379            ctx.clone(),
1380            cur_state,
1381            action_pointer,
1382            options.clone(),
1383        )
1384        .await?;
1385        cur_state = result.state;
1386
1387        setup_results.action.push(result.value);
1388    }
1389
1390    Ok(TestResult {
1391        state: cur_state,
1392        value: setup_results,
1393    })
1394}
1395
1396async fn run_teardown<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1397    client: &Client,
1398    ctx: CTX,
1399    state: Arc<Mutex<TestState>>,
1400    pointer: TypedPointer<TestScript, TestScriptTeardown>,
1401    options: Arc<TestRunnerOptions>,
1402) -> Result<TestResult<TestReportTeardown>, TestScriptError> {
1403    let mut cur_state = state;
1404
1405    let mut teardown_results = TestReportTeardown {
1406        action: vec![],
1407        ..Default::default()
1408    };
1409
1410    let Some(actions) = pointer.value() else {
1411        return Ok(TestResult {
1412            state: cur_state,
1413            value: teardown_results,
1414        });
1415    };
1416
1417    for action in actions.action.iter().enumerate() {
1418        let action_pointer = pointer
1419            .descend::<Vec<TestScriptTeardownAction>>(&Key::Field("action".to_string()))
1420            .and_then(|p| p.descend::<TestScriptTeardownAction>(&Key::Index(action.0)));
1421
1422        let action_pointer = action_pointer.ok_or_else(|| {
1423            TestScriptError::ExecutionError(format!(
1424                "Failed to retrieve TestScript teardown action at index {}.",
1425                action.0
1426            ))
1427        })?;
1428
1429        let operation_pointer = action_pointer
1430            .descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1431            .ok_or_else(|| {
1432                TestScriptError::ExecutionError(format!(
1433                    "Failed to retrieve TestScript teardown operation at index {}.",
1434                    action.0
1435                ))
1436            })?;
1437
1438        let result = run_operation(
1439            client,
1440            ctx.clone(),
1441            cur_state,
1442            operation_pointer,
1443            options.clone(),
1444        )
1445        .await?;
1446        cur_state = result.state;
1447
1448        teardown_results.action.push(TestReportTeardownAction {
1449            operation: result.value,
1450            ..Default::default()
1451        });
1452    }
1453
1454    Ok(TestResult {
1455        state: cur_state,
1456        value: teardown_results,
1457    })
1458}
1459
1460async fn run_test<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1461    client: &Client,
1462    ctx: CTX,
1463    state: Arc<Mutex<TestState>>,
1464    pointer: TypedPointer<TestScript, TestScriptTest>,
1465    options: Arc<TestRunnerOptions>,
1466) -> Result<TestResult<TestReportTest>, TestScriptError> {
1467    let mut cur_state = state;
1468    let mut test_report_test = TestReportTest {
1469        action: vec![],
1470        ..Default::default()
1471    };
1472
1473    let test = pointer.value().ok_or_else(|| {
1474        TestScriptError::ExecutionError(format!(
1475            "Failed to retrieve TestScript test at '{}'.",
1476            pointer.path()
1477        ))
1478    })?;
1479
1480    for action in test.action.iter().enumerate() {
1481        let Some(action_pointer) = pointer
1482            .descend::<Vec<TestScriptTestAction>>(&Key::Field("action".to_string()))
1483            .and_then(|p| p.descend(&Key::Index(action.0)))
1484        else {
1485            return Err(TestScriptError::ExecutionError(format!(
1486                "Failed to retrieve TestScript test action at index {}.",
1487                action.0
1488            )));
1489        };
1490        let result = run_action(
1491            client,
1492            ctx.clone(),
1493            cur_state,
1494            action_pointer,
1495            options.clone(),
1496        )
1497        .await?;
1498        cur_state = result.state;
1499        test_report_test.action.push(TestReportTestAction {
1500            operation: result.value.operation,
1501            assert: result.value.assert,
1502            ..Default::default()
1503        });
1504    }
1505
1506    Ok(TestResult {
1507        state: cur_state,
1508        value: test_report_test,
1509    })
1510}
1511
1512async fn run_tests<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1513    client: &Client,
1514    ctx: CTX,
1515    state: Arc<Mutex<TestState>>,
1516    pointer: TypedPointer<TestScript, Vec<TestScriptTest>>,
1517    options: Arc<TestRunnerOptions>,
1518) -> Result<TestResult<Vec<TestReportTest>>, TestScriptError> {
1519    let mut test_results = vec![];
1520    let mut cur_state = state;
1521
1522    let Some(tests) = pointer.value() else {
1523        return Ok(TestResult {
1524            state: cur_state,
1525            value: test_results,
1526        });
1527    };
1528
1529    for test in tests.iter().enumerate() {
1530        let Some(test_pointer) = pointer.descend(&Key::Index(test.0)) else {
1531            return Err(TestScriptError::ExecutionError(format!(
1532                "Failed to retrieve TestScript test at index {}.",
1533                test.0
1534            )));
1535        };
1536        let test_result = run_test(
1537            client,
1538            ctx.clone(),
1539            cur_state,
1540            test_pointer,
1541            options.clone(),
1542        )
1543        .await?;
1544        cur_state = test_result.state;
1545        test_results.push(test_result.value);
1546    }
1547
1548    Ok(TestResult {
1549        state: cur_state,
1550        value: test_results,
1551    })
1552}
1553
1554pub struct TestRunnerOptions {
1555    pub wait_between_operations: Option<Duration>,
1556}
1557
1558pub async fn run<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1559    client: &Client,
1560    ctx: CTX,
1561    test_script: Arc<TestScript>,
1562    options: Arc<TestRunnerOptions>,
1563) -> Result<TestReport, TestScriptError> {
1564    // Placeholder implementation
1565    tracing::info!("Running TestScript Runner with FHIR Client");
1566
1567    let mut test_report = TestReport {
1568        status: Box::new(ReportStatusCodes::Completed(None)),
1569        testScript: Box::new(Reference {
1570            reference: Some(Box::new(FHIRString {
1571                value: Some(format!(
1572                    "Testscript/{}",
1573                    test_script.id.clone().unwrap_or_default()
1574                )),
1575                ..Default::default()
1576            })),
1577            ..Default::default()
1578        }),
1579        ..Default::default()
1580    };
1581
1582    let mut state = Arc::new(Mutex::new(TestState::new()));
1583    let pointer = TypedPointer::<TestScript, TestScript>::new(test_script);
1584
1585    state = setup_fixtures(client, ctx.clone(), state, pointer.clone(), options.clone())
1586        .await
1587        .map_err(|e| TestScriptError::OperationError(e))?;
1588
1589    let mut running_state = Ok(());
1590
1591    // Run setup actions
1592    if let Some(setup_pointer) =
1593        pointer.descend::<TestScriptSetup>(&Key::Field("setup".to_string()))
1594    {
1595        tracing::info!("Running TestScript setup...");
1596        let setup_result = run_setup(
1597            client,
1598            ctx.clone(),
1599            state.clone(),
1600            setup_pointer,
1601            options.clone(),
1602        )
1603        .await;
1604        match setup_result {
1605            Ok(res) => {
1606                state = res.state;
1607                test_report.setup = Some(res.value);
1608            }
1609            Err(e) => {
1610                running_state = Err(e);
1611            }
1612        }
1613    }
1614
1615    // Run Test actions
1616    if running_state.is_ok()
1617        && let Some(test_pointer) =
1618            pointer.descend::<Vec<TestScriptTest>>(&Key::Field("test".to_string()))
1619    {
1620        tracing::info!("Running TestScript tests...");
1621        let test_result = run_tests(
1622            client,
1623            ctx.clone(),
1624            state.clone(),
1625            test_pointer,
1626            options.clone(),
1627        )
1628        .await;
1629
1630        match test_result {
1631            Ok(res) => {
1632                state = res.state;
1633                test_report.test = Some(res.value);
1634            }
1635
1636            Err(e) => {
1637                running_state = Err(e);
1638            }
1639        }
1640    }
1641
1642    if let Some(teardown_pointer) =
1643        pointer.descend::<TestScriptTeardown>(&Key::Field("teardown".to_string()))
1644    {
1645        tracing::info!("Running TestScript teardown...");
1646
1647        let result = run_teardown(
1648            client,
1649            ctx.clone(),
1650            state.clone(),
1651            teardown_pointer,
1652            options.clone(),
1653        )
1654        .await?;
1655
1656        // state = result.state;
1657        test_report.teardown = Some(result.value);
1658    }
1659
1660    running_state?;
1661
1662    let state_guard = state.lock().await;
1663    // Only set result to fail so if still pending can assume pass.
1664    // Flip to fail in assertion tests if any fail.
1665    match &state_guard.result {
1666        ReportResultCodes::Pending(_) => {
1667            test_report.result = Box::new(ReportResultCodes::Pass(None))
1668        }
1669        status => test_report.result = Box::new(status.clone()),
1670    }
1671
1672    Ok(test_report)
1673}