using Microsoft.Extensions.Logging;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;

/// <summary>
///     Regression coverage for Driver.FOCAS-007 — the driver previously swallowed every
///     exception in its poll / probe / recycle / fixed-tree loops with no logging at all,
///     leaving operators blind when a CNC was silently failing every tick.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasLoggingTests
{
    /// <summary>Verifies that the constructor accepts an ILogger.</summary>
    [Fact]
    public void Constructor_accepts_an_ILogger()
    {
        // Constructor signature must allow an ILogger<FocasDriver> so the host can wire one
        // through Microsoft.Extensions.DependencyInjection. The driver code project already
        // references Microsoft.Extensions.Logging.Abstractions.
        var logger = new CapturingLogger<FocasDriver>();
        var drv = new FocasDriver(
            new FocasDriverOptions { Probe = new FocasProbeOptions { Enabled = false } },
            "drv-1",
            new FakeFocasClientFactory(),
            logger);

        drv.ShouldNotBeNull();
    }

    /// <summary>Verifies that probe loop logs when an exception is swallowed.</summary>
    [Fact]
    public async Task ProbeLoop_logs_when_an_exception_is_swallowed()
    {
        var logger = new CapturingLogger<FocasDriver>();
        var factory = new FakeFocasClientFactory
        {
            Customise = () => new FakeFocasClient
            {
                // Make ProbeAsync throw — the probe loop swallows it but must log.
                ThrowOnConnect = false,
                ProbeResult = true, // not used because the underlying probe path throws
            },
        };
        // Force the probe to throw by making the client throw on connect.
        factory.Customise = () => new FakeFocasClient
        {
            ThrowOnConnect = true,
            Exception = new InvalidOperationException("simulated probe failure"),
        };

        var drv = new FocasDriver(
            new FocasDriverOptions
            {
                Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
                Probe = new FocasProbeOptions
                {
                    Enabled = true,
                    Interval = TimeSpan.FromMilliseconds(50),
                    Timeout = TimeSpan.FromMilliseconds(100),
                },
            },
            "drv-log",
            factory,
            logger);

        await drv.InitializeAsync("{}", CancellationToken.None);

        // Give the probe loop one tick or two to log.
        await Task.Delay(250);

        await drv.ShutdownAsync(CancellationToken.None);

        // We expect at least one log entry at Debug / Warning that mentions the simulated
        // failure or the probe loop. Without logging there's literally no record on a wedged
        // CNC — exactly the gap the finding called out.
        logger.Entries.ShouldNotBeEmpty();
        logger.Entries.Any(e => e.Message.Contains("probe", StringComparison.OrdinalIgnoreCase)
                                 || (e.Exception?.Message.Contains("simulated probe failure") ?? false))
            .ShouldBeTrue("at least one log entry should reference the probe loop or surface the swallowed exception");
    }

    private sealed class CapturingLogger<T> : ILogger<T>
    {
        /// <summary>Gets the captured log entries.</summary>
        public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();

        /// <summary>Begins a logging scope.</summary>
        /// <param name="state">The scope state.</param>
        /// <typeparam name="TState">The type of the state.</typeparam>
        public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
        /// <summary>Checks if logging is enabled for the specified level.</summary>
        /// <param name="logLevel">The log level.</param>
        public bool IsEnabled(LogLevel logLevel) => true;
        /// <summary>Logs a message.</summary>
        /// <param name="logLevel">The log level.</param>
        /// <param name="eventId">The event ID.</param>
        /// <param name="state">The state object.</param>
        /// <param name="exception">The exception, if any.</param>
        /// <param name="formatter">The formatter function.</param>
        /// <typeparam name="TState">The type of the state.</typeparam>
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
            Func<TState, Exception?, string> formatter)
        {
            Entries.Add((logLevel, formatter(state, exception), exception));
        }

        private sealed class NullScope : IDisposable
        {
            /// <summary>Gets the singleton instance.</summary>
            public static NullScope Instance { get; } = new();
            /// <summary>Disposes the scope.</summary>
            public void Dispose() { }
        }
    }
}
