using System.Reflection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.Historian;

/// <summary>
///     Structural contract tests for the historian data-source surface added in PR 1.1.
///     Asserts the type shape — implementations are tested in their own projects.
/// </summary>
public sealed class IHistorianDataSourceContractTests
{
    /// <summary>Verifies that the interface lives in the root namespace.</summary>
    [Fact]
    public void Interface_LivesInRootNamespace()
    {
        typeof(IHistorianDataSource).Namespace
            .ShouldBe("ZB.MOM.WW.OtOpcUa.Core.Abstractions");
    }

    /// <summary>Verifies that the interface is public.</summary>
    [Fact]
    public void Interface_IsPublic()
    {
        typeof(IHistorianDataSource).IsPublic.ShouldBeTrue();
        typeof(IHistorianDataSource).IsInterface.ShouldBeTrue();
    }

    /// <summary>Verifies that the interface extends IDisposable.</summary>
    [Fact]
    public void Interface_ExtendsIDisposable()
    {
        typeof(IDisposable).IsAssignableFrom(typeof(IHistorianDataSource))
            .ShouldBeTrue("data sources own backend connections; the server disposes them on shutdown");
    }

    /// <summary>Verifies that read methods return the expected task shape.</summary>
    /// <param name="methodName">The name of the interface method to inspect.</param>
    /// <param name="expectedReturnType">The expected Task return type of the method.</param>
    [Theory]
    [InlineData("ReadRawAsync", typeof(Task<HistoryReadResult>))]
    [InlineData("ReadProcessedAsync", typeof(Task<HistoryReadResult>))]
    [InlineData("ReadAtTimeAsync", typeof(Task<HistoryReadResult>))]
    [InlineData("ReadEventsAsync", typeof(Task<HistoricalEventsResult>))]
    public void ReadMethods_ReturnExpectedTaskShape(string methodName, Type expectedReturnType)
    {
        var method = typeof(IHistorianDataSource).GetMethod(methodName);
        method.ShouldNotBeNull();
        method!.ReturnType.ShouldBe(expectedReturnType);
    }

    /// <summary>Verifies that GetHealthSnapshot is synchronous.</summary>
    [Fact]
    public void GetHealthSnapshot_IsSynchronous()
    {
        var method = typeof(IHistorianDataSource).GetMethod("GetHealthSnapshot");
        method.ShouldNotBeNull();
        method!.ReturnType.ShouldBe(typeof(HistorianHealthSnapshot));
    }

    /// <summary>Verifies that HealthSnapshot accepts an empty cluster node list.</summary>
    [Fact]
    public void HealthSnapshot_AcceptsEmptyClusterNodeList()
    {
        var snapshot = new HistorianHealthSnapshot(
            TotalQueries: 0,
            TotalSuccesses: 0,
            TotalFailures: 0,
            ConsecutiveFailures: 0,
            LastSuccessTime: null,
            LastFailureTime: null,
            LastError: null,
            ProcessConnectionOpen: false,
            EventConnectionOpen: false,
            ActiveProcessNode: null,
            ActiveEventNode: null,
            Nodes: Array.Empty<HistorianClusterNodeState>());

        snapshot.Nodes.ShouldBeEmpty();
    }

    /// <summary>Verifies that HealthSnapshot preserves cluster nodes.</summary>
    [Fact]
    public void HealthSnapshot_PreservesClusterNodes()
    {
        var node = new HistorianClusterNodeState(
            Name: "hist-01",
            IsHealthy: true,
            CooldownUntil: null,
            FailureCount: 0,
            LastError: null,
            LastFailureTime: null);

        var snapshot = new HistorianHealthSnapshot(
            TotalQueries: 5,
            TotalSuccesses: 5,
            TotalFailures: 0,
            ConsecutiveFailures: 0,
            LastSuccessTime: new DateTime(2026, 4, 29, 12, 0, 0, DateTimeKind.Utc),
            LastFailureTime: null,
            LastError: null,
            ProcessConnectionOpen: true,
            EventConnectionOpen: true,
            ActiveProcessNode: "hist-01",
            ActiveEventNode: "hist-01",
            Nodes: new[] { node });

        snapshot.Nodes.Count.ShouldBe(1);
        snapshot.Nodes[0].ShouldBe(node);
    }

    /// <summary>Verifies that ClusterNodeState records are equal by value.</summary>
    [Fact]
    public void ClusterNodeState_RecordEqualityByValue()
    {
        var a = new HistorianClusterNodeState("hist-01", true, null, 0, null, null);
        var b = new HistorianClusterNodeState("hist-01", true, null, 0, null, null);

        a.ShouldBe(b);
    }

    /// <summary>Verifies that ClusterNodeState instances are distinct by any field.</summary>
    [Fact]
    public void ClusterNodeState_DistinctByAnyField()
    {
        var healthy = new HistorianClusterNodeState("hist-01", true, null, 0, null, null);
        var unhealthy = new HistorianClusterNodeState("hist-01", false, null, 1, "boom", null);

        healthy.ShouldNotBe(unhealthy);
    }

