using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Pumps;
using adas_core.Infrastructure.Repositories;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;

namespace adas_core.Test.Repositories;

[TestFixture]
[Category("Integration")]
public class PumpAlarmEventRepositoryTest
{
    private PumpAlarmEventRepository _repo;
    private IOptions<ApiSettings> _apiSettings;

    private static readonly DateTime Now = DateTime.UtcNow;
    private const string DeviceA = "Device-A";
    private const string DeviceB = "Device-B";
    private static readonly ObjectId PatientId = ObjectId.GenerateNewId();

    // -------------------------------------------------------------------
    // INIT – Setup inicial de colección con índices y datos de prueba
    // -------------------------------------------------------------------
    /// <summary>
    /// Performs one-time integration test setup for the pump alarm event repository by configuring API settings,
    /// resetting the dedicated MongoDB collection, creating required indexes, and seeding it with three initial
    /// alarm events covering both Device and DeviceB across attention and occlusion
    /// alarm types. Verifies that exactly three documents are present after seeding.
    /// </summary>
    [OneTimeSetUp]
        public async Task Init()
        {
            _apiSettings = Options.Create(new ApiSettings
            {
                PumpAlarmEvent = "pump_alarm_event"
            });
    
            await IntegrationDb.Database.DropCollectionAsync("pump_alarm_event");
            await IntegrationDb.Database.CreateCollectionAsync("pump_alarm_event");
    
            _repo = new PumpAlarmEventRepository(
                _apiSettings,
                IntegrationDb.Database
            );
    
            await _repo.CreateIndexes();
    
            var initialEvents = new List<PumpAlarmEvent>
            {
                new()
                {
                    Id = ObjectId.GenerateNewId(),
                    DeviceId = DeviceA,
                    PatientId = PatientId,
                    AlarmType = PumpEnum.AlarmType.Attention,
                    Time = Now.AddSeconds(-1)
                },
                new()
                {
                    Id = ObjectId.GenerateNewId(),
                    DeviceId = DeviceA,
                    PatientId = PatientId,
                    AlarmType = PumpEnum.AlarmType.Occlusion,
                    Time = Now.AddSeconds(-3)
                },
                new()
                {
                    Id = ObjectId.GenerateNewId(),
                    DeviceId = DeviceB,
                    PatientId = PatientId,
                    AlarmType = PumpEnum.AlarmType.Occlusion,
                    Time = Now.AddSeconds(-2)
                }
            };
    
            await IntegrationDb.Database
                .GetCollection<PumpAlarmEvent>("pump_alarm_event")
                .InsertManyAsync(initialEvents);
    
            var count = await _repo.Collection.CountDocumentsAsync(_ => true);
            Assert.That(count, Is.EqualTo(3));
        }

    // -------------------------------------------------------------------
    // INSERT
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that InsertAsync correctly persists a <see cref="PumpAlarmEvent"/> to the repository by inserting an event and confirming it can be retrieved by its identifier.
    /// </summary>
    [Test]
        public async Task InsertAsync_Works()
        {
            var evt = new PumpAlarmEvent
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-C",
                PatientId = PatientId,
                AlarmType = PumpEnum.AlarmType.Occlusion,
                Time = Now
            };
    
            await _repo.InsertAsync(evt);
    
