using Moq;
using Opc.Ua;
using Opc.Ua.Client;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;

namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests;

/// <summary>
///     Unit-only coverage of <see cref="OpcUaClientDriverBrowser"/>'s pre-connect
///     validation. These tests do not require a live OPC UA endpoint and are safe to
///     run without the opc-plc Docker fixture.
/// </summary>
[Trait("Category", "Unit")]
public sealed class OpcUaClientDriverBrowserTests
{
    private readonly OpcUaClientDriverBrowser _sut = new();

    /// <summary>The DriverType key must match the AdminUI's persisted value.</summary>
    [Fact]
    public void DriverType_is_OpcUaClient() => _sut.DriverType.ShouldBe("OpcUaClient");

    /// <summary>An empty endpoint must fail fast with a clear EndpointUrl-mentioning message.</summary>
    [Fact]
    public async Task OpenAsync_with_empty_endpoint_throws()
    {
        var json = """{"EndpointUrl":"","EndpointUrls":[]}""";
        var ex = await Should.ThrowAsync<InvalidOperationException>(
            () => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
        ex.Message.ShouldContain("EndpointUrl");
    }

    /// <summary>A JSON literal that deserializes to null must fail fast.</summary>
    [Fact]
    public async Task OpenAsync_with_null_json_throws()
    {
        var ex = await Should.ThrowAsync<InvalidOperationException>(
            () => _sut.OpenAsync("null", TestContext.Current.CancellationToken));
        ex.Message.ShouldContain("null");
    }

    /// <summary>Certificate auth is not supported by the browser; the failure message
    /// must say so explicitly rather than surfacing a downstream COM/SDK error.
    /// <c>OpcUaAuthType.Certificate</c> serializes as the numeric value 2 under the
    /// browser's default System.Text.Json options (no string-enum converter).</summary>
    [Fact]
    public async Task OpenAsync_with_certificate_auth_throws_clear_message()
    {
        var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":2}""";
        var ex = await Should.ThrowAsync<InvalidOperationException>(
            () => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
        ex.Message.ShouldContain("Certificate");
    }

    // ---- Driver.OpcUaClient.Browser-003: JsonStringEnumConverter must be present ----

    /// <summary>
    ///     AdminUI emits enum-valued driver-config fields (SecurityPolicy, SecurityMode,
    ///     AuthType, TargetNamespaceKind) as their <b>string</b> names (e.g.
    ///     <c>"SecurityPolicy":"Basic256Sha256"</c>). Without a
    ///     <see cref="System.Text.Json.Serialization.JsonStringEnumConverter"/> on the browser's
    ///     <c>JsonSerializerOptions</c> the deserialization throws a
    ///     <see cref="System.Text.Json.JsonException"/> before the browser's own validation can
    ///     run, giving the caller a confusing parse error instead of the expected
    ///     domain-specific message.
    ///
    ///     This test passes a JSON blob with <c>"AuthType":"Certificate"</c> (string form) and
    ///     asserts that the browser's validation fires — i.e. deserialization succeeds and the
    ///     resulting <see cref="InvalidOperationException"/> mentions "Certificate". Before the
    ///     fix, a raw <see cref="System.Text.Json.JsonException"/> surfaces instead.
    /// </summary>
    [Fact]
    public async Task OpenAsync_with_string_enum_AuthType_deserializes_correctly()
    {
        // "AuthType":"Certificate" — STRING form, as AdminUI emits.
        var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":"Certificate"}""";
        // The browser should reach its own Certificate-auth guard and throw
        // InvalidOperationException with a message that mentions "Certificate".
        // Without JsonStringEnumConverter the deserialization itself throws JsonException,
        // which means this Should.ThrowAsync<InvalidOperationException> assertion fails.
        var ex = await Should.ThrowAsync<InvalidOperationException>(
            () => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
        // The browser's own guard must fire (not a JsonException from the deserializer).
        ex.Message.ShouldContain("Certificate");
    }

    /// <summary>
    ///     Companion to <see cref="OpenAsync_with_string_enum_AuthType_deserializes_correctly"/>:
    ///     confirms that <see cref="System.Text.Json.Serialization.JsonStringEnumConverter"/>
    ///     still accepts the integer/numeric ordinal form that the live-fixture tests currently
    ///     rely on, so no regression is introduced for existing numeric-authored configs.
    /// </summary>
    [Fact]
    public async Task OpenAsync_with_numeric_enum_AuthType_still_works()
    {
        // "AuthType":2 — numeric ordinal form (OpcUaAuthType.Certificate == 2).
        var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":2}""";
        var ex = await Should.ThrowAsync<InvalidOperationException>(
            () => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
        // JsonStringEnumConverter accepts both string and numeric ordinal forms.
        ex.Message.ShouldContain("Certificate");
    }

    // ---- Driver.OpcUaClient.Browser-001: AttributesAsync must refresh LastUsedUtc ----

    /// <summary>
    ///     <see cref="IBrowseSession.LastUsedUtc"/> must be updated on every call
    ///     including <see cref="OpcUaClientBrowseSession.AttributesAsync"/>, which the
    ///     <see cref="BrowseSessionReaper"/> uses for idle eviction. Before the fix
    ///     the method returned immediately without touching the property, causing a
    ///     session that only received <c>AttributesAsync</c> calls to be evicted early.
    /// </summary>
    [Fact]
    public async Task AttributesAsync_updates_LastUsedUtc()
    {
        var ct = TestContext.Current.CancellationToken;

        // Arrange: build a minimal OpcUaClientBrowseSession without a live server.
        // AttributesAsync does not call ISession — MockBehavior.Loose is safe.
        var mockSession = new Mock<ISession>(MockBehavior.Loose);
        var nsMap = NamespaceMap.FromTable(new NamespaceTable());
        var sut = new OpcUaClientBrowseSession(mockSession.Object, nsMap, ObjectIds.ObjectsFolder);

        var before = sut.LastUsedUtc;

        // Introduce a small delay so clock advances at least one tick.
        await Task.Delay(5, ct);

        // Act
        var attrs = await sut.AttributesAsync("nsu=http://opcfoundation.org/UA/;i=85", ct);

        // Assert: LastUsedUtc must have been refreshed, and the result must be empty
        // (OPC UA picker treats variables as leaves, no attribute side-panel).
        attrs.ShouldBeEmpty();
        sut.LastUsedUtc.ShouldBeGreaterThan(before,
            "AttributesAsync must refresh LastUsedUtc to satisfy the IBrowseSession contract");
    }
}
