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.AppSettings;
using adas_core.Domain.Models.MongoModels;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using Moq;
using static NUnit.Framework.Assert;
using Options = Microsoft.Extensions.Options.Options;

namespace adas_core.Test.Services;

[TestFixture]
public class DisplayServiceTest
{
    private Mock<ICacheService> _mockCacheService = null!;
    private Mock<IDisplayRepository> _mockDisplayRepository = null!;
    private Mock<IPointOfCareService> _mockPointOfCareService = null!;
    private Mock<IDisplayConfigService> _mockDisplayConfigService = null!;
    private Mock<IUserRepository> _mockUserRepository = null!;
    private Mock<IAuthService> _authServiceMock = null!;
    private Mock<ISubscribersService> _mockSubscribersService = null!;
    private Mock<IClientMessageService> _mockClientMessageService = null!;
    private Mock<Lazy<IUnitService>> _mockUnitService = null!;
    private Mock<IPermissionService> _mockPermissionService = null!;
    private Lazy<IPermissionService> _lazyMockPermission = null!;
    private Mock<ILogger<DisplayService>> _mockLogger = null!;
    private readonly Mock<IHttpContextAccessor> _httpContextAccessorMock = new();
    private readonly Mock<ILocalAuditService> _auditServiceMock = new();

    private DisplayService _displayService = null!;

    /// <summary>
    /// Initializes mocked dependencies and configuration required to construct a <see cref="DisplayService"/> instance for unit tests.
    /// Sets up the HTTP context with a test user claim, creates default cache settings, and wires all collaborators (repositories, services, logger, permissions) into the service under test.
    /// </summary>
    [SetUp]
    public void SetUp()
    {
        _mockCacheService = new Mock<ICacheService>();
        _mockDisplayRepository = new Mock<IDisplayRepository>();
        _mockPointOfCareService = new Mock<IPointOfCareService>();
        _mockDisplayConfigService = new Mock<IDisplayConfigService>();
        _mockUserRepository = new Mock<IUserRepository>();
        _authServiceMock = new Mock<IAuthService>();
        _mockSubscribersService = new Mock<ISubscribersService>();
        _mockClientMessageService = new Mock<IClientMessageService>();
        _mockUnitService = new Mock<Lazy<IUnitService>>();
        _mockPermissionService = new Mock<IPermissionService>();
        _lazyMockPermission = new Lazy<IPermissionService>(() => _mockPermissionService.Object);
        _mockLogger = new Mock<ILogger<DisplayService>>();

        var claims = new ClaimsPrincipal(
            new ClaimsIdentity([new Claim(ClaimTypes.Name, "TestUser")], "mock"));
        _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(new DefaultHttpContext { User = claims });

        var cacheSettings = Options.Create(new CacheSettings());

        _displayService = new DisplayService(
            _mockDisplayRepository.Object,
            _mockPointOfCareService.Object,
            _mockUnitService.Object,
            _mockDisplayConfigService.Object,
            _mockSubscribersService.Object,
            _mockClientMessageService.Object,
            _mockUserRepository.Object,
            _authServiceMock.Object,
            _mockLogger.Object,
            _httpContextAccessorMock.Object,
            _auditServiceMock.Object,
            _lazyMockPermission,
            _mockCacheService.Object,
            cacheSettings
        );

    }

    // -----------------------------------------------------------
    // InsertOne
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="DisplayService.InsertOne"/> inserts a display by resolving the default configuration for its type and persisting it through the repository's insert method.
    /// </summary>
    [Test]
    public async Task InsertOne_ShouldInsertDisplay()
    {
        var display = new Display
        {
            Name = "Test Display",
            Type = DisplayConfigEnums.DisplayType.DisplayNurse
        };

        var defaultCfg = new DisplayConfig { Id = ObjectId.GenerateNewId(), Type = display.Type };

        _mockDisplayConfigService.Setup(s => s.GetDefaultConfig(display.Type)).ReturnsAsync(defaultCfg);

        _mockDisplayRepository.Setup(r => r.InsertOneAsync(display))
            .Returns(Task.CompletedTask);

        var result = await _displayService.InsertOne(display);

        That(result, Is.EqualTo(display));
        _mockDisplayRepository.Verify(r => r.InsertOneAsync(display), Times.Once);
    }

