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;

/// <summary>
/// Integration test fixture for the <see cref="PumpObservationRepository"/>.
/// </summary>
/// <remarks>
/// Marked with the <c>Integration</c> category, so these tests target interactions with external dependencies rather than isolated unit logic.
/// </remarks>
/// <!-- aidoc:v1 sig=c9f19af -->
[TestFixture]
[Category("Integration")]
public class PumpObservationRepositoryTest
{
    private PumpObservationRepository _repo;
    private IOptions<ApiSettings> _apiSettings;

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

    // -------------------------------------------------------------------
    // INIT – SETUP DE LA COLECCIÓN CON DATOS DE PRUEBA
    // -------------------------------------------------------------------
    /// <summary>
    /// One-time setup that prepares the integration test environment for the <see cref="PumpObservationRepository"/> by clearing the
    /// <c>pump_observations</c> collection, recreating it, building the indexes, and seeding it with three sample <see cref="PumpObservation"/>
    /// records belonging to <see cref="BasePatientObservation.PatientId"/>. The setup then asserts that exactly three documents were
    /// inserted into the collection.
    /// </summary>
    /// <!-- aidoc:v1 sig=81c0015 body=162df76 -->
    [OneTimeSetUp]
    public async Task Init()
    {
        _apiSettings = Options.Create(new ApiSettings
        {
            PumpObservations = "pump_observations"
        });

        await IntegrationDb.Database.DropCollectionAsync("pump_observations");
        await IntegrationDb.Database.CreateCollectionAsync("pump_observations");

        _repo = new PumpObservationRepository(
            _apiSettings,
            IntegrationDb.Database
        );

        await _repo.CreateIndexes();

        var samples = new List<PumpObservation>
        {
            new()
            {
                Id = ObjectId.GenerateNewId(),
                PatientId = PatientId,
                DeviceId = "Device-A",
                Name = "PumpX",
                Time = Now.AddSeconds(-1),
                MessageType = PumpEnum.PumpMessageType.Observation
            },
            new()
            {
                Id = ObjectId.GenerateNewId(),
                PatientId = PatientId,
                DeviceId = "Device-A",
                Name = "PumpY",
                Time = Now.AddSeconds(-2),
                MessageType = PumpEnum.PumpMessageType.Observation
            },
            new()
            {
                Id = ObjectId.GenerateNewId(),
                PatientId = PatientId,
                DeviceId = "Device-A",
                Name = "PumpX",
                Time = Now.AddSeconds(-3),
                MessageType = PumpEnum.PumpMessageType.Observation
            }
        };

        await _repo.InsertManyAsync(samples);

        var count = await _repo.Collection.CountDocumentsAsync(_ => true);
        Assert.That(count, Is.EqualTo(3));
    }

    // -------------------------------------------------------------------
    // FIND LAST OBSERVATIONS (LÍMITE = 2)
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that the repository returns exactly two observations for the specified patient and "PumpX", and that the returned list is ordered with the first observation's time greater than or equal to the second observation's time.
    /// </summary>
    /// <!-- aidoc:v1 sig=3214dea body=70daa63 -->
    [Test]
    public async Task FindLastObservations_ReturnsTwo()
    {
        var list = await _repo.FindLastObservations(PatientId, "PumpX");

        Assert.That(list, Is.Not.Null);
        Assert.That(list, Has.Count.EqualTo(2));
        Assert.That(list[0].Time, Is.GreaterThanOrEqualTo(list[1].Time));
    }

    // -------------------------------------------------------------------
    // INSERT
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="IRepository{PumpObservation}.InsertAsync"/> persists a new <see cref="PumpObservation"/> so that a subsequent lookup by <see cref="BasePatientObservation.Id"/> returns the stored entity.
    /// </summary>
    /// <!-- aidoc:v1 sig=2215003 body=9eae88d -->
    [Test]
    public async Task InsertAsync_Works()
    {
        var pump = new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            PatientId = ObjectId.GenerateNewId(),
            DeviceId = "Device-B",
            Time = Now
        };

        await _repo.InsertAsync(pump);

