using System.Security.Claims;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services;
using adas_core.Application.Services.Interfaces;
using adas_core.Application.Subscriptions;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Pumps;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using Moq;
using Options = Microsoft.Extensions.Options.Options;

namespace adas_core.Test.Services;

[TestFixture]
public class PumpServiceTest
{
    private PumpService _service = null!;
    private Mock<IPumpObservationRepository> _obsRepo = null!;
    private Mock<IPumpStateRepository> _stateRepo = null!;
    private Mock<IPumpAlarmEventRepository> _alarmEventRepo = null!;
    private Mock<IPumpAlarmStateRepository> _alarmStateRepo = null!;
    private Mock<IPumpArchiveRepository> _archiveRepo = null!;
    private Mock<IPatientService> _patientSvc = null!;
    private Mock<IConfigPumpsService> _configPumps = null!;
    private Mock<ISubscribersService> _subs = null!;
    private Mock<IClientMessageService> _clientMsg = null!;
    private Mock<IConfigUnitsService> _configUnits = null!;
    private Mock<ICalculatedObservationsService> _calcObs = null!;
    private Lazy<ICalculatedObservationsService> _lazyCalc = null!;
    private Mock<IHttpContextAccessor> _http = null!;
    private Mock<ILocalAuditService> _audit = null!;
    private Mock<ILogger<PumpService>> _logger = null!;

    private static readonly ObjectId PatientId = ObjectId.GenerateNewId();
    private static readonly DateTime Now = DateTime.UtcNow;

    /// <summary>
    /// Initializes the unit test fixture by creating mock repositories, services, and a mocked <see cref="HttpContext"/> with a test claims principal, then instantiates the <see cref="PumpService"/> under test using the configured API settings (5-second pump expiration, zero pump messages disabled). Pass-through mappings are configured for <see cref="ICalculatedObservationsService"/>, <see cref="IConfigPumpsService"/>, and <see cref="IConfigUnitsService"/> so that supplied pump observations are returned unchanged.
    /// </summary>
    [SetUp]
        public void Setup()
        {
            _obsRepo = new Mock<IPumpObservationRepository>();
            _stateRepo = new Mock<IPumpStateRepository>();
            _alarmEventRepo = new Mock<IPumpAlarmEventRepository>();
            _alarmStateRepo = new Mock<IPumpAlarmStateRepository>();
            _archiveRepo = new Mock<IPumpArchiveRepository>();
            _patientSvc = new Mock<IPatientService>();
            _configPumps = new Mock<IConfigPumpsService>();
            _subs = new Mock<ISubscribersService>();
            _clientMsg = new Mock<IClientMessageService>();
            _calcObs = new Mock<ICalculatedObservationsService>();
            _lazyCalc = new Lazy<ICalculatedObservationsService>(() => _calcObs.Object);
            _http = new Mock<IHttpContextAccessor>();
            _audit = new Mock<ILocalAuditService>();
            _logger = new Mock<ILogger<PumpService>>();
            _configUnits = new Mock<IConfigUnitsService>();
    
            var principal = new ClaimsPrincipal(
                new ClaimsIdentity([new Claim(ClaimTypes.Name, "test")], "mock"));
    
            _http.Setup(x => x.HttpContext)
                .Returns(new DefaultHttpContext { User = principal });
    
            _calcObs.Setup(x => x.Map(It.IsAny<PumpObservation>()))
                .ReturnsAsync((PumpObservation o) => o);
    
            var api = Options.Create(new ApiSettings
            {
                PumpExpiresSeconds = 5,
                SendPumpsZero = false
            });
    
            _service = new PumpService(
                _obsRepo.Object,
                _stateRepo.Object,
                _alarmEventRepo.Object,
                _alarmStateRepo.Object,
                _archiveRepo.Object,
                _patientSvc.Object,
                _configPumps.Object,
                api,
                _logger.Object,
                _subs.Object,
                _clientMsg.Object,
                _lazyCalc,
                _http.Object,
                _audit.Object,
                _configUnits.Object
            );
    
            _configPumps.Setup(x => x.Map(It.IsAny<PumpObservation>()))
                .ReturnsAsync((PumpObservation o) => o);
            _configUnits.Setup(x => x.Map(It.IsAny<PumpObservation>()))
                .ReturnsAsync((PumpObservation o) => o);
        }

