using System.Collections.Specialized;
using adas_core.Application.Services;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Providers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Moq;
using Quartz;
using Quartz.Impl;
using Options = Microsoft.Extensions.Options.Options;

namespace adas_core.Test.Services;

[TestFixture]
public class SchedulerServiceTest
{
    [OneTimeSetUp]
    public async Task SetUp()
    {
        #region [Services Mock seUp]

        _treatmentServiceMock = new Mock<ITreatmentService>();
        _treatmentServiceLazy = new Lazy<ITreatmentService>(() => _treatmentServiceMock.Object);

        _patientServiceMock = new Mock<IPatientService>();
        _patientServiceLazy = new Lazy<IPatientService>(() => _patientServiceMock.Object);

        _medicineServiceMock = new Mock<IMedicineService>();
        _medicineServiceLazy = new Lazy<IMedicineService>(() => _medicineServiceMock.Object);

        _observationServiceMock = new Mock<IObservationService>();
        _observationServiceLazy = new Lazy<IObservationService>(() => _observationServiceMock.Object);

        _configObservationServiceMock = new Mock<IConfigObservationService>();
        var configObservationServiceLazy =
            new Lazy<IConfigObservationService>(() => _configObservationServiceMock.Object);

        _logger = new Mock<ILogger<SchedulerService>>();

        _httpClientFactoryMock = new Mock<IHttpClientFactory>();
        _httpMessageHandlerMock = new Mock<HttpMessageHandler>();

        _calculatedObservationsServiceMock = new Mock<ICalculatedObservationsService>();
        _calculatedObservationsServiceLazy =
            new Lazy<ICalculatedObservationsService>(() => _calculatedObservationsServiceMock.Object);

        _appointmentServiceServiceMock = new Mock<IAppointmentService>();
        _appointmentServiceServiceLazy =
            new Lazy<IAppointmentService>(() => _appointmentServiceServiceMock.Object);
        _diagnosisServiceMock = new Mock<IDiagnosisService>();
        _diagnosisServiceLazy =
            new Lazy<IDiagnosisService>(() => _diagnosisServiceMock.Object);

        var client = new HttpClient(_httpMessageHandlerMock.Object);

        _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny<string>())).Returns(client);

        _optionsApiSettings = Options.Create(_apiSettings);
        _optionsProvidersSettings = Options.Create(_providersSettings);

        #endregion

        var unused = new SchedulerService(
            _optionsApiSettings,
            _optionsProvidersSettings,
            _patientServiceLazy,
            _treatmentServiceLazy,
            _appointmentServiceServiceLazy,
            _diagnosisServiceLazy,
            _medicineServiceLazy,
            _observationServiceLazy,
            configObservationServiceLazy,
            _logger.Object,
            _httpClientFactoryMock.Object,
            _calculatedObservationsServiceLazy,
            _patientCarePlanLazy
        );

        #region [Service item fill]

        _sinceDate = Now.AddHours(-24);
        _sinceDischargeTimeToArchive = 48;
        _archivePatientsWithoutObservationsSinceHours = 28;

        _jobDataMap = new JobDataMap
        {
            { "sinceDate", _sinceDate },
            { "sinceDischargeTime", _sinceDischargeTimeToArchive },
            { "archivePatientsWithoutObservationsSinceHours", _archivePatientsWithoutObservationsSinceHours },
            { "checkNewsJobIntervalMinutes", 15 }
        };

        // Grab the Scheduler instance from the Factory
        var properties = new NameValueCollection
        {
            { "quartz.scheduler.instanceName", "MyUniqueSchedulerName" }
        };
        _factory = new StdSchedulerFactory(properties);
        if (_scheduler == null)
        {
            var getFactory = await _factory.GetScheduler();
            _scheduler = getFactory;
        }

        // Set up observation list
        var itemList = new List<ConfigObservation>
        {
            new()
            {
                Name = "Resp_Rate_Calculated",
                CodingSystem = "ADAS",
                MinAlert = 14,
                MaxAlert = 25,
                Expires = 10,
                ShowOnExpired = false
            },
            new()
            {
                Name = "SpO2",
                Code = "150456",
                OriginalName = "MDC_PULS_OXIM_SAT_O2",
                CodingSystem = "MDC",
                ParentCode = "69965",
                ParentName = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS",
                MinAlert = 94,
                MaxAlert = 100,
                ParentCodingSystem = "MDC",
                Expires = 10,
                ShowOnExpired = false
            },
            new()
            {
                Name = "Temperature",
                Code = "386053000",
                OriginalName = "Temperatura",
                CodingSystem = "SNM",
                ParentCode = "386053000",
                ParentName = "Temperatura(ºC)",
                MinAlert = 34.5,
                MinWarn = 35.5,
                MaxWarn = 37.4,
                MaxAlert = 38,
                ParentCodingSystem = "SNM",
                Expires = 60
            },
            new()
            {
                CodingSystem = "MDC",
                Code = "150033",
                OriginalName = "MDC_PRESS_BLD_ART_SYS",
                Name = "TAs",
                Expires = 10
            },
            new()
            {
                Name = "FC",
                Code = "149514",
                OriginalName = "MDC_PULS_RATE",
                CodingSystem = "MDC",
                MinAlert = 60,
                MaxAlert = 100,
                Expires = 10,
                ShowOnExpired = false
            }
        };
        var observations = new List<PatientObservation>
        {
            new()
            {
                Name = "Resp_Rate_Calculated", Value = 18, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId
            },
            new() { Name = "SpO2", Value = 93, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId },
            new() { Name = "Temperature", Value = 36.5, Time = DateTime.UtcNow, PatientId = PatientId },
            new() { Name = "TAs", Value = 103, Time = DateTime.UtcNow, PatientId = PatientId },
            new() { Name = "FC", Value = 80, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId },
            new()
            {
                Name = "Resp_Rate_Calculated", Value = 18, Time = DateTime.UtcNow.AddMinutes(-60),
                PatientId = PatientId2
            },
            new() { Name = "SpO2", Value = 93, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId2 },
            new()
            {
                Name = "Temperature", Value = 36.5, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId2
            },
            new() { Name = "TAs", Value = 103, Time = DateTime.UtcNow, PatientId = PatientId2 },
            new() { Name = "FC", Value = 40, Time = DateTime.UtcNow, PatientId = PatientId2 }
        };

        // Set Up patientList
        var patientList = new List<Patient>
        {
            new()
            {
                Id = PatientId,
                UnitId = ObjectId.GenerateNewId(),
                PointOfCareId = ObjectId.GenerateNewId(),
                PatientNumber = "patientNumber1",
                Person = new Person
                {
                    FirstName = "firstName1",
                    LastName = "lastName1",
                    BirthDate = new DateTime(),
                    Gender = PatientEnum.Gender.Male
                }
            },
            new()
            {
                Id = PatientId2,
                UnitId = ObjectId.GenerateNewId(),
                PointOfCareId = ObjectId.GenerateNewId(),
                PatientNumber = "patientNumber2",
                Person = new Person
                {
                    FirstName = "firstName2",
                    LastName = "lastName2",
                    BirthDate = new DateTime(),
                    Gender = PatientEnum.Gender.Male
                }
            }
        };

        #endregion

        #region [Service behavior]

        // Set up configObservationServiceMock to return desire item
        _configObservationServiceMock
            .Setup(service => service.Get(It.IsAny<BasePatientObservation>(), It.IsAny<bool>()))
            .ReturnsAsync((BasePatientObservation obs, bool _) =>
            {
                return itemList.FirstOrDefault(obsConfig => obsConfig.Name == obs.Name);
            });
        // Set up observationService behavior
        _observationServiceMock
            .Setup(service => service.FindLastObservations(It.IsAny<ObjectId>(), 1, It.IsAny<List<string>>()))
            .ReturnsAsync((ObjectId patientId, int _, List<string>? filterObservations) =>
            {
                var filteredObservations = observations
                    .Where(obs =>
                        filterObservations != null && obs.Name != null && filterObservations.Contains(obs.Name) &&
                        obs.PatientId == patientId)
                    .ToList();

                return filteredObservations;
            });
        // Set up patientServiceMock to return patientList for FindInActivePoC
        _patientServiceMock.Setup(service => service.FindInActivePoC()).ReturnsAsync(patientList);

        #endregion

        // Asignación de dependencias estáticas para el job
        CalculateNewsJob.PatientService = _patientServiceMock.Object;
        CalculateNewsJob.ObservationService = _observationServiceMock.Object;
        CheckExpiredAlertsJob.ObservationService = _observationServiceMock.Object;
        CheckExpiredObservationsJob.ObservationService = _observationServiceMock.Object;

        CalculateNewsJob.ConfigObservationService = _configObservationServiceMock.Object;
        // and start it off
        _scheduler?.Start();
    }


    //[TearDown]
    [OneTimeTearDown]
    public void TearDown()
    {
        // Detener el motor de Quartz.NET después de las pruebas
        //scheduler.Shutdown().Wait();
        _scheduler?.Shutdown();
    }

    private Mock<ITreatmentService>? _treatmentServiceMock;
    private Mock<IPatientService>? _patientServiceMock;
    private Mock<IMedicineService>? _medicineServiceMock;
    private Mock<IObservationService>? _observationServiceMock;
    private Mock<IConfigObservationService>? _configObservationServiceMock;
    private Mock<IHttpClientFactory>? _httpClientFactoryMock;
    private Mock<HttpMessageHandler>? _httpMessageHandlerMock;
    private Mock<ICalculatedObservationsService>? _calculatedObservationsServiceMock;
    private Mock<IAppointmentService>? _appointmentServiceServiceMock;
    private Mock<IDiagnosisService>? _diagnosisServiceMock;

    private Lazy<ITreatmentService>? _treatmentServiceLazy;
    private Lazy<IPatientService>? _patientServiceLazy;
    private Lazy<IMedicineService>? _medicineServiceLazy;
    private Lazy<IObservationService>? _observationServiceLazy;

    private Lazy<ICalculatedObservationsService> _calculatedObservationsServiceLazy;
    private Lazy<IAppointmentService> _appointmentServiceServiceLazy;
    private Lazy<IDiagnosisService> _diagnosisServiceLazy;
    private readonly Lazy<IPatientCarePlanService> _patientCarePlanLazy = new();

    private readonly ApiSettings _apiSettings = new();
    private IOptions<ApiSettings>? _optionsApiSettings;

    private readonly List<ProvidersSettings> _providersSettings =
    [
        new()
        {
            Name = "Adas",
            Url = "http://localhost/info.html"
        }
    ];

    private IOptions<List<ProvidersSettings>>? _optionsProvidersSettings;

    private Mock<ILogger<SchedulerService>>? _logger;
    private JobDataMap _jobDataMap = new();
    private DateTime _sinceDate;
    private int _sinceDischargeTimeToArchive;
    private int _archivePatientsWithoutObservationsSinceHours;

    private StdSchedulerFactory? _factory;
    private IScheduler? _scheduler;

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

    /// <summary>
    /// Verifies that the <see cref="CheckInactivePatientsJob"/> is executed when its Quartz.NET trigger fires,
    /// ensuring the scheduled job invokes the patient discharge process for inactive patients at the configured interval.
    /// </summary>
    [Test]
        public void CheckInactivePatientsJob_ShouldExecuteOnTrigger()
        {
            var checkInactivePatientSchedulerIntervalHours = 12;
    
            // Arrange
            var checkInactivePatientsJob = JobBuilder.Create<CheckInactivePatientsJob>()
                .UsingJobData(_jobDataMap)
                .Build();
    
            // Crear un disparador personalizado que incremente el contador
            var checkInactivePatientsTrigger = TriggerBuilder.Create()
                .StartNow()
                .WithSimpleSchedule(x => x
                    .WithIntervalInHours(checkInactivePatientSchedulerIntervalHours)
                    .RepeatForever())
                .Build();
    
    
            _patientServiceMock?.Setup(p => p.DischargeInactivePatients(_sinceDate, _sinceDischargeTimeToArchive));
    
            // Asociar el trabajo y el desencadenador en el motor de Quartz.NET
            _scheduler?.ScheduleJob(checkInactivePatientsJob, checkInactivePatientsTrigger).Wait();
    
            // Act
            // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces
            Thread.Sleep(TimeSpan.FromSeconds(1));
    
    
            _patientServiceMock?.Verify(p => p.DischargeInactivePatients(It.IsAny<DateTime>(), It.IsAny<int>()),
                Times.AtLeastOnce());
        }


    /// <summary>
    /// Verifies that the <see cref="CheckActiveTreatmentsJob"/> is executed by the Quartz scheduler
    /// when triggered, and that the medicine service is invoked exactly once to retrieve medicines
    /// for the active treatments of the mocked patients.
    /// </summary>
    [Test]
        public void CheckActiveTreatmentsJob_ShouldExecuteOnTrigger()
        {
            var patient = new Patient
            {
                PatientId = PatientId.ToString(),
                UnitString = "NEONATAL",
                Bed = "CINA02",
                PatientNumber = "123456",
                Person = new Person
                {
                    FirstName = "Jose",
                    LastName = "Luis",
                    BirthDate = new DateTime(),
                    Gender = PatientEnum.Gender.Male
                }
            };
    
            const int checkActiveTreatmentsSchedulerIntervalMinutes = 10;
    
            var treatmentsJob = JobBuilder.Create<CheckActiveTreatmentsJob>()
                .Build();
    
            // Trigger the job to run now, and then repeat every 10 seconds
            var treatmentsTrigger = TriggerBuilder.Create()
                .StartNow()
                .WithSimpleSchedule(x => x
                    .WithIntervalInMinutes(checkActiveTreatmentsSchedulerIntervalMinutes)
                    .RepeatForever())
                .Build();
    
            _patientServiceMock?.Setup(p => p.FindAll(It.IsAny<bool>())).ReturnsAsync([patient]);
    
            // Create a mock of the singleton class subscribers
            var mockSingleton = new Mock<ICalculatedObservationsService>();
    
            // Set up the mock object to return a specific value when a method is called
            var patientTreatmentList = new List<PatientTreatment?>
            {
                new()
                {
                    Id = ObjectId.GenerateNewId(),
                    PatientId = PatientId,
                    OrderControl = OrderControlType.Nw,
                    PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" },
                    FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" },
                    OrderStatus = "A",
                    OrderTime = Now,
                    Notes = [],
                    Routes = []
                }
            };
    
            mockSingleton.Setup(x => x.GetActiveTreatmentsByPatient(PatientId)).ReturnsAsync(patientTreatmentList);
    
    
            _medicineServiceMock?.Setup(m => m.GetMedicinesOfTreatments(It.IsAny<List<PatientTreatment>>()))
                .ReturnsAsync(new List<Medicine>());
    
            // Asociar el trabajo y el desencadenador en el motor de Quartz.NET
            _scheduler?.ScheduleJob(treatmentsJob, treatmentsTrigger).Wait();
    
            // Act
            // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces
            Thread.Sleep(TimeSpan.FromSeconds(10));
    
            _medicineServiceMock?.Verify(x => x.GetMedicinesOfTreatments(It.IsAny<IEnumerable<PatientTreatment>>()),
                Times.Once());
        }

    /// <summary>
    /// Verifies that the <see cref="CheckExpiredObservationsJob"/> is executed at least once by the Quartz.NET scheduler when triggered with a recurring schedule.
    /// </summary>
    [Test]
        public void CheckExpiredObservationsJob_ShouldExecuteOnTrigger()
        {
            var checkExpiredObservationsIntervalMinutes = 3;
    
            // Arrange
            var checkExpiredObservationsJob = JobBuilder.Create<CheckExpiredObservationsJob>()
                .UsingJobData(_jobDataMap)
                .Build();
    
            // Crear un disparador personalizado que incremente el contador
            var checkExpiredObservationsTrigger = TriggerBuilder.Create()
                .StartNow()
                .WithSimpleSchedule(x => x
                    .WithIntervalInHours(checkExpiredObservationsIntervalMinutes)
                    .RepeatForever())
                .Build();
    
            // Asociar el trabajo y el desencadenador en el motor de Quartz.NET
            _scheduler?.ScheduleJob(checkExpiredObservationsJob, checkExpiredObservationsTrigger).Wait();
    
            // Act
            // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces
            Thread.Sleep(TimeSpan.FromSeconds(1));
    
    
            _observationServiceMock?.Verify(p => p.ExpireObservationsAndRecalculateAsync(), Times.AtLeastOnce());
        }


    /// <summary>
    /// Verifies that the <see cref="CheckExpiredAlertsJob"/> is executed when triggered by the Quartz.NET scheduler, ensuring that the <c>ExpireAlertsAndPowerOffAsync</c> method on the observation service is invoked at least once.
    /// </summary>
    [Test]
        public void CheckExpiredAlertsJob_ShouldExecuteOnTrigger()
        {
            var checkExpiredAlertsIntervalMinutes = 3;
    
            // Arrange
            var checkExpiredAlertsJob = JobBuilder.Create<CheckExpiredAlertsJob>()
                .UsingJobData(_jobDataMap)
                .Build();
    
            // Crear un disparador personalizado que incremente el contador
            var checkExpiredAlertsTrigger = TriggerBuilder.Create()
                .StartNow()
                .WithSimpleSchedule(x => x
                    .WithIntervalInHours(checkExpiredAlertsIntervalMinutes)
                    .RepeatForever())
                .Build();
    
            // Asociar el trabajo y el desencadenador en el motor de Quartz.NET
            _scheduler?.ScheduleJob(checkExpiredAlertsJob, checkExpiredAlertsTrigger).Wait();
    
            // Act
            // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces
            Thread.Sleep(TimeSpan.FromSeconds(1));
    
    
            _observationServiceMock?.Verify(p => p.ExpireAlertsAndPowerOffAsync(), Times.AtLeastOnce());
        }

    /// <summary>
    /// Verifies that the <see cref="GetProvidersObservationsJob"/> Quartz.NET job, when triggered, never invokes
    /// <c>InsertObservation</c> with a <see cref="PatientObservation"/> whose <c>Name</c> is not "NEWS", ensuring
    /// that only NEWS observations are considered for persistence during scheduled execution.
    /// </summary>
    [Test]
        public void GetProvidersObservationsJob_ShouldExecuteOnTrigger_Result_GetType_null()
        {
            var getProviderObservationsIntervalMinutes = 5;
    
            var patient = new Patient
            {
                PatientId = PatientId.ToString(),
                UnitString = "NEONATAL",
                Bed = "CINA02",
                PatientNumber = "123456",
                Person = new Person
                {
                    FirstName = "Jose",
                    LastName = "Luis",
                    BirthDate = new DateTime(),
                    Gender = PatientEnum.Gender.Male
                }
            };
    
            // Arrange
            var getProvidersObservationsJob = JobBuilder.Create<GetProvidersObservationsJob>()
                .UsingJobData(_jobDataMap)
                .Build();
    
            // Crear un disparador personalizado que incremente el contador
            var getProvidersObservationsTrigger = TriggerBuilder.Create()
                .StartNow()
                .WithSimpleSchedule(x => x
                    .WithIntervalInHours(getProviderObservationsIntervalMinutes)
                    .RepeatForever())
                .Build();
    
            _patientServiceMock?.Setup(p => p.FindAll(It.IsAny<bool>())).ReturnsAsync([patient]);
    
            // Asociar el trabajo y el desencadenador en el motor de Quartz.NET
            _scheduler?.ScheduleJob(getProvidersObservationsJob, getProvidersObservationsTrigger).Wait();
    
            // Act
            // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces
            Thread.Sleep(TimeSpan.FromSeconds(1));
    
            //observationServiceMock.Verify(p => p.InsertObservation(It.IsAny<PatientObservation>(),true,true), Times.AtLeastOnce());
            _observationServiceMock?.Verify(p => p.InsertObservation(
                It.Is<PatientObservation>(obs => obs.Name != "NEWS"), true, true), Times.Never());
        }

    /// <summary>
    /// Verifies that the <see cref="CalculateNewsJob"/> is executed when triggered by the Quartz.NET scheduler, confirming that the associated <c>InsertObservation</c> call on the observation service is invoked at least once within the allowed execution window.
    /// </summary>
    [Test]
        public void CheckCalculateNewsJob_ShouldExecuteOnTrigger()
        {
            var checkExpiredAlertsIntervalMinutes = 3;
    
            // Arrange
            var calculateNewsJob = JobBuilder.Create<CalculateNewsJob>()
                .UsingJobData(_jobDataMap)
                .Build();
    
            // Crear un disparador personalizado que incremente el contador
            var checkExpiredAlertsTrigger = TriggerBuilder.Create()
                .StartNow()
                .WithSimpleSchedule(x => x
                    .WithIntervalInHours(checkExpiredAlertsIntervalMinutes)
                    .RepeatForever())
                .Build();
    
            // Asociar el trabajo y el desencadenador en el motor de Quartz.NET
            _scheduler?.ScheduleJob(calculateNewsJob, checkExpiredAlertsTrigger).Wait();
    
            // Act
            // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces
            Thread.Sleep(TimeSpan.FromSeconds(6));
    
    
            _observationServiceMock?.Verify(p => p.InsertObservation(It.IsAny<PatientObservation>(), true, true),
                Times.AtLeastOnce());
        }

    /// <summary>
    /// Verifies that <see cref="CalculateNewsJob.Execute"/> correctly inserts NEWS observations with the expected calculated values (1 and 4) for multiple patients in a single execution.
    /// </summary>
    [Test]
        public async Task CalculateNewsJob_Insert_ObsFull_MultiplePatient_With_Correct_Value()
        {
            var job = new CalculateNewsJob();
    
            // Act
            await job.Execute(Mock.Of<IJobExecutionContext>());
    
            // Assert
            _observationServiceMock?.Verify(
                service => service.InsertObservation(
                    It.Is<PatientObservation>(obs =>
                        obs.Name == "NEWS" && (int)obs.Value == 1 && obs.PatientId == PatientId),
                    true, true),
                Times.Once);
    
            _observationServiceMock?.Verify(
                service => service.InsertObservation(
                    It.Is<PatientObservation>(obs =>
                        obs.Name == "NEWS" && (int)obs.Value == 4 && obs.PatientId == PatientId2),
                    true, true),
                Times.Once);
        }
    //Se deshabilita el mensaje de warning porque lo detecta como no usado y sugiere suprimirlo siendo necesario
}