using System;
using System.Reflection;
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-005 regression tests for <see cref="HistorianDataSource.GetHealthSnapshot"/>.
///     The active-node strings and the connection-open booleans were published under different
///     locks, so a snapshot could observe an internally inconsistent pairing (open with no node,
///     or closed with a non-null node). The fix derives the open booleans from the same field
///     that is published under the same lock so the snapshot is self-consistent by construction.
/// </summary>
[Trait("Category", "Unit")]
public sealed class HistorianDataSourceHealthSnapshotTests
{
    /// <summary>
    ///     Drives the "half-published" state directly via reflection: set <c>_connection</c>
    ///     to a non-null sentinel but leave <c>_activeProcessNode</c> null. The snapshot must
    ///     report <c>ProcessConnectionOpen = false</c> and <c>ActiveProcessNode = null</c>
    ///     consistently — never a mismatch.
    /// </summary>
    [Fact]
    public void Snapshot_with_connection_set_but_active_node_null_is_consistent()
    {
        var ds = new HistorianDataSource(
            new HistorianConfiguration { Enabled = true, ServerName = "h1" });

        SetField(ds, "_connection", new HistorianAccess());
        SetField(ds, "_activeProcessNode", (string?)null);

        var snap = ds.GetHealthSnapshot();
        (snap.ProcessConnectionOpen == (snap.ActiveProcessNode != null)).ShouldBeTrue(
            "snapshot must not advertise open with no node — picks one source of truth");
    }

    /// <summary>
    ///     Symmetric case for the event connection.
    /// </summary>
    [Fact]
    public void Snapshot_with_event_connection_set_but_active_node_null_is_consistent()
    {
        var ds = new HistorianDataSource(
            new HistorianConfiguration { Enabled = true, ServerName = "h1" });

        SetField(ds, "_eventConnection", new HistorianAccess());
        SetField(ds, "_activeEventNode", (string?)null);

        var snap = ds.GetHealthSnapshot();
        (snap.EventConnectionOpen == (snap.ActiveEventNode != null)).ShouldBeTrue(
            "snapshot must not advertise event open with no node");
    }

    /// <summary>
    ///     The other direction: connection cleared but node still populated (the failure path
    ///     between the two field clears). The snapshot must still pair them consistently.
    /// </summary>
    [Fact]
    public void Snapshot_with_connection_cleared_but_active_node_populated_is_consistent()
    {
        var ds = new HistorianDataSource(
            new HistorianConfiguration { Enabled = true, ServerName = "h1" });

        SetField(ds, "_connection", (HistorianAccess?)null);
        SetField(ds, "_activeProcessNode", "node-stale");

        var snap = ds.GetHealthSnapshot();
        (snap.ProcessConnectionOpen == (snap.ActiveProcessNode != null)).ShouldBeTrue(
            "snapshot must not advertise closed with a node still set");
    }

    /// <summary>
    ///     Steady-state happy path: both fields populated — snapshot reports both consistently.
    /// </summary>
    [Fact]
    public void Snapshot_with_both_fields_populated_reports_open_and_active_node()
    {
        var ds = new HistorianDataSource(
            new HistorianConfiguration { Enabled = true, ServerName = "h1" });

        SetField(ds, "_connection", new HistorianAccess());
        SetField(ds, "_activeProcessNode", "h1");

        var snap = ds.GetHealthSnapshot();
        snap.ProcessConnectionOpen.ShouldBeTrue();
        snap.ActiveProcessNode.ShouldBe("h1");
    }

    /// <summary>
    ///     Steady-state default (no connect attempted): both null.
    /// </summary>
    [Fact]
    public void Snapshot_with_default_fields_reports_closed_with_no_active_node()
    {
        var ds = new HistorianDataSource(
            new HistorianConfiguration { Enabled = true, ServerName = "h1" });

        var snap = ds.GetHealthSnapshot();
        snap.ProcessConnectionOpen.ShouldBeFalse();
        snap.ActiveProcessNode.ShouldBeNull();
        snap.EventConnectionOpen.ShouldBeFalse();
        snap.ActiveEventNode.ShouldBeNull();
    }

    private static void SetField(object target, string name, object? value)
    {
        var f = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
        f.ShouldNotBeNull($"private field '{name}' must exist on {target.GetType().Name}");
        f!.SetValue(target, value);
    }
}
