using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;

/// <summary>
///     Gitea #516 — FOCAS was excluded from the first pass because it derives MORE than options from config:
///     the <c>Backend</c> key selects its <see cref="IFocasClientFactory"/>, injected at construction. Adopting
///     a new <c>FocasDriverOptions</c> alone would poll a NEW device/tag set through the OLD backend, which
///     looks like it worked and does not.
///     <para><c>ParseBinding</c> now returns options + backend together and <c>InitializeAsync</c> adopts both
///     or neither. These tests pin the BACKEND moving — the half that was missing — because a test that only
///     checked options would have passed against the broken version.</para>
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasReinitConfigAdoptionTests
{
    /// <summary>
    ///     Reinitializing onto <c>Backend: "unimplemented"</c> must make the driver adopt that backend, whose
    ///     <c>EnsureUsable()</c> throws by design. A driver still holding the constructor's <c>wire</c>
    ///     backend would not throw — so the throw is a direct read of which backend is live.
    /// </summary>
    [Fact]
    public async Task Reinitialize_adopts_a_changed_backend_not_just_the_options()
    {
        var driver = FocasDriverFactoryExtensions.CreateInstance(
            "focas-1", """{"backend":"wire","devices":[]}""");

        var ex = await Record.ExceptionAsync(() => driver.ReinitializeAsync(
            """{"backend":"unimplemented","devices":[]}""", CancellationToken.None));

        ex.ShouldBeOfType<NotSupportedException>(
            "FocasDriver adopted a reinitialized config but kept its ORIGINAL backend — the half-applied "
            + "adoption (#516) that would poll a new device/tag set through the old backend");
    }

    /// <summary>The other direction: moving OFF the unimplemented backend must also take effect, so the
    /// adoption is not a one-way latch.</summary>
    [Fact]
    public async Task Reinitialize_away_from_the_unimplemented_backend_also_takes_effect()
    {
        var driver = FocasDriverFactoryExtensions.CreateInstance(
            "focas-1", """{"backend":"unimplemented","devices":[]}""");

        // Sanity: the constructor-supplied backend is the throwing one.
        (await Record.ExceptionAsync(() => driver.InitializeAsync("{}", CancellationToken.None)))
            .ShouldBeOfType<NotSupportedException>();

        var ex = await Record.ExceptionAsync(() => driver.ReinitializeAsync(
            """{"backend":"wire","devices":[]}""", CancellationToken.None));

        ex.ShouldNotBeOfType<NotSupportedException>(
            "FocasDriver stayed on the unimplemented backend after a reinitialize that selected 'wire'");
    }

    /// <summary>An empty/placeholder document keeps the constructor-supplied pair, so the lifecycle tests
    /// that pass <c>"{}"</c> keep meaning what they meant.</summary>
    [Fact]
    public async Task Reinitialize_with_an_empty_document_keeps_the_constructor_backend()
    {
        var driver = FocasDriverFactoryExtensions.CreateInstance(
            "focas-1", """{"backend":"unimplemented","devices":[]}""");

        (await Record.ExceptionAsync(() => driver.ReinitializeAsync("{}", CancellationToken.None)))
            .ShouldBeOfType<NotSupportedException>();
    }
}
