using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
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-010 regression. <see cref="HistorianConfiguration.RequestTimeoutSeconds"/>
///     was documented as the "outer safety timeout applied to sync-over-async Historian
///     operations" but was never read or enforced — a hung <c>StartQuery</c> or a slow
///     <c>MoveNext</c> could block the single pipe-server connection thread indefinitely.
///     The fix wires it into the read paths via a linked <see cref="CancellationTokenSource"/>
///     so the documented safety net actually exists.
///
///     The SDK-touching read methods cannot be unit-driven without a live AVEVA Historian.
///     This test pins the helper that derives the effective timeout from the config — the
///     read methods invoke that helper, so a regression in either the helper or the wiring
///     would break the test.
/// </summary>
[Trait("Category", "Unit")]
public sealed class HistorianDataSourceRequestTimeoutTests
{
    /// <summary>Verifies default request timeout is 60 seconds.</summary>
    [Fact]
    public void Default_request_timeout_is_60_seconds()
    {
        new HistorianConfiguration().RequestTimeoutSeconds.ShouldBe(60);
    }

    /// <summary>Verifies positive request timeout values are applied correctly.</summary>
    [Fact]
    public void Positive_request_timeout_is_used_verbatim()
    {
        InvokeBuildLinkedTokenSource(
            new HistorianConfiguration { RequestTimeoutSeconds = 30 },
            CancellationToken.None,
            out var cts);
        cts.ShouldNotBeNull();
        // The helper must wire CancelAfter — easiest cross-check is to observe that the
        // returned CTS is NOT already cancelled, and that disposing it is safe.
        cts!.IsCancellationRequested.ShouldBeFalse();
        cts.Dispose();
    }

    /// <summary>Verifies zero or negative timeout values disable the outer safety timeout.</summary>
    [Fact]
    public void Zero_or_negative_request_timeout_is_treated_as_no_timeout()
    {
        // A zero/negative value means "no outer timeout" — the helper must still return a
        // linked CTS so callers can use one code path, but it must not auto-cancel.
        InvokeBuildLinkedTokenSource(
            new HistorianConfiguration { RequestTimeoutSeconds = 0 },
            CancellationToken.None,
            out var cts);
        cts.ShouldNotBeNull();
        cts!.IsCancellationRequested.ShouldBeFalse();
        // Give the runtime a moment — a misconfigured CancelAfter(0) would fire immediately.
        Thread.Sleep(50);
        cts.IsCancellationRequested.ShouldBeFalse("RequestTimeoutSeconds <= 0 must not auto-cancel");
        cts.Dispose();
    }

    /// <summary>Verifies short timeout values correctly fire cancellation on the linked token.</summary>
    [Fact]
    public async Task Small_timeout_cancels_the_linked_token()
    {
        // 50 ms timeout — sleep 250 ms then assert the linked CTS has fired.
        InvokeBuildLinkedTokenSource(
            new HistorianConfiguration { RequestTimeoutSeconds = 1 }, // smallest non-zero whole-second value
            CancellationToken.None,
            out var cts);
        cts.ShouldNotBeNull();

        // The wall-clock cost of waiting a full second per test is acceptable — this
        // pins the actual CancelAfter wiring rather than just the conditional logic.
        await Task.Delay(1500);
        cts!.IsCancellationRequested.ShouldBeTrue("RequestTimeoutSeconds=1 must cancel within 1.5s");
        cts.Dispose();
    }

    /// <summary>Verifies caller's cancellation token propagates to the linked token.</summary>
    [Fact]
    public void Inbound_cancellation_propagates_into_the_linked_token()
    {
        using var outer = new CancellationTokenSource();
        InvokeBuildLinkedTokenSource(
            new HistorianConfiguration { RequestTimeoutSeconds = 60 },
            outer.Token,
            out var cts);
        cts.ShouldNotBeNull();
        cts!.IsCancellationRequested.ShouldBeFalse();

        outer.Cancel();
        cts.IsCancellationRequested.ShouldBeTrue("cancelling the caller's CT must cancel the linked CTS");
        cts.Dispose();
    }

    private static void InvokeBuildLinkedTokenSource(
        HistorianConfiguration cfg, CancellationToken ct, out CancellationTokenSource? cts)
    {
        // The helper is internal so the InternalsVisibleTo on the data-source project lets
        // us bind to it directly. Reflection keeps the test resilient if the method name is
        // ever shortened.
        var method = typeof(HistorianDataSource)
            .GetMethod("BuildRequestCts", BindingFlags.Static | BindingFlags.NonPublic);
        method.ShouldNotBeNull(
            "HistorianDataSource.BuildRequestCts must exist — wires RequestTimeoutSeconds into the read paths");
        cts = (CancellationTokenSource?)method!.Invoke(null, new object[] { cfg, ct });
    }
}
