using System.Security.Claims;
using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
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 Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Moq;
using Options = Microsoft.Extensions.Options.Options;

namespace adas_core.Test.Services;

[TestFixture]
public class ConfigObservationServiceTest
{
    private ConfigObservationService _service = null!;
    private Mock<IConfigObservationRepository> _repo = null!;
    private Mock<IUnitService> _unitSvc = null!;
    private Mock<ILocalAuditService> _auditSvc = null!;
    private Mock<IHttpContextAccessor> _http = null!;
    private Mock<ICacheService> _cache = null!;
    private Mock<ILogger<ConfigObservationService>> _logger = null!;

    private IOptions<ApiSettings> _settings = null!;
    private IOptions<CacheSettings> _cacheSettings = null!;

    private static readonly ObjectId Id = ObjectId.GenerateNewId();

    private static readonly ConfigObservation ConfigList = new()
    {
        Id = Id,
        Name = "FC",
        Code = "147842",
        CodingSystem = "MDC",
        ParentCode = "69965",
        ParentName = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS",
        MinAlert = 60,
        MaxAlert = 100,
        ForceAlert = true
    };

    private readonly List<ConfigObservation> _allConfigList =
    [
        new() { Id = ObjectId.GenerateNewId(), Name = "Hemoglobina", Code = "12345", CodingSystem = "SNM" },
        new() { Id = ObjectId.GenerateNewId(), Name = "Glucemia", Code = "54321", CodingSystem = "SNM", OriginalName = "GLU" },
        new() { Id = ObjectId.GenerateNewId(), Name = "Sodio", OriginalName = "Sodio" },
        new()
        {
            Id = ObjectId.GenerateNewId(),
            Name = "ph",
            Code = "555",
            CodingSystem = "MG4",
            ParentCode = "3333",
            ParentCodingSystem = "SNM"
        },
        new()
        {
            Id = ObjectId.GenerateNewId(),
            Name = "SOFA",
            Code = "278061009",
            OriginalName = "SOFA",
            CodingSystem = "SNM",
            MinAlert = 10,
            MinWarn = 14
        },
        ConfigList,
        new() { Id = ObjectId.GenerateNewId(), Name = "Alarm_BlueCode", CodingSystem = "ADAS_EVENT" }
    ];

    /// <summary>
    /// Initializes the test fixture by creating mock instances of all dependencies required by <see cref="ConfigObservationService"/>,
    /// including the repository, unit service, audit service, HTTP context accessor, cache service, and logger.
    /// Configures a default authenticated <see cref="ClaimsPrincipal"/> in the HTTP context and sets the cache mocks to bypass caching by executing the factory function directly,
    /// ensuring repository calls are invoked during tests.
    /// </summary>
    [SetUp]
        public void Setup()
        {
            _repo = new Mock<IConfigObservationRepository>();
            _unitSvc = new Mock<IUnitService>();
            _auditSvc = new Mock<ILocalAuditService>();
            _http = new Mock<IHttpContextAccessor>();
            _cache = new Mock<ICacheService>();
            _logger = new Mock<ILogger<ConfigObservationService>>();
    
            var user = new ClaimsPrincipal(
                new ClaimsIdentity([new Claim(ClaimTypes.Name, "TestUser")], "mock"));
    
            _http.Setup(x => x.HttpContext).Returns(new DefaultHttpContext { User = user });
    
            _settings = Options.Create(new ApiSettings
            {
                ConfigObservation = new ConfigObservationSettings
                {
                    IgnoreUnknownObservation = false,
                    Refresh = null
                },
            });
    
            _cacheSettings = Options.Create(new CacheSettings());
    
            // KEY: mock cache to execute repository calls
            _cache.Setup(c => c.GetOrSetObjectAsync(
                    It.IsAny<string>(),
                    It.IsAny<Func<Task<ICollection<ConfigObservation>>>>(),
                    It.IsAny<TimeSpan?>(),
                    It.IsAny<CancellationToken>()))
                .Returns((string _, Func<Task<ICollection<ConfigObservation>>> f, TimeSpan? __, CancellationToken ___) => f());
    
            _cache.Setup(c => c.GetOrSetObjectAsync(
                    It.IsAny<string>(),
                    It.IsAny<Func<Task<ConfigObservation>>>(),
                    It.IsAny<TimeSpan?>(),
                    It.IsAny<CancellationToken>()))
                .Returns((string _, Func<Task<ConfigObservation>> f, TimeSpan? __, CancellationToken ___) => f());
    
            _service = new ConfigObservationService(
                _repo.Object,
                _settings,
                _cacheSettings,
                _logger.Object,
                _unitSvc.Object,
                _http.Object,
                _auditSvc.Object,
                _cache.Object);
    
            _repo.Setup(x => x.FindAll()).ReturnsAsync([ConfigList]);
        }

