Files
adas-core/adas-core.Test/Customizations/HUVH/UCIA/CalculatedObservationsTest.cs
T
2026-06-26 10:29:23 +02:00

845 lines
39 KiB
C#

using adas_core.Application.Customizations.HUVH.UCIA;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Moq;
using Options = Microsoft.Extensions.Options.Options;
namespace adas_core.Test.Customizations.HUVH.UCIA;
[TestFixture]
[Order(2)]
public class CalculatedObservationsTest
{
/// <summary>
/// Initializes the test environment by creating mock instances of the observation, medicine, and treatment services,
/// configuring a service provider with these dependencies, and instantiating the <see cref="CalculatedObservations"/>
/// class under test.
/// </summary>
[SetUp]
public void Setup()
{
_observationServiceMock = new Mock<IObservationService>();
_medicineServiceMock = new Mock<IMedicineService>();
_treatmentServiceMock = new Mock<ITreatmentService>();
_logger = new Mock<ILogger<CalculatedObservations>>();
_optionsApiSettings = Options.Create(_apiSettings);
var serviceCollection = new ServiceCollection();
//serviceCollection.AddSingleton(_observationServiceMock.Object);
serviceCollection.AddSingleton(new Lazy<IObservationService>(() => _observationServiceMock.Object));
serviceCollection.AddSingleton(new Lazy<IMedicineService>(() => _medicineServiceMock.Object));
serviceCollection.AddSingleton(new Lazy<ITreatmentService>(() => _treatmentServiceMock.Object));
serviceCollection.AddSingleton(_optionsApiSettings);
serviceCollection.AddSingleton(_logger.Object);
var serviceProvider = serviceCollection.BuildServiceProvider();
_calculatedObservations = new CalculatedObservations(serviceProvider);
}
private CalculatedObservations _calculatedObservations;
private Mock<IMedicineService> _medicineServiceMock;
private Mock<IObservationService> _observationServiceMock;
private Mock<ITreatmentService> _treatmentServiceMock;
private Mock<ILogger<CalculatedObservations>> _logger;
private readonly ApiSettings _apiSettings = new()
{
ConfigObservation = new ConfigObservationSettings
{
IgnoreUnknownObservation = false
}
};
private IOptions<ApiSettings> _optionsApiSettings;
//Mandar observación 'SOBRESEDACIÓN'
//cuando:
//(RASS =-4 o RASS =-5) y
//PSI < 25 y
//TS > 5 y
//(Propofol > 3 ó Midazolam > 0,05 ó Isoflorano > 10)
/// <summary>
/// Verifies that an over-sedation observation is inserted when the patient meets the clinical criteria:
/// a RASS observation with a sufficiently low score, recent PSI and TS observations, and an active treatment
/// (e.g., Propofol) with a minimum requested give amount that triggers the over-sedation condition.
/// </summary>
[Test]
public async Task CalculateOverSedation_Should_Insert_Observation_When_Conditions_Met()
{
var patientId = ObjectId.GenerateNewId();
var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 };
var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 };
var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "PSI", "TS" }))
.ReturnsAsync([psiObservation, tsObservation]);
_treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId))
.ReturnsAsync(new List<PatientTreatment>
{ new() { RequestedGiveCodes = [new Code { Text = "Propofol" }], RequestedGiveAmountMinimum = 4 } });
await _calculatedObservations.Map(rassObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Once);
}
/// <summary>
/// Verifies that no over-sedation observation is inserted when the patient's RASS score is not -4 or -5, ensuring the over-sedation calculation only applies for those specific values.
/// </summary>
[Test]
public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_RASS_Is_Not_Negative4_Or_Negative5()
{
var patientId = ObjectId.GenerateNewId();
var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -3 };
await _calculatedObservations.Map(rassObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never);
}
/// <summary>
/// Verifies that no observation is inserted when calculating over-sedation and the patient's PSI (Sedation Intensity Index) is 25 or higher, which indicates the threshold above which the over-sedation mapping should not produce an observation.
/// </summary>
[Test]
public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_PSI_Is_25_Or_Higher()
{
var patientId = ObjectId.GenerateNewId();
var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 };
var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 25 };
var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "PSI", "TS" }))
.ReturnsAsync([psiObservation, tsObservation]);
await _calculatedObservations.Map(rassObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never);
}
/// <summary>
/// Verifies that the over-sedation calculation does not insert an observation when the patient's TS (Tidal Spontaneous) value is 5 or lower, even with a low RASS score and an elevated PSI value.
/// </summary>
[Test]
public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_TS_Is_5_Or_Lower()
{
var patientId = ObjectId.GenerateNewId();
var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 };
var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 };
var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 5 };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "PSI", "TS" }))
.ReturnsAsync([psiObservation, tsObservation]);
await _calculatedObservations.Map(rassObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never);
}
/// <summary>
/// Verifies that no observation is inserted when the patient has no active treatments that exceed the configured threshold during the over-sedation calculation.
/// </summary>
[Test]
public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_No_Treatments_Exceed_Threshold()
{
var patientId = ObjectId.GenerateNewId();
var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 };
var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 };
var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "PSI", "TS" }))
.ReturnsAsync([psiObservation, tsObservation]);
_treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId))
.ReturnsAsync(new List<PatientTreatment>()); // No hay tratamientos activos que superen el umbral
await _calculatedObservations.Map(rassObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never);
}
/// <summary>
/// Verifies that an over-sedation observation is inserted when the calculation is triggered by a PSI observation
/// and the patient's last RASS and TS observations, combined with active treatment data, meet the triggering criteria.
/// </summary>
[Test]
public async Task CalculateOverSedation_Should_Insert_Observation_When_Triggered_By_PSI_Observation()
{
var patientId = ObjectId.GenerateNewId();
var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 };
var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 };
var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "RASS", "TS" }))
.ReturnsAsync([rassObservation, tsObservation]);
_treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId))
.ReturnsAsync(new List<PatientTreatment>
{ new() { RequestedGiveCodes = [new Code { Text = "Propofol" }], RequestedGiveAmountMinimum = 4 } });
await _calculatedObservations.Map(psiObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Once);
}
/// <summary>
/// Verifies that the over-sedation calculation logic inserts a new observation when a triggering TS (Twitch Score) observation is received, given the presence of related RASS and PSI observations and an active treatment (e.g., Propofol) meeting the required minimum amount threshold.
/// </summary>
[Test]
public async Task CalculateOverSedation_Should_Insert_Observation_When_Triggered_By_TS_Observation()
{
var patientId = ObjectId.GenerateNewId();
var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 };
var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 };
var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "RASS", "PSI" }))
.ReturnsAsync([rassObservation, psiObservation]);
_treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId))
.ReturnsAsync(new List<PatientTreatment>
{ new() { RequestedGiveCodes = [new Code { Text = "Propofol" }], RequestedGiveAmountMinimum = 4 } });
await _calculatedObservations.Map(tsObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Once);
}
//- Si llega valor de PI pero NO COMPL, el tipo de ventilación es VMNI
//- Si llega valor de PS pero no llega COMPL, el tipo de ventilación es VMNI
//- Si llega COMPL, el tipo de ventilación es VMI
//- Si llega Flujo, leer el tipo de ventilación de CCC(CNAF o CTAF)
/// <summary>
/// Verifies that when an Inspiratory Pressure (PI) observation is processed and no Compliancia (COMPL) observation is found for the patient, the ventilation mode is set to "VMNI".
/// </summary>
[Test]
public async Task CalculateVentilationMode_Should_Set_VMNI_When_PI_PS_Arrives_Without_COMPL()
{
var patientId = ObjectId.GenerateNewId();
var piObservation = new PatientObservation { PatientId = patientId, Name = "Inspiratory_Pressure" };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "Compliancia" }))
.ReturnsAsync([]); // No hay COMPL
await _calculatedObservations.Map(piObservation);
_observationServiceMock.Verify(
x => x.InsertObservation(It.Is<PatientObservation>(o => o.Value.ToString() == "VMNI"), true, false),
Times.Once);
}
/// <summary>
/// Verifies that when a "Compliancia" (COMPL) observation is mapped, the ventilation mode is calculated and set to "VMI".
/// </summary>
[Test]
public async Task CalculateVentilationMode_Should_Set_VMI_When_COMPL_Arrives()
{
var patientId = ObjectId.GenerateNewId();
var complObservation = new PatientObservation { PatientId = patientId, Name = "Compliancia" };
await _calculatedObservations.Map(complObservation);
_observationServiceMock.Verify(
x => x.InsertObservation(It.Is<PatientObservation>(o => o.Value.ToString() == "VMI"), true, false),
Times.Once);
}
/// <summary>
/// Verifies that when an Air Flow observation is processed and a CCC Ventilation Mode observation exists for the patient, the calculated ventilation mode is set to the value retrieved from the CCC source (e.g., "CNAF") and persisted via an insert call.
/// </summary>
[Test]
public async Task CalculateVentilationMode_Should_Set_CCC_Mode_When_AirFlow_Arrives_And_CCC_Has_Mode()
{
var patientId = ObjectId.GenerateNewId();
var airFlowObservation = new PatientObservation { PatientId = patientId, Name = "Air_Flow" };
var mockCursor = new Mock<IAsyncCursor<PatientObservation>>();
mockCursor.Setup(c => c.Current).Returns(new List<PatientObservation> { new() { Value = "CNAF" } });
mockCursor.SetupSequence(c => c.MoveNext(It.IsAny<CancellationToken>())).Returns(true).Returns(false);
mockCursor.SetupSequence(c => c.MoveNextAsync(It.IsAny<CancellationToken>())).ReturnsAsync(true)
.ReturnsAsync(false);
_observationServiceMock.Setup(x => x.FindByPatientIdAndCodingSystemAsync(patientId, "CCC", "Ventilation_Mode"))
.ReturnsAsync(mockCursor.Object);
await _calculatedObservations.Map(airFlowObservation);
_observationServiceMock.Verify(
x => x.InsertObservation(It.Is<PatientObservation>(o => o.Value.ToString() == "CNAF"), true, false),
Times.Once);
}
[Test]
public async Task CalculateVentilationMode_Should_Insert_When_AirFlow_Arrives_And_CCC_Mode()
{
var patientId = ObjectId.GenerateNewId();
var airFlowObservation = new PatientObservation { PatientId = patientId, Name = "Air_Flow" };
var mockCursor = new Mock<IAsyncCursor<PatientObservation>>();
// Configura la lista de observaciones devueltas por el cursor
var observations = new List<PatientObservation> { new() { Value = "CNAF" } };
mockCursor.Setup(c => c.Current).Returns(observations);
// Configura el comportamiento de MoveNext y MoveNextAsync para simular la enumeración
mockCursor.SetupSequence(c => c.MoveNext(It.IsAny<CancellationToken>()))
.Returns(true) // Primera llamada: hay datos
.Returns(false); // Segunda llamada: no hay más datos
mockCursor.SetupSequence(c => c.MoveNextAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(true) // Primera llamada asíncrona: hay datos
.ReturnsAsync(false); // Segunda llamada: no hay más datos
// Configura el mock del servicio para devolver el cursor simulado
_observationServiceMock.Setup(x => x.FindByPatientIdAndCodingSystemAsync(patientId, "CCC", "Ventilation_Mode"))
.ReturnsAsync(mockCursor.Object);
await _calculatedObservations.Map(airFlowObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Once);
}
/// <summary>
/// Verifies that when a PI observation is processed and a COMPL observation already exists for the patient, no new calculated observation is inserted.
/// </summary>
[Test]
public async Task CalculateVentilationMode_Should_Not_Insert_When_PI_Arrives_And_COMPL_Already_Exists()
{
var patientId = ObjectId.GenerateNewId();
var piObservation = new PatientObservation { PatientId = patientId, Name = "PI" };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "COMPL" }))
.ReturnsAsync([new PatientObservation { Name = "COMPL" }]); // Existe COMPL
await _calculatedObservations.Map(piObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never);
}
/// <summary>
/// Verifies that no calculated observation is inserted when a PS (Pressure Support) observation is processed
/// and a COMPL observation already exists for the patient.
/// </summary>
[Test]
public async Task CalculateVentilationMode_Should_Not_Insert_When_PS_Arrives_And_COMPL_Already_Exists()
{
var patientId = ObjectId.GenerateNewId();
var psObservation = new PatientObservation { PatientId = patientId, Name = "PS" };
_observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List<string> { "COMPL" }))
.ReturnsAsync([new PatientObservation { Name = "COMPL" }]); // Existe COMPL
await _calculatedObservations.Map(psObservation);
_observationServiceMock.Verify(x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never);
}
//Las opciones pueden ser VV, VA o ECMO.
//Cuando llegue VV se muestra VV.
//Cuando llegue VA se muestra VA.
//Cuando llegue los siguientes parámetros, se debe mostrar ECMO: ECCO2r, VVDL, VA+V, VVDL+V, VV+V, VVA
/// <summary>
/// Verifies that mapping a <see cref="PatientObservation"/> with Name "ECMO_Location" and Value "VV" returns a non-null result whose value is "VV".
/// </summary>
[Test]
public async Task CalculateEcmoLocation_Should_Return_VV_When_Value_Is_VV()
{
var observation = new PatientObservation { Name = "ECMO_Location", Value = "VV" };
var result = await _calculatedObservations.Map(observation);
Assert.That(result, Is.Not.Null);
Assert.That(result.Value, Is.EqualTo("VV"));
}
/// <summary>
/// Verifies that the ECMO location calculation returns "VA" when the input observation value is "VA".
/// </summary>
[Test]
public async Task CalculateEcmoLocation_Should_Return_VA_When_Value_Is_VA()
{
var observation = new PatientObservation { Name = "ECMO_Location", Value = "VA" };
var result = await _calculatedObservations.Map(observation);
Assert.That(result, Is.Not.Null);
Assert.That(result.Value, Is.EqualTo("VA"));
}
/// <summary>
/// Verifies that the mapping logic returns "ECMO" when the observation value is one of the recognized ECMO location codes
/// (e.g., "ECCO2r", "VVDL", "VA+V", "VVDL+V", "VV+V", "VVA").
/// </summary>
/// <param name="inputValue">The ECMO location code assigned to the <see cref="PatientObservation"/> value under test.</param>
[Test]
[TestCase("ECCO2r")]
[TestCase("VVDL")]
[TestCase("VA+V")]
[TestCase("VVDL+V")]
[TestCase("VV+V")]
[TestCase("VVA")]
public async Task CalculateEcmoLocation_Should_Return_ECMO_When_Value_Is_In_Ecmo_List(string inputValue)
{
var observation = new PatientObservation { Name = "ECMO_Location", Value = inputValue };
var result = await _calculatedObservations.Map(observation);
Assert.That(result, Is.Not.Null);
Assert.That(result.Value, Is.EqualTo("ECMO"));
}
/// <summary>
/// Verifies that the ECMO Location mapping does not alter the observation value when the provided value is not present in the recognized ECMO list, leaving the original value unchanged.
/// </summary>
[Test]
public async Task CalculateEcmoLocation_Should_Not_Change_Value_When_Not_In_Ecmo_List()
{
var observation = new PatientObservation { Name = "ECMO_Location", Value = "OTHER" };
var result = await _calculatedObservations.Map(observation);
Assert.That(result, Is.Not.Null);
Assert.That(result.Value, Is.EqualTo("OTHER"));
}
/// <summary>
/// Verifies that the mapping of a <see cref="PatientObservation"/> through the calculated observations mapper returns the original observation unchanged, ensuring the mapping is a no-op for patient observations.
/// </summary>
[Test]
public async Task CalculateEcmoLocation_Should_Return_Original_Observation_When_Not_PatientObservation()
{
var observation = new PatientObservation();
var result = await _calculatedObservations.Map(observation);
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.EqualTo(observation));
}
// Mandar observación 'SOBREANALGESIA'
// 0 <= EVN <= 3 o ESCID = 3 o ANI > 70
// y
// Perfusiones de morfina, remifentanilo 0 fentanilo sostenidas > 24h.
/// <summary>
/// Verifies that an "Over_Analgesia" observation with value "True" is created when the patient has active analgesic treatments (Morfina, Remifentanilo) and the last ANI observation indicates a high value (80), suggesting over-analgesia.
/// </summary>
[Test]
public async Task CalculateOverAnalgesia_Should_Create_Observation_When_Conditions_Are_Met()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "EVN", Value = "3", PatientId = patientId };
var morfina = new PatientTreatment
{ RequestedGiveCodes = [new Code { Text = "Morfina" }], StartTime = DateTime.UtcNow.AddHours(-25) };
var remifentanilo = new PatientTreatment
{ RequestedGiveCodes = [new Code { Text = "Remifentanilo" }], StartTime = DateTime.UtcNow.AddHours(-25) };
//var fentanilo = new PatientTreatment
// { RequestedGiveCodes = [new Code { Text = "Fentanilo" }], StartTime = DateTime.UtcNow.AddHours(-25) };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([new PatientObservation { Name = "ANI", Value = "80" }]);
_treatmentServiceMock
.Setup(s => s.GetActiveTreatmentsByPatient(patientId))
.ReturnsAsync(new List<PatientTreatment>
{ morfina, remifentanilo /*, fentanilo*/ }); // No hay tratamientos de analgesia activos
_observationServiceMock
.Setup(s => s.InsertObservation(It.IsAny<PatientObservation>(), true, false))
.Returns(Task.CompletedTask);
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(
It.Is<PatientObservation>(o =>
o.PatientId == patientId && o.Name == "Over_Analgesia" && o.Value.ToString() == "True"),
true, false),
Times.Once
);
}
[Test]
public async Task CalculateOverAnalgesia_Should_Not_Create_Observation_When_Conditions_Are_Not_Met()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "EVN", Value = "5", PatientId = patientId }; // No cumple la condición
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([new PatientObservation { Name = "ANI", Value = "60" }]);
_treatmentServiceMock
.Setup(s => s.GetActiveTreatmentsByPatient(patientId))
.ReturnsAsync(new List<PatientTreatment>()); // No hay tratamientos activos
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never
);
}
//Diferencia entre la Pmeset y la PEEP
//Calculo Valor = Pmeset - PEEP
/// <summary>
/// Verifies that the mapping correctly calculates and inserts the driving pressure (Pmeset minus PEEP) when both source observations are available.
/// </summary>
[Test]
public async Task CalculateDrivingPressure_Should_Calculate_Correctly_When_Both_Observations_Are_Present()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Pmeset", Value = "25", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([new PatientObservation { Name = "PEEP", Value = "10" }]);
_observationServiceMock
.Setup(s => s.InsertObservation(It.IsAny<PatientObservation>(), true, false))
.Returns(Task.CompletedTask);
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(
It.Is<PatientObservation>(o =>
o.PatientId == patientId && o.Name == "Driving_Pressure" && o.Value.ToString() == "15"),
true, false),
Times.Once
);
}
[Test]
public async Task CalculateDrivingPressure_Should_Not_Calculate_If_Observation_Is_Missing()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Pmeset", Value = "25", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([]); // No hay observación complementaria
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never
);
}
// Cálculo dividiendo el ratio SpO2/FiO2 entre la FR
// Valor = (SpO2_FiO2_Ratio) / FR
/// <summary>
/// Verifies that the Rox Index is calculated correctly by dividing the SpO2/FiO2 ratio by the respiratory rate (FR)
/// when both required observations are available, and that the result is persisted as a new "Rox_Index" observation.
/// </summary>
[Test]
public async Task CalculateRoxIndex_Should_Calculate_Correctly_When_Both_Observations_Are_Present()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "SpO2_FiO2_Ratio", Value = "200", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([new PatientObservation { Name = "FR", Value = "25" }]);
_observationServiceMock
.Setup(s => s.InsertObservation(It.IsAny<PatientObservation>(), true, false))
.Returns(Task.CompletedTask);
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(
It.Is<PatientObservation>(o =>
o.PatientId == patientId && o.Name == "Rox_Index" && o.Value.ToString() == (200 / 25).ToString()),
true, false),
Times.Once
);
}
[Test]
public async Task CalculateRoxIndex_Should_Not_Calculate_If_Observation_Is_Missing()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "SpO2_FiO2_Ratio", Value = "200", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([]); // No hay observación complementaria
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never
);
}
/// <summary>
/// Verifies that the ROX index calculation is not performed when the respiratory rate (FR) is zero,
/// preventing a division by zero error.
/// </summary>
[Test]
public async Task CalculateRoxIndex_Should_Not_Calculate_When_FR_Is_Zero_To_Avoid_Division_By_Zero()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "SpO2_FiO2_Ratio", Value = "200", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([new PatientObservation { Name = "FR", Value = "0" }]);
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never
);
}
// Calculado como el sumatorio de la Diuresis de las últimas 6h
// entre el último peso medido del paciente y entre 6. Medido en ml/kg/h
// Valor = sum(Diuresis últimas 6 horas) / Weight / 6
/// <summary>
/// Verifies that the diuresis calculation produces the correct value when prior diuresis observations and a weight observation are available for the patient, resulting in a new "Calculated_Diuresis" observation being inserted.
/// </summary>
[Test]
public async Task CalculateDiuresis_Should_Calculate_Correctly_When_Observations_Are_Available()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Diuresis", Value = "100", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 2, It.IsAny<List<string>>()))
.ReturnsAsync([
new PatientObservation { Name = "Diuresis", Value = "120" },
new PatientObservation { Name = "Diuresis", Value = "110" }
]);
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([new PatientObservation { Name = "Weight", Value = "70" }]);
_observationServiceMock
.Setup(s => s.InsertObservation(It.IsAny<PatientObservation>(), true, false))
.Returns(Task.CompletedTask);
// Act
await _calculatedObservations.Map(obs);
// Assert
const double expectedValue = (100 + 120 + 110) / 70.0 / 6;
_observationServiceMock.Verify(
x => x.InsertObservation(
It.Is<PatientObservation>(o =>
o.PatientId == patientId && o.Name == "Calculated_Diuresis" &&
Math.Abs((double)o.Value - expectedValue) < 0.1),
true, false),
Times.Once
);
}
/// <summary>
/// Verifies that diuresis is not calculated when no diuresis observations are found for the patient.
/// Ensures the mapping logic skips persistence of a calculated diuresis observation when required prior observations are missing.
/// </summary>
[Test]
public async Task CalculateDiuresis_Should_Not_Calculate_If_Diuresis_Observations_Are_Missing()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Diuresis", Value = "100", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 2, It.IsAny<List<string>>()))
.ReturnsAsync([]); // No hay diuresis
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never
);
}
/// <summary>
/// Verifies that the diuresis calculation is not performed when the patient has no weight observation available, ensuring no new observation is inserted.
/// </summary>
[Test]
public async Task CalculateDiuresis_Should_Not_Calculate_If_Weight_Is_Invalid()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Diuresis", Value = "100", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 2, It.IsAny<List<string>>()))
.ReturnsAsync([
new PatientObservation { Name = "Diuresis", Value = "120" },
new PatientObservation { Name = "Diuresis", Value = "110" }
]);
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny<List<string>>()))
.ReturnsAsync([]); // No hay peso
// Act
await _calculatedObservations.Map(obs);
// Assert
_observationServiceMock.Verify(
x => x.InsertObservation(It.IsAny<PatientObservation>(), true, false),
Times.Never
);
}
/// <summary>
/// Verifies that the rehabilitation observation mapping sets the status to Alert when the recent rehabilitation values meet the alert condition.
/// </summary>
[Test]
public async Task CalculateRehabilitationAlarm_Should_Set_Alert_When_Condition_Is_Met()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Rehabilitation", Value = "2", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 6, It.IsAny<List<string>>()))
.ReturnsAsync([
new PatientObservation { Name = "Rehabilitation", Value = "0" },
new PatientObservation { Name = "Rehabilitation", Value = "1" },
new PatientObservation { Name = "Rehabilitation", Value = "2" },
new PatientObservation { Name = "Rehabilitation", Value = "0" },
new PatientObservation { Name = "Rehabilitation", Value = "1" },
new PatientObservation { Name = "Rehabilitation", Value = "2" }
]);
// Act
var result = await _calculatedObservations.Map(obs);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Status, Is.EqualTo(StatusEnum.Type.Alert));
}
/// <summary>
/// Verifies that the rehabilitation alarm calculation does not set the alert status when the
/// configured condition is not met, even when the most recent observation values are within an
/// expected range and an out-of-range value is present among the evaluated history.
/// </summary>
[Test]
public async Task CalculateRehabilitationAlarm_Should_Not_Set_Alert_When_Condition_Is_Not_Met()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Rehabilitation", Value = "2", PatientId = patientId };
_observationServiceMock
.Setup(s => s.FindLastObservations(patientId, 6, It.IsAny<List<string>>()))
.ReturnsAsync([
new PatientObservation { Name = "Rehabilitation", Value = "0" },
new PatientObservation { Name = "Rehabilitation", Value = "1" },
new PatientObservation { Name = "Rehabilitation", Value = "2" },
new PatientObservation { Name = "Rehabilitation", Value = "1" },
new PatientObservation { Name = "Rehabilitation", Value = "3" }, //<--fuera de rango
new PatientObservation { Name = "Rehabilitation", Value = "0" }
]);
// Act
var result = await _calculatedObservations.Map(obs);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Status, Is.Not.EqualTo(StatusEnum.Type.Alert));
}
/// <summary>
/// Verifies that the <c>Map</c> method does not set an alert for a Rehabilitation observation when its value is greater than 2, returning the original observation unchanged.
/// </summary>
[Test]
public async Task CalculateRehabilitationAlarm_Should_Not_Set_Alert_When_Value_Is_Greater_Than_2()
{
// Arrange
var patientId = ObjectId.GenerateNewId();
var obs = new PatientObservation { Name = "Rehabilitation", Value = "3", PatientId = patientId };
// Act
var result = await _calculatedObservations.Map(obs);
// Assert
Assert.That(result, Is.SameAs(obs));
}
/// <summary>
/// Verifies that the rehabilitation alarm calculation returns the same observation unchanged when the input observation is not a <see cref="PatientObservation"/>.
/// This ensures that the mapping method does not perform any transformation for non-patient observations.
/// </summary>
[Test]
public async Task CalculateRehabilitationAlarm_Should_Return_Same_Observation_If_Not_PatientObservation()
{
// Arrange
var obs = new BasePatientObservation(); // No es un PatientObservation
// Act
var result = await _calculatedObservations.Map(obs);
// Assert
Assert.That(result, Is.SameAs(obs));
}
}