    // -----------------------------------------------------------
    // GetById
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that the <see cref="DisplayService"/> returns the expected <see cref="Display"/> instance when a matching id is provided through the repository.
    /// </summary>
    [Test]
    public async Task GetById_ShouldReturnDisplay()
    {
        var id = ObjectId.GenerateNewId();
        var d = new Display { Id = id, Name = "D1" };

        _mockDisplayRepository.Setup(r => r.GetById(id)).ReturnsAsync(d);

        var result = await _displayService.GetById(id);

        That(result, Is.EqualTo(d));
    }

    // -----------------------------------------------------------
    // GetAllByUser - userName null
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that _displayService.GetAllByUser returns an empty collection when the user name is null.
    /// </summary>
    [Test]
    public async Task GetAllByUser_ShouldReturnEmpty_WhenUserNameNull()
    {
        var result = await _displayService.GetAllByUser(null);
        That(result, Is.Empty);
    }

    // -----------------------------------------------------------
    // GetAllByUser - Admin case
    // -----------------------------------------------------------

    [Test]
    public async Task GetAllByUser_ShouldReturnDisplays_WhenAdmin()
    {
        // Arrange
        const string userName = "admin";

        var adminId = ObjectId.GenerateNewId();
        var unitId = ObjectId.GenerateNewId();

        var user = new User
        {
            Id = adminId,
            UserName = userName,
            Authorization =
            [
                new Authorization { UnitId = unitId.ToString(), Rol = nameof(PermissionEnum.RolesType.AuthAdmin) }
            ]
        };

        // 1) Usuario + authorities
        _mockUserRepository.Setup(r => r.GetByUserAndAuthoritesName(userName))
            .ReturnsAsync(user);

        _authServiceMock.Setup(a => a.GetUserAuthorities(adminId))
            .ReturnsAsync(user.Authorization);

        // 2) Displays por UnitId
        var d1 = new Display { Id = ObjectId.GenerateNewId(), Name = "D1", UnitId = unitId };
        var d2 = new Display { Id = ObjectId.GenerateNewId(), Name = "D2", UnitId = unitId };
        var displays = new List<Display> { d1, d2 };

        _mockDisplayRepository.Setup(r => r.GetByUnitId(unitId))
            .ReturnsAsync(displays);

        // 3) GetInfo (rama base) → caché debe devolver el display haciendo el factory
        _mockDisplayRepository.Setup(r => r.GetById(d1.Id)).ReturnsAsync(d1);
        _mockDisplayRepository.Setup(r => r.GetById(d2.Id)).ReturnsAsync(d2);

        _mockCacheService
            .Setup(c => c.GetOrSetObjectAsync(
                It.IsAny<string>(),
                It.IsAny<Func<Task<Display?>>>(),
                It.IsAny<TimeSpan?>(),
                It.IsAny<CancellationToken>()))
            .Returns((string _, Func<Task<Display?>> factory, TimeSpan? _, CancellationToken _) => factory());


        // 4) Permisos (si es null, el servicio NO añade el display)
        _mockPermissionService
            .Setup(p => p.GetPermissionsForUnit(unitId.ToString(), user))
            .ReturnsAsync(new DisplayPermissionTypes(
                new UserActions(true, true, true, true, true),
                new UserActions(true, true, true, true, true),
                new UserActions(true, true, true, true, true),
                new UserActions(true, true, true, true, true),
                new UserActions(true, true, true, true, true),
                new UserActions(true, true, true, true, true),
                new UserActions(true, true, true, true, true),
                true
            ));

        // Act
        var result = await _displayService.GetAllByUser(userName);

        // Assert
        That(result, Has.Count.EqualTo(2));
        using (Assert.EnterMultipleScope())
        {
            That(result.Any(r => r.Display != null && r.Display.Id == d1.Id), Is.True);
            That(result.Any(r => r.Display != null && r.Display.Id == d2.Id), Is.True);
        }
    }