    // ---------------------------------------------------------
    // TESTS 
    // ---------------------------------------------------------

    /// <summary>
    /// Verifies that querying a patient observation by <c>CodingSystem</c> only, without specifying a code or name, returns a null result.
    /// </summary>
    [Test]
        public async Task Get_config_observation_item_by_codingSystem_only_return_null()
        {
            var obs = new PatientObservation
            {
                CodingSystem = "ADAS_EVENT",
                Code = "Pump_X",
                Name = "Alarm_Pump_X"
            };
    
            var result = await _service.Get(obs);
    
            Assert.That(result, Is.Null);
        }

    /// <summary>
    /// Verifies that <c>Get</c> returns the matching patient observation configuration for a given observation identified by its code and coding system.
    /// Asserts that the resolved item is not null and that the displayed name is mapped from the configured lookup to the expected localized value.
    /// </summary>
    [Test]
        public async Task Get_config_observation_item_by_code_and_codingSystem()
        {
            _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
    
            var obs = new BasePatientObservation
            {
                Name = "Hemo^GBr",
                Code = "12345",
                CodingSystem = "SNM"
            };
    
            var result = await _service.Get(obs);
    
            Assert.That(result, Is.Not.Null);
            Assert.That(result!.Name, Is.EqualTo("Hemoglobina"));
        }

    /// <summary>
    /// Verifies that the service retrieves the correct configuration observation item by its name,
    /// matching the requested observation against the list returned by the repository and returning
    /// the item with the expected name.
    /// </summary>
    [Test]
        public async Task Get_config_observation_item_by_name()
        {
            _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
    
            var obs = new BasePatientObservation { Name = "Hemoglobina" };
    
            var result = await _service.Get(obs);
    
            Assert.That(result!.Name, Is.EqualTo("Hemoglobina"));
        }

    /// <summary>
    /// Verifies that the service returns null when a patient observation item is requested by a name that does not exist in the configuration list.
    /// </summary>
    [Test]
        public async Task Get_config_observation_item_by_name_not_exist_returns_null()
        {
            _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
    
            var obs = new BasePatientObservation { Name = "XXX" };
    
            var result = await _service.Get(obs);
    
            Assert.That(result, Is.Null);
        }

    /// <summary>
    /// Verifies that the service retrieves the correct configuration by matching both the observation code with its coding system and the parent data code with its coding system, returning the configuration named "ph".
    /// </summary>
    [Test]
        public async Task Get_config_by_code_and_parent()
        {
            _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
    
            var obs = new BasePatientObservation
            {
                Code = "555",
                CodingSystem = "MG4",
                ParentData = new ParentDataClass
                {
                    Code = "3333",
                    CodingSystem = "SNM"
                }
            };
    
            var result = await _service.Get(obs);
    
            Assert.That(result!.Name, Is.EqualTo("ph"));
        }

    /// <summary>
    /// Verifies that the mapping service returns <c>null</c> when the observation name is unknown
    /// and the configuration is set to ignore unknown observations.
    /// </summary>
    [Test]
        public async Task Map_unknown_ignore_true_returns_null()
        {
            _settings.Value.ConfigObservation!.IgnoreUnknownObservation = true;
            _repo.Setup(x => x.FindAll()).ReturnsAsync([]);
    
            var result = await _service.Map(new BasePatientObservation { Name = "XX" });
    
            Assert.That(result, Is.Null);
        }

    /// <summary>
    /// Verifies that when ConfigObservation.IgnoreUnknownObservation is set to <c>false</c>,
    /// the <c>Map</c> method returns the original observation as-is when no matching record is found
    /// in the repository, rather than discarding it as an unknown observation.
    /// </summary>
    [Test]
        public async Task Map_unknown_ignore_false_returns_obs()
        {
            _settings.Value.ConfigObservation!.IgnoreUnknownObservation = false;
            _repo.Setup(x => x.FindAll()).ReturnsAsync([]);
    
            var obs = new BasePatientObservation { Name = "XX" };
    
            var result = await _service.Map(obs);
    
            Assert.That(result, Is.EqualTo(obs));
        }

