using Microsoft.Extensions.Logging;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

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

/// <summary>
///     Regression coverage for Driver.AbCip-007 — the driver previously swallowed every
///     exception in its read / write / probe / template-read / alarm-poll paths with no
///     logging at all, leaving operators blind when a PLC was silently failing every tick.
///     Driver.AbCip-011 — when the probe is Enabled but no ProbeTagPath is configured, the
///     driver used to silently leave every device's HostState=Unknown forever; the fix logs a
///     warning at init time so the operator notices.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipLoggingTests
{
    private const string Device = "ab://10.0.0.5/1,0";

    /// <summary>Verifies that constructor accepts an ILogger.</summary>
    [Fact]
    public void Constructor_accepts_an_ILogger()
    {
        // Constructor signature must allow an ILogger<AbCipDriver> so the host can wire one
        // through Microsoft.Extensions.DependencyInjection. The driver code project already
        // pulls in Microsoft.Extensions.Logging.Abstractions transitively via Core.
        var logger = new CapturingLogger<AbCipDriver>();
        var drv = new AbCipDriver(
            new AbCipDriverOptions { Probe = new AbCipProbeOptions { Enabled = false } },
            "drv-1",
            logger: logger);

        drv.ShouldNotBeNull();
    }

    /// <summary>Verifies that ProbeLoop logs when an exception is swallowed.</summary>
    [Fact]
    public async Task ProbeLoop_logs_when_an_exception_is_swallowed()
    {
        var logger = new CapturingLogger<AbCipDriver>();
        var factory = new FakeAbCipTagFactory
        {
            // Force the probe to throw on initialize so the swallow path runs every tick.
            Customise = p => new FakeAbCipTag(p)
            {
                ThrowOnInitialize = true,
                Exception = new InvalidOperationException("simulated probe init failure"),
            },
        };

        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(Device)],
            Probe = new AbCipProbeOptions
            {
                Enabled = true,
                ProbeTagPath = "ProbeTag",
                Interval = TimeSpan.FromMilliseconds(20),
                Timeout = TimeSpan.FromMilliseconds(50),
            },
        }, "drv-log", factory, logger: logger);

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

        // Give the probe loop a couple of ticks to log.
        await Task.Delay(200);

        await drv.ShutdownAsync(CancellationToken.None);

        // We expect at least one log entry that mentions the probe loop or carries the
        // simulated exception. Without it there is no record of a wedged probe — 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 init failure") ?? false))
            .ShouldBeTrue("at least one log entry should reference the probe loop or surface the swallowed exception");
    }

    /// <summary>Verifies that ReadFailure logs at warning level.</summary>
    [Fact]
    public async Task ReadFailure_logs_at_warning_level()
    {
        // A non-zero libplctag status used to be silently classified into BadCommunicationError
        // with no log. After the fix the driver logs a warning so operators can correlate the
        // status code with the affected tag.
        var logger = new CapturingLogger<AbCipDriver>();
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new FakeAbCipTag(p) { Status = (int)libplctag.Status.ErrorBadConnection },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(Device)],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1", factory, logger: logger);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.ReadAsync(["Speed"], CancellationToken.None);

        logger.Entries.Any(e => e.Level >= LogLevel.Warning
                                 && e.Message.Contains("Speed", StringComparison.OrdinalIgnoreCase))
            .ShouldBeTrue("read failure on tag 'Speed' should be logged at warning level or above");
    }

    /// <summary>Verifies that ReadException logs at warning level.</summary>
    [Fact]
    public async Task ReadException_logs_at_warning_level()
    {
        // A transport-level exception used to be silently mapped to BadCommunicationError with
        // no log. After the fix the driver logs a warning carrying the exception so operators
        // can see the root cause.
        var logger = new CapturingLogger<AbCipDriver>();
        var factory = new FakeAbCipTagFactory
        {
            Customise = p => new FakeAbCipTag(p)
            {
                ThrowOnRead = true,
                Exception = new InvalidOperationException("simulated wire failure"),
            },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(Device)],
            RawTags = AbCipRawTags.From(new AbCipTagDefinition("Speed", Device, "Speed", AbCipDataType.DInt)),
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1", factory, logger: logger);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.ReadAsync(["Speed"], CancellationToken.None);

        logger.Entries.Any(e => e.Level >= LogLevel.Warning
                                 && (e.Exception?.Message.Contains("simulated wire failure") ?? false))
            .ShouldBeTrue("read transport exception should be logged at warning level with the inner exception attached");
    }

    /// <summary>Verifies that InitializeAsync warns when probe is enabled but ProbeTagPath is blank.</summary>
    [Fact]
    public async Task InitializeAsync_warns_when_probe_is_enabled_but_ProbeTagPath_is_blank()
    {
        // Driver.AbCip-011: when Probe.Enabled is true but ProbeTagPath is null/blank, the
        // driver used to start no probe loop and leave the device's HostState=Unknown forever.
        // The fix logs a warning so the operator sees the misconfiguration instead of getting
        // a silently inert health surface.
        var logger = new CapturingLogger<AbCipDriver>();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(Device)],
            Probe = new AbCipProbeOptions
            {
                Enabled = true,
                ProbeTagPath = null, // explicitly inert
            },
        }, "drv-1", logger: logger);

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

        logger.Entries.Any(e => e.Level == LogLevel.Warning
                                 && (e.Message.Contains("probe", StringComparison.OrdinalIgnoreCase)
                                     && e.Message.Contains("ProbeTagPath", StringComparison.OrdinalIgnoreCase)))
            .ShouldBeTrue("probe-enabled-but-inert configuration should be logged at warning level");
    }

    /// <summary>Verifies that InitializeAsync does not warn when probe is disabled.</summary>
    [Fact]
    public async Task InitializeAsync_does_not_warn_when_probe_is_disabled()
    {
        // No warning when the operator explicitly opted out.
        var logger = new CapturingLogger<AbCipDriver>();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(Device)],
            Probe = new AbCipProbeOptions { Enabled = false },
        }, "drv-1", logger: logger);

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

        logger.Entries.Any(e => e.Level == LogLevel.Warning
                                 && e.Message.Contains("ProbeTagPath", StringComparison.OrdinalIgnoreCase))
            .ShouldBeFalse("no probe warning expected when Probe.Enabled is false");
    }

    /// <summary>Test logger that captures all log entries.</summary>
    internal 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 scope (stub implementation).</summary>
        /// <typeparam name="TState">The type of the scope state.</typeparam>
        /// <param name="state">The scope state.</param>
        public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
        /// <summary>Checks if logging is enabled (always true).</summary>
        /// <param name="logLevel">The log level to check.</param>
        public bool IsEnabled(LogLevel logLevel) => true;
        /// <summary>Logs an entry and captures it.</summary>
        /// <typeparam name="TState">The type of the log state.</typeparam>
        /// <param name="logLevel">The log level.</param>
        /// <param name="eventId">The event ID.</param>
        /// <param name="state">The log state.</param>
        /// <param name="exception">The exception, if any.</param>
        /// <param name="formatter">The log message formatter.</param>
        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 (stub implementation).</summary>
            public void Dispose() { }
        }
    }
}