    // -----------------------------------------------------------
    // GetByType
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that retrieving display configurations by type returns the associated displays
    /// from the display repository, ensuring the service correctly resolves configurations and
    /// their linked displays for the given display type.
    /// </summary>
    [Test]
    public async Task GetByType_ShouldReturnDisplays()
    {
        var cfgId = ObjectId.GenerateNewId();
        var cfg = new DisplayConfig { Id = cfgId, Type = DisplayConfigEnums.DisplayType.DisplayNurse };
        var display = new Display { Id = ObjectId.GenerateNewId(), DisplayConfigId = cfgId, Name = "D" };

        _mockDisplayConfigService.Setup(s => s.GetByType(cfg.Type))
            .ReturnsAsync([cfg]);

        _mockDisplayRepository.Setup(r => r.GetByConfigId(cfgId))
            .ReturnsAsync([display]);

        var result = await _displayService.GetByType(cfg.Type);

        That(result.Count, Is.EqualTo(1));
        That(result[0], Is.EqualTo(display));
    }

    /// <summary>
    /// Verifies that <see cref="DisplayService.GetByType"/> returns an empty collection when no display configurations are found for the specified display type.
    /// </summary>
    /// <returns>A task that completes when the assertion confirming the empty result has been executed.</returns>
    [Test]
    public async Task GetByType_ShouldReturnEmpty_WhenNoConfigsFound()
    {
        _mockDisplayConfigService.Setup(s => s.GetByType(It.IsAny<DisplayConfigEnums.DisplayType>()))
            .ReturnsAsync([]);

        var result = await _displayService.GetByType(DisplayConfigEnums.DisplayType.DisplayNurse);

        That(result, Is.Empty);
    }