            var found = await _repo.Collection.FindAsync(x => x.Id == evt.Id);
            Assert.That(found.FirstOrDefault(), Is.Not.Null);
        }

    // -------------------------------------------------------------------
    // FIND BY DEVICE ID
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that <c>FindByDeviceIdAsync</c> returns a non-empty collection of records for the specified device, ordered by time so that the most recent entry appears first.
    /// </summary>
    [Test]
        public async Task FindByDeviceIdAsync_ReturnsOrdered()
        {
            var result = await _repo.FindByDeviceIdAsync(DeviceA);
    
            var list = result.ToList();
    
            using (Assert.EnterMultipleScope())
            {
                Assert.That(list, Is.Not.Empty);
                Assert.That(list.First().Time, Is.GreaterThanOrEqualTo(list.Last().Time));
            }
        }

    /// <summary>
    /// Verifies that <c>FindByDeviceIdAsync</c> returns only pump alarm events whose timestamps fall within the specified date range.
    /// </summary>
    [Test]
        public async Task FindByDeviceIdAsync_WithDateRange_Works()
        {
            var from = Now.AddSeconds(-2);
            var to = Now;
    
            var result = await _repo.FindByDeviceIdAsync(DeviceA, from, to);
    
            using (Assert.EnterMultipleScope())
            {
                var pumpAlarmEvents = result.ToList();
                Assert.That(pumpAlarmEvents, Is.Not.Empty);
                Assert.That(pumpAlarmEvents.All(x => x.Time >= from && x.Time <= to), Is.True);
            }
        }

    /// <summary>
    /// Verifies that <c>FindByDeviceIdAsync</c> returns at most the specified number of results when a limit is provided.
    /// </summary>
    [Test]
        public async Task FindByDeviceIdAsync_WithLimit_Works()
        {
            var result = await _repo.FindByDeviceIdAsync(DeviceA, limit: 1);
    
            var list = result.ToList();
            Assert.That(list, Has.Count.EqualTo(1));
        }

    // -------------------------------------------------------------------
    // FIND LAST BY DEVICE
    // -------------------------------------------------------------------
    [Test]
    public async Task FindLastByDeviceIdAsync_Works()
    {
        // Arrange: dataset ya insertado en Init()

        var events = await _repo.FindByDeviceIdAsync("Device-A");
        var expected = events.OrderByDescending(e => e.Time).First();

        // Act
        var evt = await _repo.FindLastByDeviceIdAsync("Device-A");

        // Assert
        Assert.That(evt, Is.Not.Null);
        Assert.That(evt!.Id, Is.EqualTo(expected.Id)); // comparación robusta
    }

    // -------------------------------------------------------------------
    // DELETE BY PATIENT ID
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that DeleteByPatientId correctly removes all alarm events associated with a given patient.
    /// Inserts a sample <c>PumpAlarmEvent</c> for a specific patient, deletes it by patient ID, and asserts that no matching events remain in the collection.
    /// </summary>
    [Test]
        public async Task DeleteByPatientId_Works()
        {
            var patient2 = ObjectId.GenerateNewId();
    
            var evt = new PumpAlarmEvent
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-X",
                PatientId = patient2,
                AlarmType = PumpEnum.AlarmType.Occlusion,
                Time = Now
            };
    
            await _repo.InsertAsync(evt);
    
            await _repo.DeleteByPatientId(patient2);
    
            var found = await _repo.Collection.FindAsync(x => x.PatientId == patient2);
            Assert.That(await found.AnyAsync(), Is.False);
        }

    // -------------------------------------------------------------------
    // UPDATE MANY (CAMBIO DE PATIENTID O SIMILAR)
    // -------------------------------------------------------------------

    /// <summary>
    /// Verifies that <c>UpdateManyObjectIdByFiledNameAsync</c> correctly updates documents matching the specified field name and old ObjectId value, replacing it with the new ObjectId, and that the changes are persisted and queryable.
    /// </summary>
    [Test]
        public async Task UpdateManyObjectIdByFiledNameAsync_Works()
        {
            var oldId = ObjectId.GenerateNewId();
            var newId = ObjectId.GenerateNewId();
    
            var evt = new PumpAlarmEvent
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-U",
                PatientId = oldId,
                AlarmType = PumpEnum.AlarmType.Attention,
                Time = Now
            };
    
            await _repo.InsertAsync(evt);
    
            var updated = await _repo.UpdateManyObjectIdByFiledNameAsync("PatientId", newId, oldId);
    
            Assert.That(updated, Is.EqualTo(1));
    
            var found = await _repo.Collection.FindAsync(x => x.PatientId == newId);
            Assert.That(await found.AnyAsync(), Is.True);
        }
    
    /// <summary>
    /// Verifies that <c>FindByDeviceIdAsync</c> returns an empty result when the queried date range falls outside of the available data window.
    /// </summary>
    [Test]
        public async Task FindByDeviceIdAsync_ReturnsEmpty_WhenOutOfRange()
        {
            var from = Now.AddYears(-2);
            var to = Now.AddYears(-2).AddDays(1);
    
            var result = await _repo.FindByDeviceIdAsync("Device-A", from, to);
    
            Assert.That(result, Is.Empty);
        }
    
    /// <summary>
    /// Verifies that <c>FindByDeviceIdAsync</c> correctly applies the requested limit and orders results so that the most recent alarm event for the given device is returned first.
    /// Seeds multiple events for the target device, requests a single record, and asserts that the returned entry matches the latest event retrieved by <c>FindLastByDeviceIdAsync</c>.
    /// </summary>
    [Test]
        public async Task FindByDeviceIdAsync_RespectsLimitAndOrder()
        {
            // Semilla adicional para asegurar > 1
            var events = new[]
            {
                new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = DeviceA, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.AirInLine,  Time = Now.AddMilliseconds(-10) },
                new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = DeviceA, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Occlusion, Time = Now.AddMilliseconds(-5) }
            };
            await _repo.Collection.InsertManyAsync(events);
    
            var result = await _repo.FindByDeviceIdAsync(DeviceA, limit: 1);
            var list = result.ToList();
    
            using (Assert.EnterMultipleScope())
            {
                Assert.That(list, Has.Count.EqualTo(1));
                var last = await _repo.FindLastByDeviceIdAsync(DeviceA);
                Assert.That(list[0].Id, Is.EqualTo(last!.Id));
            }
        }
    
    /// <summary>
    /// Verifies that InsertAsync successfully persists a <see cref="PumpAlarmEvent"/> when its optional fields (<c>PatientId</c> and <c>AlarmType</c>) are <c>null</c>, and that the record can be retrieved afterwards.
    /// </summary>
    [Test]
        public async Task InsertAsync_AllowsNullOptionalFields()
        {
            var evt = new PumpAlarmEvent
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-Nulls",
                PatientId = null,                // opcional
                AlarmType = null,                // opcional
                Time = Now
            };
    
            await _repo.InsertAsync(evt);
    
            var found = await _repo.Collection.FindAsync(x => x.Id == evt.Id);
            Assert.That(found.FirstOrDefault(), Is.Not.Null);
        }

    /// <summary>
    /// Verifies that <c>DeleteByPatientId</c> removes all alarm events associated with the given patient and can be invoked repeatedly without throwing once the records no longer exist.
    /// </summary>
    [Test]
        public async Task DeleteByPatientId_DeletesMany_AndIsIdempotent()
        {
            var pid = ObjectId.GenerateNewId();
    
            var docs = Enumerable.Range(0, 3).Select(i => new PumpAlarmEvent
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-Del",
                PatientId = pid,
                AlarmType = PumpEnum.AlarmType.Attention,
                Time = Now.AddSeconds(-i)
            });
    
            await _repo.Collection.InsertManyAsync(docs);
    
            // Primera vez: borra 3
            await _repo.DeleteByPatientId(pid);
            var left = await _repo.Collection.FindAsync(x => x.PatientId == pid);
            Assert.That(left.ToList(), Is.Empty);
    
            Func<Task> act = async () => await _repo.DeleteByPatientId(pid);
            Assert.That(act, Throws.Nothing);
        }

    
    /// <summary>
    /// Verifies that <c>UpdateManyObjectIdByFiledNameAsync</c> updates the <c>PatientId</c> field from an old <see cref="ObjectId"/> to a new one across all matching documents, returning the modified count and persisting the new identifier in the collection.
    /// </summary>
    [Test]
        public async Task UpdateManyObjectIdByFiledNameAsync_UpdatesMultiple()
        {
            var oldId = ObjectId.GenerateNewId();
            var newId = ObjectId.GenerateNewId();
    
            var events = Enumerable.Range(0, 4).Select(i => new PumpAlarmEvent
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-U",
                PatientId = oldId,
                AlarmType = PumpEnum.AlarmType.AirInLine,
                Time = Now.AddMilliseconds(-i)
            });
    
            await _repo.Collection.InsertManyAsync(events);
    
            var modified = await _repo.UpdateManyObjectIdByFiledNameAsync("PatientId", newId, oldId);
    
            using (Assert.EnterMultipleScope())
            {
                Assert.That(modified, Is.EqualTo(4));
                var foundNew = await _repo.Collection.FindAsync(x => x.PatientId == newId);
                Assert.That(foundNew.ToList(), Has.Count.EqualTo(4));
            }
        }
    /// <summary>
    /// Verifies that <c>FindByDeviceIdAsync</c> treats the <c>from</c> and <c>to</c> time bounds as inclusive,
    /// ensuring events whose timestamp matches the upper bound exactly are returned and that all returned
    /// events fall within the supplied range.
    /// </summary>
    [Test]
        public async Task FindByDeviceIdAsync_InclusiveRange_BoundsRespected()
        {
            var start = Now.AddMinutes(-30);
            var end = Now.AddMinutes(-29);
    
            var exactEvt = new PumpAlarmEvent
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-Range",
                PatientId = PatientId,
                AlarmType = PumpEnum.AlarmType.Attention,
                Time = end // exactamente igual al to
            };
    
            await _repo.InsertAsync(exactEvt);
    
            var result = await _repo.FindByDeviceIdAsync("Device-Range", from: start, to: end);
    
            using (Assert.EnterMultipleScope())
            {
                var pumpAlarmEvents = result as PumpAlarmEvent[] ?? result.ToArray();
                Assert.That(pumpAlarmEvents, Is.Not.Empty);
                Assert.That(pumpAlarmEvents.All(r => r.Time >= start && r.Time <= end), Is.True);
            }
        }
    
    /// <summary>
    /// Verifies that FindLastByDeviceIdAsync returns the most recent alarm event for a given device and tracks newly inserted events as the latest entry.
    /// </summary>
    [Test]
        public async Task FindLastByDeviceIdAsync_TracksNewest()
        {
            const string dev = "Device-LastCheck";
    
            var e1 = new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.AirInLine,  Time = Now.AddSeconds(-10) };
            var e2 = new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Occlusion, Time = Now.AddSeconds(-5) };
    
            await _repo.Collection.InsertManyAsync([e1, e2]);
    
            var last1 = await _repo.FindLastByDeviceIdAsync(dev);
            Assert.That(last1!.Id, Is.EqualTo(e2.Id));
    
            var e3 = new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Attention, Time = Now };
            await _repo.InsertAsync(e3);
    
            var last2 = await _repo.FindLastByDeviceIdAsync(dev);
            Assert.That(last2!.Id, Is.EqualTo(e3.Id));
        }
    
    /// <summary>
    /// Verifies that <c>FindByDeviceIdAsync</c> correctly filters pump alarm events by device identifier
    /// and time range when multiple queries are issued against overlapping windows, ensuring each
    /// range returns only the event that falls within its boundaries.
    /// </summary>
    [Test]
        public async Task FindByDeviceIdAsync_MultipleOverlappingRanges()
        {
            const string dev = "Device-Timeline";
    
            var batch = new[]
            {
                new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.AirInLine,  Time = Now.AddMinutes(-20) },
                new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Occlusion, Time = Now.AddMinutes(-10) },
                new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Attention, Time = Now.AddMinutes(-5)  }
            };
    
            await _repo.Collection.InsertManyAsync(batch);
    
            var r1 = await _repo.FindByDeviceIdAsync(dev, from: Now.AddMinutes(-30), to: Now.AddMinutes(-15));
            var r2 = await _repo.FindByDeviceIdAsync(dev, from: Now.AddMinutes(-12), to: Now.AddMinutes(-8));
            var r3 = await _repo.FindByDeviceIdAsync(dev, from: Now.AddMinutes(-6), to: Now);
    
            using (Assert.EnterMultipleScope())
            {
                var r1List = r1.ToList();
                Assert.That(r1List, Has.Count.EqualTo(1));  // -20
                var r2List = r2.ToList();
                Assert.That(r2List, Has.Count.EqualTo(1));  // -10
                var r3List = r3.ToList();
                Assert.That(r3List, Has.Count.EqualTo(1));  // -5
            }
        }
    
}