    // --------------------------------------------------------------
    // SAVE REQUEST — casos básicos
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="ApiRequest"/> processing does not insert any <see cref="PumpObservation"/> records
    /// when the request is saved without associated observations.
    /// </summary>
    [Test]
        public async Task SaveRequest_Returns_When_No_Observations()
        {
            var req = new ApiRequest { Type = "ORU_R01" };
            await _service.SaveRequest(req);
            _obsRepo.Verify(r => r.InsertAsync(It.IsAny<PumpObservation>()), Times.Never);
        }

    /// <summary>
    /// Verifies that <c>SaveRequest</c> does not insert a <see cref="PumpObservation"/> when the <see cref="ApiRequest.Type"/> is an unrecognized value.
    /// </summary>
    [Test]
        public async Task SaveRequest_UnknownType_DoesNotInsert()
        {
            var req = new ApiRequest
            {
                Type = "UNKNOWN",
                PumpObservation = new PumpObservation { Time = Now }
            };
    
            await _service.SaveRequest(req);
            _obsRepo.Verify(r => r.InsertAsync(It.IsAny<PumpObservation>()), Times.Never);
        }

    /// <summary>
    /// Verifies that SaveRequest converts a single <c>PumpObservation</c> on an incoming
    /// <c>ApiRequest</c> into a list with one entry on the request after processing.
    /// </summary>
    [Test]
        public async Task SaveRequest_Converts_SingleObservation_ToList()
        {
            var obs = new PumpObservation { Time = Now };
            var req = new ApiRequest
            {
                Type = "ORU_R01",
                PumpObservation = obs,
                PatientNumber = "123"
            };
    
            _patientSvc.Setup(p => p.FindPatientByApiRequest(req))
                .ReturnsAsync(new Patient { Id = PatientId });
    
            await _service.SaveRequest(req);
            Assert.That(req.PumpObservations, Has.Count.EqualTo(1));
        }

    /// <summary>
    /// Verifies that Service.SaveRequest sets the <c>Expires</c> property of the <see cref="PumpObservation"/> before persisting it via the repository.
    /// </summary>
    [Test]
        public async Task SaveRequest_SetsExpires_BeforeInsert()
        {
            var obs = new PumpObservation
            {
                Time = Now,
                DeviceId = "D1"
            };
    
            var req = new ApiRequest
            {
                Type = "ORU_R01",
                PumpObservation = obs,
                PatientNumber = "1"
            };
    
            _patientSvc.Setup(x => x.FindPatientByApiRequest(req))
                .ReturnsAsync(new Patient { Id = PatientId });
    
            await _service.SaveRequest(req);
    
            _obsRepo.Verify(r => r.InsertAsync(
                It.Is<PumpObservation>(o => o.Expires == 5)), Times.Once);
        }
    // --------------------------------------------------------------
    // PROCESS ALARM
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that saving a request of type "ORU_R40" creates a <see cref="PumpAlarmEvent"/>
    /// and does not create a <see cref="PumpObservation"/>, ensuring alarm-phase events are
    /// routed to the alarm event store rather than the observation store.
    /// </summary>
    [Test]
        public async Task SaveRequest_ORU_R40_CreatesAlarmEvent()
        {
            var obs = new PumpObservation
            {
                Time = Now,
                DeviceId = "D1",
                AlarmType = PumpEnum.AlarmType.Occlusion,
                EventPhase = PumpEnum.EventPhase.Start
            };
    
            var req = new ApiRequest { Type = "ORU_R40", PumpObservation = obs };
            _patientSvc.Setup(x => x.FindPatientByApiRequest(req))
                .ReturnsAsync(new Patient { Id = PatientId });
    
            await _service.SaveRequest(req);
    
            _alarmEventRepo.Verify(r => r.InsertAsync(It.IsAny<PumpAlarmEvent>()), Times.Once);
            _obsRepo.Verify(r => r.InsertAsync(It.IsAny<PumpObservation>()), Times.Never);
        }

