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