    /// <summary>
    /// Verifies that mapping a patient observation with a value of 14 results in an "Ok" status, confirming the threshold mapping logic returns the expected outcome.
    /// </summary>
    [Test]
        public async Task Map_threshold_Ok()
        {
            _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
    
            var obs = new PatientObservation
            {
                Name = "SOFA",
                CodingSystem = "SNM",
                Value = 14
            };
    
            var result = await _service.Map(obs);
    
            Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Ok));
        }

    /// <summary>
    /// Tests that the Map method returns a Warning status when a patient observation value exceeds the configured threshold.
    /// </summary>
    [Test]
        public async Task Map_threshold_Warning()
        {
            _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
    
            var obs = new PatientObservation
            {
                Name = "SOFA",
                CodingSystem = "SNM",
                Value = 13
            };
    
            var result = await _service.Map(obs);
    
            Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Warning));
        }

    /// <summary>
    /// Verifies that mapping a <see cref="PatientObservation"/> with a SOFA score of 9 through the configured service results in a result with an <see cref="StatusEnum.Type.Alert"/> status.
    /// </summary>
    [Test]
        public async Task Map_threshold_Alert()
        {
            _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
    
            var obs = new PatientObservation
            {
                Name = "SOFA",
                CodingSystem = "SNM",
                Value = 9
            };
    
            var result = await _service.Map(obs);
    
            Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Alert));
        }

    /// <summary>
    /// Verifies that mapping a parent <see cref="PatientObservation"/> returns an <see cref="StatusEnum.Type.Ok"/> status
    /// when the repository successfully locates the configuration by identifier.
    /// </summary>
    [Test]
        public async Task Map_parent_ok()
        {
            _repo.Setup(x => x.FindById(Id)).ReturnsAsync(ConfigList);
    
            var fc = new PatientObservation
            {
                Id = Id,
                Name = "FC",
                Value = 70
            };
    
            var result = await _service.Map(fc);
    
            Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Ok));
        }

    /// <summary>
    /// Verifies that <c>UpdateConfig</c> successfully updates an existing configuration and returns the updated <see cref="ConfigObservation"/> with the new name.
    /// </summary>
    [Test]
        public async Task UpdateConfig_ok()
        {
            var newCfg = new ConfigObservation
            {
                Id = Id,
                Name = "New",
                Code = "X",
                CodingSystem = "S"
            };
    
            _repo.Setup(r => r.FindById(Id)).ReturnsAsync(ConfigList);
            _repo.Setup(r => r.Update(It.IsAny<ConfigObservation>())).ReturnsAsync(newCfg);
    
            var result = await _service.UpdateConfig(newCfg);
    
            Assert.That(result!.Name, Is.EqualTo("New"));
        }

    /// <summary>
    /// Verifies that <c>UpdateConfig</c> throws a <see cref="NotFoundException"/> when the target <c>ConfigObservation</c> cannot be found by its identifier.
    /// </summary>
    [Test]
        public async Task UpdateConfig_notfound()
        {
            _repo
                .Setup(r => r.FindById(Id))
                .ReturnsAsync((ConfigObservation?)null);
    
            Func<Task> act = () => _service.UpdateConfig(new ConfigObservation { Id = Id });
    
            Assert.ThrowsAsync<NotFoundException>(act);
        }


    /// <summary>
    /// Verifies that the <c>CreateConfig</c> service method successfully inserts the provided configuration and returns the expected result.
    /// </summary>
    [Test]
        public async Task CreateConfig_ok()
        {
            _repo.Setup(r => r.InsertOneAsyncAndReturn(ConfigList)).Returns(Task.FromResult(ConfigList));
    
            var result = await _service.CreateConfig(ConfigList);
    
            Assert.That(result, Is.EqualTo(ConfigList));
        }

    /// <summary>
    /// Verifies that <see cref="BadRequestException"/> is thrown when attempting to create a configuration that already exists.
    /// </summary>
    [Test]
        public void CreateConfig_duplicate_throws()
        {
            _repo.Setup(r => r.FindById(It.IsAny<ObjectId>())).ReturnsAsync(ConfigList);
    
    
            Func<Task> act = () => _service.CreateConfig(ConfigList);
    
            Assert.ThrowsAsync<BadRequestException>(act);
    
        }

    /// <summary>
    /// Verifies that RemoveConfigItem successfully removes a configuration item and returns the deleted entity when the repository finds and deletes it.
    /// </summary>
    [Test]
        public async Task RemoveConfigItem_ok()
        {
            var deleted = new ConfigObservation { Id = ObjectId.GenerateNewId(), Name = "X" };
    
            _repo.Setup(r => r.FindById(It.IsAny<ObjectId>())).ReturnsAsync(ConfigList);
            _repo.Setup(r => r.Delete(It.IsAny<ObjectId>())).ReturnsAsync(deleted);
    
            var result = await _service.RemoveConfigItem(ObjectId.GenerateNewId());
    
            Assert.That(result, Is.EqualTo(deleted));
        }

    /// <summary>
    /// Verifies that <c>RemoveConfigItem</c> returns <c>null</c> when the underlying delete operation on the repository yields no result, simulating the case where the configuration item does not exist.
    /// </summary>
    [Test]
        public async Task RemoveConfigItem_null()
        {
            _repo.Setup(r => r.FindById(Id)).ReturnsAsync(ConfigList);
            _repo.Setup(r => r.Delete(Id)).ReturnsAsync((ConfigObservation?)null);
    
            var result = await _service.RemoveConfigItem(Id);
    
            Assert.That(result, Is.Null);
        }
}