    /// <summary>
    /// Verifies that processing a pump observation with <c>EventPhase.End</c> removes the corresponding alarm state
    /// by calling <c>RemoveAsync</c> on the alarm state repository with the matching device, alarm type, and MDC code.
    /// </summary>
    [Test]
        public async Task ProcessAlarm_End_RemovesAlarmState()
        {
            var obs = new PumpObservation
            {
                Time = Now,
                DeviceId = "DX",
                AlarmType = PumpEnum.AlarmType.Occlusion,
                AlarmTypeMdc = "H1",
                EventPhase = PumpEnum.EventPhase.End
            };
    
            var req = new ApiRequest { Type = "ORU_R40", PumpObservation = obs };
            _patientSvc.Setup(x => x.FindPatientByApiRequest(req))
                .ReturnsAsync(new Patient { Id = PatientId });
    
            await _service.SaveRequest(req);
    
            _alarmStateRepo.Verify(r => r.RemoveAsync("DX", PumpEnum.AlarmType.Occlusion, "H1"), Times.Once);
        }

    // --------------------------------------------------------------
    // SNAPSHOT DE BOMBA
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that <c>SaveRequest</c> creates and upserts a new <see cref="PumpState"/>
    /// for the device when no existing pump state is found in the repository.
    /// </summary>
    [Test]
        public async Task SaveRequest_CreatesPumpState_IfNotExists()
        {
            var obs = new PumpObservation { Time = Now, DeviceId = "P1" };
            var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" };
    
            _patientSvc.Setup(x => x.FindPatientByApiRequest(req))
                .ReturnsAsync(new Patient { Id = PatientId });
    
            _stateRepo.Setup(r => r.FindByDeviceIdAsync("P1"))
                .ReturnsAsync((PumpState?)null);
    
            await _service.SaveRequest(req);
    
            _stateRepo.Verify(r => r.UpsertAsync(It.Is<PumpState>(s => s.DeviceId == "P1")), Times.Once);
        }

    // --------------------------------------------------------------
    // BROADCAST
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that SaveRequest does not broadcast a message via the client when there are no active subscribers.
    /// </summary>
    /// <returns>A task that completes when the assertion has been executed.</returns>
    [Test]
        public async Task SaveRequest_NoSubscribers_NoBroadcast()
        {
            var obs = new PumpObservation { Time = Now, DeviceId = "BR1", PatientId = PatientId };
            var patient = new Patient
            {
                Id = PatientId,
                Location = new PatientLocation("U1", "B1")
            };
    
            var req = new ApiRequest
            {
                Type = "ORU_R01",
                PumpObservation = obs,
                PatientNumber = "1"
            };
    
            _subs.Setup(x => x.GetSubscribers()).Returns([]);
            _patientSvc.Setup(x => x.FindPatientByApiRequest(req))
                .ReturnsAsync(patient);
    
            await _service.SaveRequest(req);
    
            _clientMsg.Verify(r => r.SendAsync(It.IsAny<string>(), It.IsAny<OperationType>(), It.IsAny<object>()),
                Times.Never);
        }

