using System.Reflection;
using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Communication;
using ParkedMessagesPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.ParkedMessages;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;

/// <summary>
/// Regression tests for CentralUI-023. <c>DiffDialog.TryLockBodyAsync</c> /
/// <c>TryUnlockBodyAsync</c> and <c>ParkedMessages.CopyAsync</c> wrapped JS
/// interop in bare <c>catch { }</c> blocks: a genuine <see cref="JSException"/>
/// was indistinguishable from an expected <see cref="JSDisconnectedException"/>
/// and neither was logged. The fix narrows the catch and logs real interop
/// failures via <c>ILogger</c>, consistent with the CentralUI-018 fixes.
/// </summary>
public class JsInteropLoggingTests : BunitContext
{
    /// <summary>Captures log entries so the test can assert on them.</summary>
    private sealed class CapturingLoggerProvider : ILoggerProvider
    {
        public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();

        public ILogger CreateLogger(string categoryName) => new CapturingLogger(Entries);
        public void Dispose() { }

        private sealed class CapturingLogger : ILogger
        {
            private readonly List<(LogLevel, string, Exception?)> _entries;
            public CapturingLogger(List<(LogLevel, string, Exception?)> entries) => _entries = entries;

            public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
            public bool IsEnabled(LogLevel logLevel) => true;

            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
                Exception? exception, Func<TState, Exception?, string> formatter)
                => _entries.Add((logLevel, formatter(state, exception), exception));
        }
    }

    [Fact]
    public void DiffDialog_BodyLock_GenuineJsException_IsLogged()
    {
        var provider = new CapturingLoggerProvider();
        Services.AddLogging(b => b.AddProvider(provider));

        // The body scroll-lock runs on OnAfterRender when the dialog is shown.
        // Configure that JS call to throw a genuine JSException.
        JSInterop.Mode = JSRuntimeMode.Strict;
        JSInterop.SetupVoid("document.body.classList.add", "modal-open")
            .SetException(new JSException("body lock failed"));
        // Focus and any other interop is harmless here — allow it loosely.
        JSInterop.SetupVoid("document.body.classList.remove", "modal-open");

        var cut = Render<DiffDialog>();
        cut.InvokeAsync(() => cut.Instance.ShowAsync("Compare", "a", "b"));
        cut.Render();

        cut.WaitForAssertion(() =>
        {
            var warnings = provider.Entries.Where(e => e.Level >= LogLevel.Warning).ToList();
            Assert.Contains(warnings, e => e.Exception is JSException);
        });
    }

    [Fact]
    public void DiffDialog_BodyLock_Disconnect_IsNotLogged()
    {
        var provider = new CapturingLoggerProvider();
        Services.AddLogging(b => b.AddProvider(provider));

        // A circuit disconnect during the lock is expected — it must NOT log.
        JSInterop.Mode = JSRuntimeMode.Strict;
        JSInterop.SetupVoid("document.body.classList.add", "modal-open")
            .SetException(new JSDisconnectedException("circuit gone"));
        JSInterop.SetupVoid("document.body.classList.remove", "modal-open");

        var cut = Render<DiffDialog>();
        cut.InvokeAsync(() => cut.Instance.ShowAsync("Compare", "a", "b"));
        cut.Render();

        Assert.DoesNotContain(provider.Entries, e => e.Level >= LogLevel.Warning);
    }

    [Fact]
    public async Task ParkedMessages_Copy_GenuineJsException_IsLogged()
    {
        var provider = new CapturingLoggerProvider();
        Services.AddLogging(b => b.AddProvider(provider));

        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetAllSitesAsync().Returns(new List<Site>());
        Services.AddSingleton(siteRepo);

        var comms = new CommunicationService(
            Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance);
        Services.AddSingleton(comms);

        var identity = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie");
        var stubAuth = new StubAuthStateProvider(
            new AuthenticationState(new ClaimsPrincipal(identity)));
        Services.AddSingleton<AuthenticationStateProvider>(stubAuth);
        Services.AddScoped(_ => new SiteScopeService(stubAuth));
        Services.AddScoped<IDialogService, DialogService>();

        JSInterop.Mode = JSRuntimeMode.Strict;
        JSInterop.SetupVoid("navigator.clipboard.writeText", _ => true)
            .SetException(new JSException("clipboard permission denied"));

        var cut = Render<ParkedMessagesPage>();

        // CopyAsync is a private handler; invoke it directly with a clipboard
        // call configured to fail. Pre-fix the bare catch swallowed it silently.
        var copy = typeof(ParkedMessagesPage).GetMethod(
            "CopyAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
        await cut.InvokeAsync(() => (Task)copy.Invoke(cut.Instance, new object[] { "some-id" })!);

        var warnings = provider.Entries.Where(e => e.Level >= LogLevel.Warning).ToList();
        Assert.Contains(warnings, e => e.Exception is JSException);
    }

    private sealed class StubAuthStateProvider : AuthenticationStateProvider
    {
        private readonly AuthenticationState _state;
        public StubAuthStateProvider(AuthenticationState state) => _state = state;
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
            => Task.FromResult(_state);
    }
}
