using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ArchestrA;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.Backend;

/// <summary>
///     Driver.Historian.Wonderware-012 coverage — pins <see cref="HistorianDataSource"/>'s
///     connect-failover / cooldown loop via a fake <see cref="IHistorianConnectionFactory"/>.
///     A live <see cref="HistorianAccess"/> is never instantiated; the fake throws on every
///     attempt so the read path surfaces the connect failure without touching the SDK.
/// </summary>
[Trait("Category", "Unit")]
public sealed class HistorianDataSourceConnectFailoverTests
{
    /// <summary>Verifies that ReadRaw throws when no nodes are healthy.</summary>
    [Fact]
    public async Task ReadRaw_when_no_nodes_are_healthy_throws_so_IPC_surfaces_Success_false()
    {
        var cfg = new HistorianConfiguration
        {
            Enabled = true,
            ServerNames = new List<string> { "node-a" },
            FailureCooldownSeconds = 60,
            // Disable the outer request timeout so the test doesn't race the connect failure
            // against the timeout (we want the connect failure path, not a TimeoutException).
            RequestTimeoutSeconds = 0,
        };
        var ds = new HistorianDataSource(cfg, new ThrowingConnectionFactory());

        // Read methods used to swallow the connect exception and return an empty list with
        // Success=true; the fix re-throws so the IPC layer surfaces Success=false. The
        // exception must therefore propagate.
        await Should.ThrowAsync<Exception>(() => ds.ReadRawAsync(
            "Tank.Level",
            new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc),
            new DateTime(2026, 5, 1, 0, 1, 0, DateTimeKind.Utc),
            maxValues: 100,
            CancellationToken.None));
    }

    /// <summary>Verifies that ReadRaw tries each cluster node in order.</summary>
    [Fact]
    public async Task ReadRaw_tries_each_cluster_node_in_order_until_one_succeeds_or_all_fail()
    {
        var cfg = new HistorianConfiguration
        {
            Enabled = true,
            ServerNames = new List<string> { "node-a", "node-b", "node-c" },
            FailureCooldownSeconds = 60,
            RequestTimeoutSeconds = 0,
        };
        var factory = new TrackingThrowingConnectionFactory();
        var ds = new HistorianDataSource(cfg, factory);

        await Should.ThrowAsync<Exception>(() => ds.ReadRawAsync(
            "Tank.Level",
            new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc),
            new DateTime(2026, 5, 1, 0, 1, 0, DateTimeKind.Utc),
            maxValues: 100,
            CancellationToken.None));

        // All three candidates must be attempted in the configured order before the
        // connect-loop gives up.
        factory.AttemptedNodes.ShouldBe(new[] { "node-a", "node-b", "node-c" });
    }

    /// <summary>Verifies that failed nodes are marked in cooldown and not retried immediately.</summary>
    [Fact]
    public async Task ReadRaw_marks_failed_nodes_in_cooldown_so_a_subsequent_call_sees_no_healthy_nodes()
    {
        var cfg = new HistorianConfiguration
        {
            Enabled = true,
            ServerNames = new List<string> { "node-a", "node-b" },
            FailureCooldownSeconds = 60,
            RequestTimeoutSeconds = 0,
        };
        var ds = new HistorianDataSource(cfg, new ThrowingConnectionFactory());

        await Should.ThrowAsync<Exception>(() => ds.ReadRawAsync(
            "Tank.Level",
            DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow,
            maxValues: 100, CancellationToken.None));

        var snap = ds.GetHealthSnapshot();
        snap.NodeCount.ShouldBe(2);
        snap.HealthyNodeCount.ShouldBe(0, "both nodes failed and entered cooldown after the connect attempts");
        snap.ProcessConnectionOpen.ShouldBeFalse();
        snap.ActiveProcessNode.ShouldBeNull();
    }

    /// <summary>Verifies that ReadEvents uses a separate event connection path.</summary>
    [Fact]
    public async Task ReadEvents_uses_a_separate_event_connection_path()
    {
        // ReadEventsAsync uses _eventConnection / EnsureEventConnected — a different
        // codepath than ReadRawAsync. Symmetric test to pin the dual-connection design.
        var cfg = new HistorianConfiguration
        {
            Enabled = true,
            ServerNames = new List<string> { "node-a" },
            FailureCooldownSeconds = 60,
            RequestTimeoutSeconds = 0,
        };
        var factory = new TrackingThrowingConnectionFactory();
        var ds = new HistorianDataSource(cfg, factory);

        await Should.ThrowAsync<Exception>(() => ds.ReadEventsAsync(
            sourceName: "Tank.HiHi",
            DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow,
            maxEvents: 100, CancellationToken.None));

        factory.AttemptedTypes.ShouldContain(HistorianConnectionType.Event,
            "event reads must open an Event-typed connection");
        factory.AttemptedNodes.ShouldBe(new[] { "node-a" });
    }

    // ── helpers ──────────────────────────────────────────────────────────

    private sealed class ThrowingConnectionFactory : IHistorianConnectionFactory
    {
        /// <summary>
        /// Simulates a connection failure by throwing an exception.
        /// </summary>
        /// <param name="config">The historian configuration.</param>
        /// <param name="type">The connection type.</param>
        /// <param name="readOnly">Whether to open a read-only connection.</param>
        public HistorianAccess CreateAndConnect(
            HistorianConfiguration config, HistorianConnectionType type, bool readOnly = true)
            => throw new InvalidOperationException($"simulated connect failure to {config.ServerName}");
    }

    private sealed class TrackingThrowingConnectionFactory : IHistorianConnectionFactory
    {
        /// <summary>Gets the list of node names that were attempted.</summary>
        public List<string> AttemptedNodes { get; } = new();
        /// <summary>Gets the list of connection types that were attempted.</summary>
        public List<HistorianConnectionType> AttemptedTypes { get; } = new();

        /// <summary>
        /// Tracks connection attempts and simulates a connection failure.
        /// </summary>
        /// <param name="config">The historian configuration.</param>
        /// <param name="type">The connection type.</param>
        /// <param name="readOnly">Whether to open a read-only connection.</param>
        public HistorianAccess CreateAndConnect(
            HistorianConfiguration config, HistorianConnectionType type, bool readOnly = true)
        {
            AttemptedNodes.Add(config.ServerName);
            AttemptedTypes.Add(type);
            throw new InvalidOperationException($"simulated connect failure to {config.ServerName}");
        }
    }
}