        var found = await _repo.Collection.FindAsync(x => x.Id == pump.Id);
        Assert.That(found.FirstOrDefault(), Is.Not.Null);
    }

    /// <summary>
    /// Verifies that the repository successfully inserts a batch of <see cref="PumpObservation"/> documents and that all items can be retrieved by their identifiers from the underlying collection.
    /// </summary>
    /// <!-- aidoc:v1 sig=5a5d3db body=8d0d26b -->
    [Test]
    public async Task InsertManyAsync_Works()
    {
        var list = new List<PumpObservation>
        {
            new() { Id = ObjectId.GenerateNewId(), DeviceId="D", Time=Now },
            new() { Id = ObjectId.GenerateNewId(), DeviceId="D", Time=Now }
        };

        await _repo.InsertManyAsync(list);

        var count = await _repo.Collection.CountDocumentsAsync(x => list.Select(y => y.Id).Contains(x.Id));
        Assert.That(count, Is.EqualTo(2));
    }

    // -------------------------------------------------------------------
    // FIND METHODS
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that the FindByDeviceIdAsync repository method returns a non-empty collection of pump observations for the specified device, ordered by time in descending order with the most recent observation first.
    /// </summary>
    /// <!-- aidoc:v1 sig=01527dc body=3a6c4b2 -->
    [Test]
    public async Task FindByDeviceIdAsync_ReturnsOrdered()
    {
        var result = await _repo.FindByDeviceIdAsync("Device-A");

        var pumpObservations = result.ToList();
        using (Assert.EnterMultipleScope())
        {
            Assert.That(pumpObservations, Is.Not.Empty);
            Assert.That(pumpObservations.First().Time, Is.GreaterThanOrEqualTo(pumpObservations.Last().Time));
        }
    }

    /// <summary>
    /// Verifies that the repository's <c>FindByPatientAsync</c> call returns a non-empty collection of observations for the configured <c>PatientId</c>, ordered by time in descending order so that the most recent observation appears first.
    /// </summary>
    /// <!-- aidoc:v1 sig=1926bb1 body=010dd96 -->
    [Test]
    public async Task FindByPatientAsync_ReturnsOrdered()
    {
        var result = await _repo.FindByPatientAsync(PatientId);

        var pumpObservations = result.ToList();
        using (Assert.EnterMultipleScope())
        {
            Assert.That(pumpObservations, Is.Not.Empty);
            Assert.That(pumpObservations.First().Time, Is.GreaterThanOrEqualTo(pumpObservations.Last().Time));
        }
    }

    /// <summary>
    /// Verifies that <see cref="Repository.FindLastByDeviceIdAsync"/> returns a non-null observation whose <see cref="BasePatientObservation.DeviceId"/> matches the device identifier passed in.
    /// </summary>
    /// <!-- aidoc:v1 sig=eeea0f1 body=d0bcd5d -->
    [Test]
    public async Task FindLastByDeviceIdAsync_Works()
    {
        var obs = await _repo.FindLastByDeviceIdAsync("Device-A");

        Assert.That(obs, Is.Not.Null);
        Assert.That(obs.DeviceId, Is.EqualTo("Device-A"));
    }

    /// <summary>
    /// Verifies that the repository lookup by patient identifier returns a non-empty collection of results.
    /// </summary>
    /// <!-- aidoc:v1 sig=363130a body=c6821a0 -->
    [Test]
    public async Task FindByPatientId_Works()
    {
        var list = await _repo.FindByPatientId(PatientId);

        Assert.That(list.Any(), Is.True);
    }

    // -------------------------------------------------------------------
    // AGGREGATED LAST OBSERVATIONS
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that the aggregated last observations retrieved for the test <c>PatientId</c> are distinct by name, ensuring the resulting collection is non-empty and contains at most two entries.
    /// </summary>
    /// <!-- aidoc:v1 sig=878cf7b body=9a03f04 -->
    /// <!-- aidoc-review:v1 severity=medium kind=wrong_summary
    ///      "The summary states the test 'verifies that the aggregated last observations... are distinct by name', but the assertions only check that the result is non-empty and has at most two entries — no distinctness-by-name check is performed." -->
    [Test]
    public async Task AggregatedPatientLastObservations_DistinctByName()
    {
        var result = await _repo.AggregatedPatientLastObservations(PatientId);

        using (Assert.EnterMultipleScope())
        {
            Assert.That(result, Is.Not.Empty);
            Assert.That(result, Has.Count.LessThanOrEqualTo(2));
        }
    }

    // -------------------------------------------------------------------
    // DELETE METHODS
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="PumpObservation"/> records associated with a given patient can be removed via <c>DeleteByPatientId</c>, ensuring no matching observations remain in the repository after deletion.
    /// </summary>
    /// <!-- aidoc:v1 sig=d53a759 body=16f0ddb -->
    [Test]
    public async Task DeleteByPatientId_Works()
    {
        var pid = ObjectId.GenerateNewId();

        await _repo.InsertAsync(new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            PatientId = pid,
            DeviceId = "Z",
            Time = Now
        });

        await _repo.DeleteByPatientId(pid);

        var list = await _repo.FindByPatientId(pid);
        Assert.That(list.Any(), Is.False);
    }

    /// <summary>
    /// Tests that <c>DeleteOlderThanDaysAsync</c> successfully deletes pump observations older than the given number of days, asserting that the returned count is at least one when matching data exists.
    /// </summary>
    /// <!-- aidoc:v1 sig=eedf26a body=5bdb401 -->
    [Test]
    public async Task DeleteOlderThanDaysAsync_Works()
    {
        var pid = ObjectId.GenerateNewId();

        await _repo.InsertAsync(new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            PatientId = pid,
            Name = "TestOld",
            Time = Now.AddDays(-10)
        });

        var deleted = await _repo.DeleteOlderThanDaysAsync(5);

        Assert.That(deleted, Is.GreaterThanOrEqualTo(1));
    }

    /// <summary>
    /// Verifies that the older-observation pruning operation retains only the requested number of newest <see cref="PumpObservation"/> entries for the given name and returns the count of deleted items.
    /// Inserts five timed observations and asserts that exactly three older entries are removed when two are kept.
    /// </summary>
    /// <!-- aidoc:v1 sig=d7625b7 body=dc1036f -->
    [Test]
    public async Task DeleteOlderNumberAsync_Works()
    {
        const string name = "HistoryPump";

        var obs = Enumerable.Range(0, 5).Select(i =>
            new PumpObservation
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "H",
                Name = name,
                Time = Now.AddSeconds(-i)
            });

        await _repo.InsertManyAsync(obs);

        var deleted = await _repo.DeleteOlderNumberAsync(name, 2);

        Assert.That(deleted, Is.EqualTo(3));
    }

    /// <summary>
    /// Verifies that <see cref="_repo.DeleteKeepLastNAsync"/> correctly deletes the older records while preserving the most recent <paramref name="n"/> entries, returning the expected count of deleted items.
    /// </summary>
    /// <!-- aidoc:v1 sig=6f857ff body=814bec4 -->
    /// <!-- aidoc-review:v1 severity=low kind=wrong_param_role
    ///      "<paramref name=\"n\"/> references a parameter named 'n' but the test method DeleteKeepLastNAsync_Works has no parameters; 'n' is actually an argument passed to the called method DeleteKeepLastNAsync(3)." -->
    [Test]
    public async Task DeleteKeepLastNAsync_Works()
    {
        var items = Enumerable.Range(0, 8).Select(i =>
            new PumpObservation
            {
                Id = ObjectId.GenerateNewId(),
                DeviceId = "Device-Clean",
                Time = Now.AddSeconds(-i)
            });

        await _repo.InsertManyAsync(items);

        var deleted = await _repo.DeleteKeepLastNAsync(3);

        Assert.That(deleted, Is.EqualTo(5));
    }

    // -------------------------------------------------------------------
    // UPDATE METHOD
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="Repository{BasePatientObservation}.UpdateManyObjectIdByFieldAsync"/> correctly updates the <see cref="BasePatientObservation.PatientId"/> field of all matching records, replacing the old <see cref="MongoDB.Bson.ObjectId"/> value with a new one, and reports the number of documents modified.
    /// </summary>
    /// <!-- aidoc:v1 sig=e139730 body=639f796 -->
    [Test]
    public async Task UpdateManyObjectIdByFieldAsync_Works()
    {
        var oldId = ObjectId.GenerateNewId();
        var newId = ObjectId.GenerateNewId();

        await _repo.InsertAsync(new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            PatientId = oldId,
            DeviceId = "UpdateTest",
            Time = Now
        });

        var updated = await _repo.UpdateManyObjectIdByFieldAsync("PatientId", newId, oldId);

        Assert.That(updated, Is.EqualTo(1));

        var found = await _repo.FindByPatientId(newId);
        Assert.That(found.Any(), Is.True);
    }

    // -------------------------------------------------------------------
    // AGGREGATION: LAST OBSERVATION TIME PER PATIENT
    // -------------------------------------------------------------------
    /// <summary>
    /// Verifies that the repository returns a non-null dictionary containing the last patient observation
    /// time for the specified patient, and that the recorded timestamp is not in the future relative to
    /// <see cref="DateTime.UtcNow"/>.
    /// </summary>
    /// <!-- aidoc:v1 sig=1accf32 body=8a4a652 -->
    [Test]
    public async Task FindAllLastPatientObservationTimeAsync_Works()
    {
        var dict = await _repo.FindAllLastPatientObservationTimeAsync();

        Assert.That(dict, Is.Not.Null);
        using (Assert.EnterMultipleScope())
        {
            Assert.That(dict.ContainsKey(PatientId), Is.True);
            Assert.That(dict[PatientId], Is.LessThanOrEqualTo(DateTime.UtcNow));
        }
    }
    
    /// <summary>
    /// Verifies that looking up records by device ID with a date range entirely outside the stored data window returns an empty collection.
    /// </summary>
    /// <!-- aidoc:v1 sig=c885ffe body=2c99833 -->
    [Test]
    public async Task FindByDeviceIdAsync_EmptyWhenOutOfDateRange()
    {
        var from = Now.AddYears(-1);
        var to = Now.AddYears(-1).AddHours(1);

        var result = await _repo.FindByDeviceIdAsync("Device-A", from, to);

        Assert.That(result, Is.Empty);
    }
    
    /// <summary>
    /// Verifies that the repository's FindByPatientAsync method respects the <c>limit</c> parameter when retrieving records for a patient, asserting that the returned collection contains exactly one item when the limit is set to one.
    /// </summary>
    /// <!-- aidoc:v1 sig=4955050 body=a2ef54e -->
    [Test]
    public async Task FindByPatientAsync_RespectsLimit()
    {
        var result = await _repo.FindByPatientAsync(PatientId, limit: 1);
        var list =  result.ToList();
        Assert.That(list, Has.Count.EqualTo(1));
    }
    
    /// <summary>
    /// Verifies that FindLastByDeviceIdAsync returns null when no observation exists for the supplied device identifier.
    /// </summary>
    /// <!-- aidoc:v1 sig=275de54 body=b35493b -->
    [Test]
    public async Task FindLastByDeviceIdAsync_ReturnsNullWhenNotExists()
    {
        var obs = await _repo.FindLastByDeviceIdAsync("Device-DoesNotExist");

        Assert.That(obs, Is.Null);
    }

    /// <summary>
    /// Verifies that InsertManyAsync completes without throwing when invoked with an empty collection of <see cref="PumpObservation"/>.
    /// </summary>
    /// <!-- aidoc:v1 sig=a2eefa5 body=d0db476 -->
    [Test]
    public async Task InsertManyAsync_IgnoresEmptyList()
    {
        var empty = new List<PumpObservation>();

        Func<Task> act = () => _repo.InsertManyAsync(empty);

        Assert.That(act, Throws.Nothing);
    }

    
    /// <summary>
    /// Verifies that <c>DeleteOlderNumberAsync</c> leaves the stored data unchanged and returns zero when the total number of <see cref="PumpObservation"/> entries for the given device name is below the specified <c>maxCount</c> threshold.
    /// </summary>
    /// <!-- aidoc:v1 sig=934d0ad body=3bd6af8 -->
    [Test]
    public async Task DeleteOlderNumberAsync_DoesNothingWhenCountBelowLimit()
    {
        const string name = "LimitTestPump";

        var obs = new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            DeviceId = "Device-X",
            Name = name,
            Time = Now
        };

        await _repo.InsertAsync(obs);

        var deleted = await _repo.DeleteOlderNumberAsync(name, maxCount: 5);

        Assert.That(deleted, Is.Zero);
    }
    
    /// <summary>
    /// Verifies that the repository cleanup operation preserves recent observations by inserting a <see cref="PumpObservation"/> whose timestamp is one minute in the past and asserting that the deletion count returned by <c>DeleteOlderThanDaysAsync</c> is zero.
    /// </summary>
    /// <!-- aidoc:v1 sig=1c414ad body=819234d -->
    [Test]
    public async Task DeleteOlderThanDaysAsync_DoesNotDeleteRecentObservations()
    {
        var obs = new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            PatientId = PatientId,
            DeviceId = "Device-Recent",
            Name = "RecentPump",
            Time = Now.AddMinutes(-1) // reciente
        };

        await _repo.InsertAsync(obs);

        var deleted = await _repo.DeleteOlderThanDaysAsync(10);

        Assert.That(deleted, Is.Zero);
    }
    
    /// <summary>
    /// Verifies that <see cref="AggregatedPatientLastObservations"/> deduplicates <see cref="PumpObservation"/> entries that share the same <see cref="PumpObservation.Code"/> and <see cref="PumpObservation.Name"/> for a given patient, returning only a single entry per code/name combination.
    /// </summary>
    /// <!-- aidoc:v1 sig=646e129 body=eebdec4 -->
    [Test]
    public async Task AggregatedPatientLastObservations_RemovesDuplicatesByCodeAndName()
    {
        var patient = ObjectId.GenerateNewId();

        var p1 = new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            PatientId = patient,
            DeviceId = "DevA",
            Code = "C1",
            Name = "PumpA",
            Time = Now
        };

        var p2 = new PumpObservation
        {
            Id = ObjectId.GenerateNewId(),
            PatientId = patient,
            DeviceId = "DevA",
            Code = "C1",
            Name = "PumpA",
            Time = Now.AddSeconds(-1)
        };

        await _repo.InsertManyAsync([p1, p2]);

        var result = await _repo.AggregatedPatientLastObservations(patient);

        Assert.That(result, Has.Count.EqualTo(1));
    }
}