    /// <summary>
    ///     Core.Abstractions-006: the <c>maxValuesPerNode</c> (raw / processed) and <c>maxEvents</c>
    ///     parameter types are intentionally asymmetric — raw/processed reads use <c>uint</c>
    ///     because OPC UA HistoryRead's NumValuesPerNode is unsigned, while event reads use
    ///     <c>int</c> to allow zero / negative as a "use backend default cap" sentinel
    ///     (see <c>WonderwareHistorianClient</c> / <c>HistorianDataSource</c> usage). This test
    ///     pins both shapes so accidental changes are caught.
    /// </summary>
    /// <param name="methodName">The name of the method to test.</param>
    /// <param name="parameterName">The name of the parameter to verify.</param>
    /// <param name="expectedType">The expected parameter type.</param>
    [Theory]
    [InlineData("ReadRawAsync", "maxValuesPerNode", typeof(uint))]
    [InlineData("ReadEventsAsync", "maxEvents", typeof(int))]
    public void HistoryRead_MaxParameter_TypePinned(string methodName, string parameterName, Type expectedType)
    {
        var method = typeof(IHistorianDataSource).GetMethod(methodName);
        method.ShouldNotBeNull();
        var parameter = method!.GetParameters().FirstOrDefault(p => p.Name == parameterName);
        parameter.ShouldNotBeNull($"Method {methodName} should expose a '{parameterName}' parameter.");
        parameter!.ParameterType.ShouldBe(expectedType);
    }

    /// <summary>Verifies that history provider max parameters have the expected type.</summary>
    /// <param name="methodName">The name of the method to test.</param>
    /// <param name="parameterName">The name of the parameter to verify.</param>
    /// <param name="expectedType">The expected parameter type.</param>
    [Theory]
    [InlineData("ReadRawAsync", "maxValuesPerNode", typeof(uint))]
    [InlineData("ReadEventsAsync", "maxEvents", typeof(int))]
    public void HistoryProvider_MaxParameter_TypePinned(string methodName, string parameterName, Type expectedType)
    {
        var method = typeof(IHistoryProvider).GetMethod(methodName);
        method.ShouldNotBeNull();
        var parameter = method!.GetParameters().FirstOrDefault(p => p.Name == parameterName);
        parameter.ShouldNotBeNull($"Method {methodName} should expose a '{parameterName}' parameter.");
        parameter!.ParameterType.ShouldBe(expectedType);
    }

    /// <summary>
    ///     Core.Abstractions-009 (OpcUaServer-002): when <c>maxEvents &lt;= 0</c> the contract
    ///     requires implementations to set <see cref="HistoricalEventsResult.ContinuationPoint"/>
    ///     non-null when more results exist. Pins the <c>maxEvents</c> parameter type and the
    ///     <see cref="HistoricalEventsResult"/> continuation-point member so accidental removal
    ///     breaks this test — the downstream paging contract depends on both.
    /// </summary>
    [Fact]
    public void ReadEventsAsync_sentinel_and_continuation_contract_types_pinned()
    {
        var method = typeof(IHistorianDataSource).GetMethod("ReadEventsAsync");
        method.ShouldNotBeNull();

        var maxEventsParam = method!.GetParameters().FirstOrDefault(p => p.Name == "maxEvents");
        maxEventsParam.ShouldNotBeNull("maxEvents parameter must exist for the sentinel contract");
        maxEventsParam!.ParameterType.ShouldBe(typeof(int),
            "maxEvents is int (not uint) so callers can pass <=0 as a 'use backend default cap' sentinel");

        // HistoricalEventsResult must carry a nullable ContinuationPoint so implementations
        // can signal 'more results exist' (non-null) vs 'all results returned' (null).
        var resultType = typeof(HistoricalEventsResult);
        var continuationProp = resultType.GetProperty("ContinuationPoint");
        continuationProp.ShouldNotBeNull("HistoricalEventsResult.ContinuationPoint must exist for paging");
        continuationProp!.PropertyType.ShouldBe(typeof(byte[]),
            "ContinuationPoint is byte[]? — null means no more pages, non-null means caller must page");
    }

    /// <summary>
    ///     Core.Abstractions-008: <see cref="IHistoryProvider.ReadAtTimeAsync"/> and
    ///     <see cref="IHistoryProvider.ReadEventsAsync"/> are C# default interface methods
    ///     (drivers opt in), whereas <see cref="IHistorianDataSource.ReadAtTimeAsync"/> and
    ///     <see cref="IHistorianDataSource.ReadEventsAsync"/> are required (the server-side
    ///     historian must implement them). This test pins the asymmetry so an implementer cannot
    ///     accidentally collapse the two surfaces and so the documented rationale stays load-bearing.
    /// </summary>
    /// <param name="methodName">The name of the optional method to check.</param>
    [Theory]
    [InlineData("ReadAtTimeAsync")]
    [InlineData("ReadEventsAsync")]
    public void HistoryProvider_OptionalMethods_HaveDefaultImplementation(string methodName)
    {
        var providerMethod = typeof(IHistoryProvider).GetMethod(methodName);
        providerMethod.ShouldNotBeNull();
        // Default interface methods carry a method body — IsAbstract is false.
        providerMethod!.IsAbstract.ShouldBeFalse(
            $"IHistoryProvider.{methodName} must remain a default-impl-throws method so legacy drivers continue to compile.");

        var dataSourceMethod = typeof(IHistorianDataSource).GetMethod(methodName);
        dataSourceMethod.ShouldNotBeNull();
        dataSourceMethod!.IsAbstract.ShouldBeTrue(
            $"IHistorianDataSource.{methodName} must remain required — server-side historians own the full surface.");
    }
}