    [Test]
    public async Task SaveRequest_WithSubscribers_UsesReqLocation_AndSendsBroadcast()
    {
        // Arrange
        var obs = new PumpObservation
        {
            Time = Now,
            DeviceId = "D22",
            PatientId = null // <- clave para que se use req.Location
        };

        var req = new ApiRequest
        {
            Type = "ORU_R01",
            PumpObservation = obs,
            Location = new PatientLocation("UCI5C", "Box4", "Room1"),
            PatientNumber = "437537"
        };

        var subscriber = new WsSubscriber("sub1")
        {
            Locations = [ new PatientLocation("UCI5C", "Box4", "Room1") ]
        };

        // 1) NO devolver paciente en FindPatientByApiRequest -> fuerza uso de req. Location
        _patientSvc.Setup(p => p.FindPatientByApiRequest(req))
                   .ReturnsAsync((Patient?)null);

        // 2) Suscriptores con la misma ubicación que req.Location
        _subs.Setup(x => x.GetSubscribers())
             .Returns([subscriber]);

        // 3) Evitar null en alarmas activas al enumerar (foreach)
        _alarmStateRepo.Setup(r => r.FindAllActiveByDeviceAsync("D22"))
                       .ReturnsAsync(Enumerable.Empty<PumpAlarmState>());

        // 4) Snapshot: por claridad, fuerza que se cree uno nuevo
        _stateRepo.Setup(r => r.FindByDeviceIdAsync("D22"))
                  .ReturnsAsync((PumpState?)null);

        // Map mínimo para que avance el flujo
        _configPumps.Setup(m => m.Map(It.IsAny<PumpObservation>()))
                    .ReturnsAsync((PumpObservation o) => o);
        _configUnits.Setup(m => m.Map(It.IsAny<PumpObservation>()))
                    .ReturnsAsync((PumpObservation o) => o);

        // Act
        await _service.SaveRequest(req);

        // Assert
        _clientMsg.Verify(x =>
            x.SendAsync("sub1", OperationType.Pump, It.IsAny<PumpState>()),
            Times.Once);

        // No se envían PumpAlarm si no hay activas
        _clientMsg.Verify(x =>
            x.SendAsync("sub1", OperationType.PumpAlarm, It.IsAny<object>()),
            Times.Never);
    }

    // --------------------------------------------------------------
    // RETENCIÓN — DeleteOlderDays
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that <c>SaveRequest</c> invokes the observation repository's
    /// <c>DeleteOlderThanDaysAsync</c> method with the configured retention value when the
    /// retention policy returned for the pump observation is <c>DeleteOlderDays</c>.
    /// </summary>
    [Test]
        public async Task SaveRequest_Retention_DeleteOlderDays_CallsRepo()
        {
            var obs = new PumpObservation { Time = Now, DeviceId = "R1", PatientId = PatientId };
            var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" };
    
            _patientSvc.Setup(x => x.FindPatientByApiRequest(req))
                .ReturnsAsync(new Patient { Id = PatientId });
    
            _configPumps.Setup(x => x.RetentionActions(It.IsAny<PumpObservation>()))
                .ReturnsAsync(new ObservatitonRetentionResult
                {
                    RetentionPolicy = RetentionPolicy.DeleteOlderDays,
                    RetentionPolicyValue = 7
                });
    
            await _service.SaveRequest(req);
    
            _obsRepo.Verify(r => r.DeleteOlderThanDaysAsync(7, null), Times.Once);
        }
    // --------------------------------------------------------------
    // PAGINACIÓN
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that <c>GetPaginatedPump</c> returns the correct paginated results when filtering by patient identifier.
    /// Ensures the first page contains the most recent observation within the configured time tolerance.
    /// </summary>
    [Test]
        public async Task GetPaginatedPump_ByPatient_Works()
        {
            var obs = new List<PumpObservation>
            {
                new() { Time = Now.AddMinutes(-1), PatientId = PatientId },
                new() { Time = Now.AddMinutes(-5), PatientId = PatientId }
            };
    
            _obsRepo.Setup(r => r.FindByPatientId(PatientId))
                .ReturnsAsync(obs);
    
            var fileredRequest = new FilteredRequest
            {
                PatientId = PatientId.ToString()
            };
    
            var filter = new PaginationFilter(1, 1, fileredRequest);
    
            var result = await _service.GetPaginatedPump(filter);
    
            using (Assert.EnterMultipleScope())
            {
                Assert.That(result!.Data, Has.Count.EqualTo(1));
                Assert.That(result.Data[0].Time, Is.EqualTo(Now.AddMinutes(-1)).Within(TimeSpan.FromMilliseconds(2)));
            }
        }