    // -----------------------------------------------------------
    // GetByPointOfCare
    // -----------------------------------------------------------
    /// <summary>
    /// Tests that GetByPointOfCare returns the displays associated with the specified point of care.
    /// </summary>
    /// <param name="poc">The point of care used to look up associated displays.</param>
    /// <returns>A task representing the asynchronous test execution.</returns>
    [Test]
    public async Task GetByPointOfCare_ShouldReturnDisplays()
    {
        var poc = new PointOfCare { Id = ObjectId.GenerateNewId() };
        var d = new Display { Id = ObjectId.GenerateNewId(), Name = "Display" };

        _mockDisplayRepository.Setup(r => r.GetByPointOfCare(poc))
            .ReturnsAsync([d]);

        var result = await _displayService.GetByPointOfCare(poc);

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

    // -----------------------------------------------------------
    // GetByConfigId
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="DisplayService.GetByConfigId"/> returns the displays associated with the specified configuration ID retrieved from the repository.
    /// </summary>
    /// <returns>A task that completes when the assertion confirms the returned collection contains the expected number of displays.</returns>
    [Test]
    public async Task GetByConfigId_ShouldReturnDisplays()
    {
        var cfgId = ObjectId.GenerateNewId();
        var d = new Display { Id = ObjectId.GenerateNewId(), DisplayConfigId = cfgId, Name = "display1" };

        _mockDisplayRepository.Setup(r => r.GetByConfigId(cfgId))
            .ReturnsAsync([d]);

        var result = await _displayService.GetByConfigId(cfgId);

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

    // -----------------------------------------------------------
    // GetByName
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that the <see cref="DisplayService.GetByName"/> method throws a <see cref="NotFoundException"/> when no display matching the specified name is found in the repository.
    /// </summary>
    [Test]
    public void GetByName_ShouldThrow_WhenNotFound()
    {
        _mockDisplayRepository.Setup(r => r.GetByName("X"))
            .ReturnsAsync((Display?)null);

        Func<Task> act = () => _displayService.GetByName("X");
        Assert.ThrowsAsync<NotFoundException>(act);
    }

    // -----------------------------------------------------------
    // GetInfo
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that <c>GetInfo</c> returns a <see cref="Display"/> with its associated <c>PointOfCare</c> entries populated when invoked with the "with POC" flag enabled and a valid display identifier.
    /// </summary>
    [Test]
    public async Task GetInfo_ShouldReturnDisplayWithPoc()
    {
        var id = ObjectId.GenerateNewId();
        var cfgId = ObjectId.GenerateNewId();
        var pocId = ObjectId.GenerateNewId();

        var display = new Display
        {
            Id = id,
            Name = "Display",
            DisplayConfigId = cfgId,
            PointOfCareIdList = [pocId]
        };

        _mockCacheService
            .Setup(c => c.GetOrSetObjectAsync(
                It.IsAny<string>(),
                It.IsAny<Func<Task<Display?>>>(),
                It.IsAny<TimeSpan?>(),
                It.IsAny<CancellationToken>()))
            .Returns((string _, Func<Task<Display?>> factory, TimeSpan? _, CancellationToken _) => factory());

        _mockDisplayRepository.Setup(r => r.GetById(id)).ReturnsAsync(display);
        _mockDisplayConfigService.Setup(s => s.GetById(cfgId))
            .ReturnsAsync(new DisplayConfig { Id = cfgId });

        var poc = new PointOfCare { Id = pocId };

        _mockPointOfCareService.Setup(s => s.GetInfo(pocId, null, true, It.IsAny<CancellationToken>()))
            .ReturnsAsync(poc);

        var result = await _displayService.GetInfo(id, "user", [], null, true, true, false);

        That(result, Is.Not.Null);
        That(result!.PointOfCares.First(), Is.EqualTo(poc));
    }

    // -----------------------------------------------------------
    // GetByUnitId
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="IDisplayService.GetByUnitId"/> returns the displays associated with the specified unit identifier when the repository contains matching records.
    /// </summary>
    [Test]
    public async Task GetByUnitId_ShouldReturnDisplays()
    {
        var id = ObjectId.GenerateNewId();
        var d = new Display { Id = ObjectId.GenerateNewId(), UnitId = id, Name = "display1" };

        _mockDisplayRepository.Setup(r => r.GetByUnitId(id))
            .ReturnsAsync([d]);

        var result = await _displayService.GetByUnitId(id);

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

    // -----------------------------------------------------------
    // UpdatePointOfCareList
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that UpdatePointOfCareList throws a <see cref="NotFoundException"/> when the display with the specified identifier does not exist.
    /// </summary>
    /// <exception cref="NotFoundException">Thrown when no display is found for the given id.</exception>
    [Test]
    public void UpdatePointOfCareList_ShouldThrow_WhenDisplayNotFound()
    {
        var id = ObjectId.GenerateNewId();

        _mockDisplayRepository.Setup(r => r.GetById(id))
            .ReturnsAsync((Display?)null);

        Func<Task> act = () => _displayService.UpdatePointOfCareList(id, []);
        Assert.ThrowsAsync<NotFoundException>(act);
    }

    // -----------------------------------------------------------
    // UpdateConfigPreset
    // -----------------------------------------------------------
    /// <summary>
    /// Verifies that <see cref="DisplayService.UpdateConfigPreset"/> throws a <see cref="NotFoundException"/> when the repository update operation returns a null result, indicating the display or configuration preset could not be found.
    /// </summary>
    /// <param name="id">The unique identifier of the display whose configuration preset is being updated.</param>
    /// <param name="cfgId">The unique identifier of the configuration preset to associate with the display.</param>
    [Test]
    public void UpdateConfigPreset_ShouldThrow_WhenUpdateFails()
    {
        var id = ObjectId.GenerateNewId();
        var cfgId = ObjectId.GenerateNewId();

        _mockDisplayRepository.Setup(r => r.UpdateConfigPreset(id, cfgId))
            .ReturnsAsync((Display?)null);

        Func<Task> act = () => _displayService.UpdateConfigPreset(id, cfgId);
        Assert.ThrowsAsync<NotFoundException>(act);
    }
}