    // --------------------------------------------------------------
    // ARCHIVO
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that <c>ArchiveByPatientId</c> inserts the patient's pump observations into the archive
    /// repository and then deletes them, along with their related alarm events and alarm states.
    /// </summary>
    [Test]
        public async Task ArchiveByPatientId_InsertsArchive_ThenDeletes()
        {
            var list = new List<PumpObservation>
            {
                new() { Id = ObjectId.GenerateNewId(), PatientId = PatientId }
            };
    
            _obsRepo.Setup(r => r.FindByPatientId(PatientId)).ReturnsAsync(list);
    
            await _service.ArchiveByPatientId(PatientId);
    
            _archiveRepo.Verify(r => r.InsertManyAsync(list), Times.Once);
            _obsRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once);
            _alarmEventRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once);
            _alarmStateRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once);
        }

    // --------------------------------------------------------------
    // UPDATE MANY
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that <c>UpdateManyObjectId</c> correctly updates the patient identifier across observations, alarms, and alarm state repositories.
    /// </summary>
    /// <returns>A task representing the asynchronous test execution.</returns>
    [Test]
        public async Task UpdateManyObjectId_UpdatesObs_Alarms_States()
        {
            var oldId = ObjectId.GenerateNewId();
            var newId = ObjectId.GenerateNewId();
    
            _obsRepo.Setup(r => r.UpdateManyObjectIdByFieldAsync("patientId", newId, oldId))
                .ReturnsAsync(3);
            _alarmEventRepo.Setup(r => r.UpdateManyObjectIdByFiledNameAsync("patientId", newId, oldId))
                .ReturnsAsync(1);
            _alarmStateRepo.Setup(r => r.UpdateManyObjectIdByFieldNameAsync("patientId", newId, oldId))
                .ReturnsAsync(2);
    
            await _service.UpdateManyObjectId("patientId", newId, oldId);
         
        }

    // --------------------------------------------------------------
    // FIND LAST OBSERVATIONS
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that <c>FindLastPumpObservations</c> returns pump observations ordered with the most recent first,
    /// returning only the specified number of latest entries when the repository provides multiple observations.
    /// </summary>
    [Test]
        public async Task FindLastPumpObservations_ReturnsOrdered()
        {
            var items = new List<PumpObservation>
            {
                new() { Time = Now.AddMinutes(-10) },
                new() { Time = Now.AddMinutes(-1) }
            };
    
            _obsRepo.Setup(r => r.FindByPatientId(PatientId))
                .ReturnsAsync(items);
    
            var result = await _service.FindLastPumpObservations(PatientId, 1);
    
            Assert.That(result, Has.Count.EqualTo(1));
            Assert.That(result[0].Time, Is.EqualTo(items[1].Time));
        }

    // --------------------------------------------------------------
    // INSERT MANUAL
    // --------------------------------------------------------------
    /// <summary>
    /// Verifies that InsertPumpObservation persists the observation and upserts the corresponding
    /// pump state when no existing state is found for the device and there are no active subscribers.
    /// </summary>
    [Test]
        public async Task InsertPumpObservation_Inserts_AndBroadcasts()
        {
            var obs = new PumpObservation
            {
                DeviceId = "D11",
                PatientId = PatientId,
                Time = Now
            };
    
            _stateRepo.Setup(r => r.FindByDeviceIdAsync("D11"))
                .ReturnsAsync((PumpState?)null);
    
            _subs.Setup(x => x.GetSubscribers()).Returns([]);
    
            await _service.InsertPumpObservation(obs);
    
            _obsRepo.Verify(r => r.InsertAsync(It.IsAny<PumpObservation>()), Times.Once);
            _stateRepo.Verify(r => r.UpsertAsync(It.IsAny<PumpState>()), Times.Once);
        }
}