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

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

/// <summary>
///     Task 9 — <see cref="MTConnectDriver"/>'s <see cref="IDriver"/> lifecycle over the
///     <see cref="IMTConnectAgentClient"/> seam: construction, initialize, reinitialize, shutdown,
///     health, footprint, and cache flush. Every test runs against
///     <see cref="CannedAgentClient"/>; no socket is opened anywhere in this file.
/// </summary>
/// <remarks>
///     The load-bearing assertions here are the ones about <b>what must NOT happen</b>: a
///     constructor that dials, an initialize that faults but still reports Healthy, a re-point that
///     keeps talking to the old Agent, and a cache flush that wedges browse. Each of those is a
///     defect the happy-path assertions alone would pass straight over.
/// </remarks>
public sealed class MTConnectDriverLifecycleTests
{
    private const string AgentUri = "http://fixture-agent:5000";

    /// <summary>Every dataItemId the canned probe declares — the browse-cache footprint basis.</summary>
    private const int FixtureDataItemCount = 7;

    private static MTConnectDriverOptions Opts(
        string agentUri = AgentUri,
        string? deviceName = null,
        int requestTimeoutMs = 5000,
        int heartbeatMs = 10000,
        int sampleIntervalMs = 1000,
        int sampleCount = 1000) =>
        new()
        {
            AgentUri = agentUri,
            DeviceName = deviceName,
            RequestTimeoutMs = requestTimeoutMs,
            HeartbeatMs = heartbeatMs,
            SampleIntervalMs = sampleIntervalMs,
            SampleCount = sampleCount,
            Tags =
            [
                new MTConnectTagDefinition("dev1_pos", DriverDataType.Float64),
                new MTConnectTagDefinition("dev1_execution", DriverDataType.String),
            ],
        };

    /// <summary>A driver wired to one canned client, plus that client, for the common case.</summary>
    private static (MTConnectDriver Driver, CannedAgentClient Client) NewDriver(
        MTConnectDriverOptions? options = null)
    {
        var client = CannedAgentClient.FromFixtures();

        return (new MTConnectDriver(options ?? Opts(), "mt1", _ => client), client);
    }

    private static async Task<(MTConnectDriver Driver, CannedAgentClient Client)> InitializedDriverAsync()
    {
        var (driver, client) = NewDriver();
        await driver.InitializeAsync("{}", CancellationToken.None);

        return (driver, client);
    }

    // ---- connection-free construction ----

    [Fact]
    public void Constructor_dials_nothing_and_does_not_even_build_a_client()
    {
        var factoryCalls = 0;
        var client = CannedAgentClient.FromFixtures();

        var driver = new MTConnectDriver(Opts(), "mt1", _ =>
        {
            factoryCalls++;

            return client;
        });

        // The universal discovery browser constructs a throwaway instance purely to ask CanBrowse.
        // If the ctor built (or worse, dialled) a client, every browse probe would open a connection
        // pool against a possibly-unreachable Agent.
        factoryCalls.ShouldBe(0);
        client.ProbeCallCount.ShouldBe(0);
        client.CurrentCallCount.ShouldBe(0);
        client.SampleCallCount.ShouldBe(0);
        driver.GetHealth().State.ShouldBe(DriverState.Unknown);
    }

    [Fact]
    public void Identity_is_the_constructor_arguments()
    {
        var (driver, _) = NewDriver();

        driver.DriverInstanceId.ShouldBe("mt1");
        driver.DriverType.ShouldBe("MTConnect");
    }

    // ---- initialize ----

    [Fact]
    public async Task Initialize_primes_index_and_reports_healthy()
    {
        var (driver, client) = NewDriver();

        await driver.InitializeAsync("{}", CancellationToken.None);

        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
        client.ProbeCallCount.ShouldBe(1);
        client.CurrentCallCount.ShouldBe(1);

        // The /current snapshot is what makes the driver able to answer a read at all.
        driver.ObservationIndex.Count.ShouldBe(FixtureDataItemCount);
        driver.ObservationIndex.Get("dev1_pos").StatusCode.ShouldBe(0u);
    }

    [Fact]
    public async Task Initialize_records_the_agent_instance_id_and_the_probe_model()
    {
        var (driver, client) = NewDriver();

        await driver.InitializeAsync("{}", CancellationToken.None);

        driver.AgentInstanceId.ShouldBe(client.Current.InstanceId);
        driver.CachedProbeModel.ShouldNotBeNull();
        driver.CachedProbeModel!.Devices.Count.ShouldBe(1);
    }

    [Fact]
    public async Task Initialize_stamps_last_successful_read_between_the_call_boundaries()
    {
        var (driver, _) = NewDriver();

        var before = DateTime.UtcNow;
        await driver.InitializeAsync("{}", CancellationToken.None);
        var after = DateTime.UtcNow;

        var lastRead = driver.GetHealth().LastSuccessfulRead;
        lastRead.ShouldNotBeNull();
        lastRead!.Value.ShouldBeGreaterThanOrEqualTo(before);
        lastRead.Value.ShouldBeLessThanOrEqualTo(after);
    }

    [Fact]
    public async Task Initialize_faults_and_rethrows_when_probe_fails()
    {
        var (driver, client) = NewDriver();
        client.ProbeFailure = new HttpRequestException("agent unreachable");

        var thrown = await Should.ThrowAsync<HttpRequestException>(
            () => driver.InitializeAsync("{}", CancellationToken.None));

        thrown.Message.ShouldBe("agent unreachable");

        var health = driver.GetHealth();
        health.State.ShouldBe(DriverState.Faulted);
        health.LastError.ShouldNotBeNull();
        health.LastError!.ShouldContain("agent unreachable");

        // No half-initialized object: the client it built must not survive the failure.
        client.IsDisposed.ShouldBeTrue();
    }

    [Fact]
    public async Task Initialize_faults_when_probe_succeeds_but_the_priming_current_fails()
    {
        // Deliberate posture (documented on InitializeAsync): a driver whose /probe worked but whose
        // priming /current did not has an EMPTY value index and no sample cursor. Reporting Healthy
        // there would render every tag BadWaitingForInitialData forever while the dashboard shows a
        // green driver — the #485 "a failure that looks like success" shape.
        var (driver, client) = NewDriver();
        client.CurrentFailure = new InvalidDataException("current unparseable");

        await Should.ThrowAsync<InvalidDataException>(
            () => driver.InitializeAsync("{}", CancellationToken.None));

        client.ProbeCallCount.ShouldBe(1);
        driver.GetHealth().State.ShouldBe(DriverState.Faulted);
        driver.GetHealth().State.ShouldNotBe(DriverState.Healthy);
        driver.CachedProbeModel.ShouldBeNull();
        driver.ObservationIndex.Count.ShouldBe(0);
        client.IsDisposed.ShouldBeTrue();
    }

    /// <summary>
    ///     arch-review 01/S-6: an operator-authorable <c>0</c> must never come to mean "wait
    ///     forever" (or "ask the Agent for nothing"). All four knobs share one
    ///     <c>RequirePositive</c> path, so all four are asserted — a guard that covered only the
    ///     one knob with a test is exactly how three of them would quietly lose the check.
    /// </summary>
    /// <remarks>
    ///     Asserted through a FAKE client factory, so this proves the DRIVER validates rather than
    ///     leaning on the production client's own constructor guard.
    /// </remarks>
    [Theory]
    [InlineData("RequestTimeoutMs")]
    [InlineData("HeartbeatMs")]
    [InlineData("SampleIntervalMs")]
    [InlineData("SampleCount")]
    public async Task Initialize_rejects_a_non_positive_timing_knob_before_dialling(string knob)
    {
        var options = knob switch
        {
            "RequestTimeoutMs" => Opts(requestTimeoutMs: 0),
            "HeartbeatMs" => Opts(heartbeatMs: 0),
            "SampleIntervalMs" => Opts(sampleIntervalMs: 0),
            "SampleCount" => Opts(sampleCount: 0),
            _ => throw new ArgumentOutOfRangeException(nameof(knob), knob, "unhandled knob"),
        };

        var factoryCalls = 0;
        var driver = new MTConnectDriver(options, "mt1", _ =>
        {
            factoryCalls++;

            return CannedAgentClient.FromFixtures();
        });

        var thrown = await Should.ThrowAsync<ArgumentOutOfRangeException>(
            () => driver.InitializeAsync("{}", CancellationToken.None));

        // The message must name the offending key, or an operator cannot act on it.
        thrown.Message.ShouldContain(knob);
        factoryCalls.ShouldBe(0);
        driver.GetHealth().State.ShouldBe(DriverState.Faulted);
    }

    // ---- the driverConfigJson argument ----

    [Fact]
    public async Task Initialize_honours_the_config_json_over_the_constructor_options()
    {
        // The runtime delivers a config change ONLY through this argument (DriverInstanceActor
        // ApplyDelta -> ReinitializeAsync(json) on the LIVE instance). Ignoring it would make every
        // MTConnect config change a silent no-op that still seals the deployment green.
        MTConnectDriverOptions? seen = null;
        var driver = new MTConnectDriver(Opts(), "mt1", o =>
        {
            seen = o;

            return CannedAgentClient.FromFixtures();
        });

        await driver.InitializeAsync(
            """{"agentUri":"http://other-agent:5001","deviceName":"VMC-3Axis"}""",
            CancellationToken.None);

        seen.ShouldNotBeNull();
        seen!.AgentUri.ShouldBe("http://other-agent:5001");
        seen.DeviceName.ShouldBe("VMC-3Axis");
    }

    /// <summary>
    ///     "Semantically empty" is decided by parsing, not by matching the literal two-character
    ///     spellings. A pretty-printer, a formatter, or a hand edit turns <c>{}</c> into <c>{ }</c>
    ///     or <c>{\n}</c> without changing its meaning — and under literal matching each of those
    ///     reached ParseOptions, failed the required-AgentUri check, and turned an empty document
    ///     into a cold-start fault or a rejected deployment.
    /// </summary>
    [Theory]
    [InlineData("{}")]
    [InlineData("{ }")]
    [InlineData("{\n}")]
    [InlineData("  {\r\n\t}  ")]
    [InlineData("[]")]
    [InlineData("[ ]")]
    [InlineData("null")]
    [InlineData("")]
    [InlineData("   ")]
    public async Task Initialize_with_a_semantically_empty_config_body_keeps_the_constructor_options(string json)
    {
        MTConnectDriverOptions? seen = null;
        var driver = new MTConnectDriver(Opts(), "mt1", o =>
        {
            seen = o;

            return CannedAgentClient.FromFixtures();
        });

        await driver.InitializeAsync(json, CancellationToken.None);

        seen.ShouldNotBeNull();
        seen!.AgentUri.ShouldBe(AgentUri);
        seen.Tags.Count.ShouldBe(2);
        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
    }

    [Fact]
    public async Task Initialize_treats_a_malformed_config_body_as_a_parse_error_not_as_empty()
    {
        // The other half of the emptiness rule: unreadable text must NOT fall through to "no config
        // supplied" and silently start the driver on stale options.
        var (driver, _) = NewDriver();

        var thrown = await Should.ThrowAsync<InvalidOperationException>(
            () => driver.InitializeAsync("""{"agentUri": """, CancellationToken.None));

        thrown.Message.ShouldContain("not valid JSON");
        driver.GetHealth().State.ShouldBe(DriverState.Faulted);
    }

    [Fact]
    public async Task Initialize_faults_on_a_config_body_with_no_agent_uri()
    {
        var (driver, _) = NewDriver();

        await Should.ThrowAsync<InvalidOperationException>(
            () => driver.InitializeAsync("""{"deviceName":"VMC-3Axis"}""", CancellationToken.None));

        driver.GetHealth().State.ShouldBe(DriverState.Faulted);
    }

    [Fact]
    public async Task Initialize_on_a_live_instance_with_an_unreadable_config_keeps_it_serving()
    {
        // One rule for both entry points: an unreadable document never destroys working state; it
        // faults only a driver that had none. InitializeAsync IS reachable on a live instance
        // (DriverInstanceActor re-initialises during Connecting/Reconnecting), so parsing must
        // happen BEFORE the teardown — otherwise a bad edit kills a healthy client and the driver
        // ends up in exactly the state ReinitializeAsync is written to avoid.
        var (driver, client) = await InitializedDriverAsync();

        await Should.ThrowAsync<InvalidOperationException>(
            () => driver.InitializeAsync("""{"deviceName":"no-agent-uri"}""", CancellationToken.None));

        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
        driver.EffectiveOptions.AgentUri.ShouldBe(AgentUri);
        client.IsDisposed.ShouldBeFalse();
        driver.ObservationIndex.Count.ShouldBe(FixtureDataItemCount);
    }

    [Fact]
    public async Task Initialize_parses_authored_tags_out_of_the_config_json()
    {
        MTConnectDriverOptions? seen = null;
        var driver = new MTConnectDriver(Opts(), "mt1", o =>
        {
            seen = o;

            return CannedAgentClient.FromFixtures();
        });

        await driver.InitializeAsync(
            """
            {"agentUri":"http://a:5000",
             "tags":[{"fullName":"dev1_partcount","driverDataType":"Int32"}]}
            """,
            CancellationToken.None);

        seen.ShouldNotBeNull();
        seen!.Tags.Count.ShouldBe(1);
        seen.Tags[0].FullName.ShouldBe("dev1_partcount");

        // Enum-serialization trap (project-wide MEMORY): enum-carrying config fields are NAMES.
        seen.Tags[0].DriverDataType.ShouldBe(DriverDataType.Int32);
    }

    // ---- reinitialize ----

    [Fact]
    public async Task Reinitialize_with_unchanged_endpoint_config_keeps_the_same_client()
    {
        var built = new List<CannedAgentClient>();
        var driver = new MTConnectDriver(Opts(), "mt1", _ =>
        {
            var c = CannedAgentClient.FromFixtures();
            built.Add(c);

            return c;
        });

        await driver.InitializeAsync("{}", CancellationToken.None);
        await driver.ReinitializeAsync(
            $$"""{"agentUri":"{{AgentUri}}","tags":[{"fullName":"dev1_pos","driverDataType":"Float64"}]}""",
            CancellationToken.None);

        // Config-only: the live connection pool survives, so no client churn...
        built.Count.ShouldBe(1);
        built[0].IsDisposed.ShouldBeFalse();

        // ...but everything DERIVED from config is genuinely rebuilt against the new document.
        built[0].ProbeCallCount.ShouldBe(2);
        built[0].CurrentCallCount.ShouldBe(2);
        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
    }

    [Fact]
    public async Task Reinitialize_with_a_changed_agent_uri_tears_down_and_rebuilds_the_client()
    {
        var built = new List<(MTConnectDriverOptions Options, CannedAgentClient Client)>();
        var driver = new MTConnectDriver(Opts(), "mt1", o =>
        {
            var c = CannedAgentClient.FromFixtures();
            built.Add((o, c));

            return c;
        });

        await driver.InitializeAsync("{}", CancellationToken.None);
        await driver.ReinitializeAsync(
            """{"agentUri":"http://relocated-agent:5000"}""", CancellationToken.None);

        // A re-pointed driver that kept the old client would keep reading the OLD machine while the
        // deployment reports success.
        built.Count.ShouldBe(2);
        built[0].Client.IsDisposed.ShouldBeTrue();
        built[1].Options.AgentUri.ShouldBe("http://relocated-agent:5000");
        built[1].Client.IsDisposed.ShouldBeFalse();
        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
    }

    [Fact]
    public async Task Reinitialize_with_a_changed_device_scope_rebuilds_the_client()
    {
        var built = new List<(MTConnectDriverOptions Options, CannedAgentClient Client)>();
        var driver = new MTConnectDriver(Opts(), "mt1", o =>
        {
            var c = CannedAgentClient.FromFixtures();
            built.Add((o, c));

            return c;
        });

        await driver.InitializeAsync("{}", CancellationToken.None);
        await driver.ReinitializeAsync(
            $$"""{"agentUri":"{{AgentUri}}","deviceName":"VMC-3Axis"}""", CancellationToken.None);

        // DeviceName is baked into every request URI at client construction.
        built.Count.ShouldBe(2);
        built[0].Client.IsDisposed.ShouldBeTrue();
        built[1].Options.DeviceName.ShouldBe("VMC-3Axis");
    }

    [Fact]
    public async Task Reinitialize_with_changed_request_knobs_rebuilds_the_client()
    {
        // RequestTimeoutMs / HeartbeatMs / SampleIntervalMs / SampleCount are all read by the client
        // CONSTRUCTOR (deadlines, watchdog window, /sample query string). Keeping the old client
        // because the URI happened not to change would silently discard the operator's edit.
        var built = new List<(MTConnectDriverOptions Options, CannedAgentClient Client)>();
        var driver = new MTConnectDriver(Opts(), "mt1", o =>
        {
            var c = CannedAgentClient.FromFixtures();
            built.Add((o, c));

            return c;
        });

        await driver.InitializeAsync("{}", CancellationToken.None);
        await driver.ReinitializeAsync(
            $$"""{"agentUri":"{{AgentUri}}","heartbeatMs":2500}""", CancellationToken.None);

        built.Count.ShouldBe(2);
        built[1].Options.HeartbeatMs.ShouldBe(2500);
        built[0].Client.IsDisposed.ShouldBeTrue();
    }

    [Fact]
    public async Task Reinitialize_before_initialize_behaves_as_initialize()
    {
        var (driver, client) = NewDriver();

        await driver.ReinitializeAsync("{}", CancellationToken.None);

        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
        client.ProbeCallCount.ShouldBe(1);
    }

    [Fact]
    public async Task Reinitialize_that_fails_leaves_the_driver_faulted_not_healthy()
    {
        var built = new List<CannedAgentClient>();
        var driver = new MTConnectDriver(Opts(), "mt1", _ =>
        {
            var c = CannedAgentClient.FromFixtures();
            built.Add(c);

            return c;
        });

        await driver.InitializeAsync("{}", CancellationToken.None);
        built[0].CurrentFailure = new HttpRequestException("agent went away");

        // Config-only shape (only the tag list changes), so the failing leg is the re-prime against
        // the client that is already live — the path a rebuild would otherwise mask.
        await Should.ThrowAsync<HttpRequestException>(
            () => driver.ReinitializeAsync(
                $$"""{"agentUri":"{{AgentUri}}","tags":[{"fullName":"dev1_pos"}]}""", CancellationToken.None));

        built.Count.ShouldBe(1);
        driver.GetHealth().State.ShouldBe(DriverState.Faulted);
        driver.GetHealth().LastError.ShouldNotBeNull().ShouldContain("agent went away");

        // A failed refresh must not leave a live client nobody owns.
        built[0].IsDisposed.ShouldBeTrue();
    }

    [Fact]
    public async Task Reinitialize_with_an_unreadable_config_keeps_the_running_driver_serving()
    {
        // Blast radius: an unparseable config edit fails the DEPLOYMENT (ApplyResult(false)); it must
        // not down a driver that is happily serving its previous, valid configuration.
        var (driver, client) = await InitializedDriverAsync();

        await Should.ThrowAsync<InvalidOperationException>(
            () => driver.ReinitializeAsync("""{"deviceName":"no-agent-uri"}""", CancellationToken.None));

        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
        driver.EffectiveOptions.AgentUri.ShouldBe(AgentUri);
        client.IsDisposed.ShouldBeFalse();
        driver.ObservationIndex.Count.ShouldBe(FixtureDataItemCount);
    }

    // ---- shutdown ----

    [Fact]
    public async Task Shutdown_disposes_the_client_and_drops_the_served_state()
    {
        var (driver, client) = await InitializedDriverAsync();
        var lastRead = driver.GetHealth().LastSuccessfulRead;

        await driver.ShutdownAsync(CancellationToken.None);

        client.DisposeCount.ShouldBe(1);
        driver.GetHealth().State.ShouldBe(DriverState.Unknown);

        // Retained: it is diagnostic ("when did we last hear from the Agent"), not served data.
        driver.GetHealth().LastSuccessfulRead.ShouldBe(lastRead);

        // Dropped: values from a torn-down connection must never keep reading Good.
        driver.ObservationIndex.Count.ShouldBe(0);
        driver.CachedProbeModel.ShouldBeNull();
    }

    [Fact]
    public async Task Shutdown_is_idempotent()
    {
        var (driver, client) = await InitializedDriverAsync();

        await driver.ShutdownAsync(CancellationToken.None);
        await driver.ShutdownAsync(CancellationToken.None);

        client.DisposeCount.ShouldBe(1);
        driver.GetHealth().State.ShouldBe(DriverState.Unknown);
    }

    [Fact]
    public async Task Shutdown_before_initialize_is_a_no_op()
    {
        var (driver, client) = NewDriver();

        await driver.ShutdownAsync(CancellationToken.None);

        client.DisposeCount.ShouldBe(0);
        driver.GetHealth().State.ShouldBe(DriverState.Unknown);
    }

    // ---- footprint + flush ----

    [Fact]
    public async Task Memory_footprint_grows_when_initialize_populates_the_caches()
    {
        var (driver, _) = NewDriver();

        var before = driver.GetMemoryFootprint();
        await driver.InitializeAsync("{}", CancellationToken.None);
        var after = driver.GetMemoryFootprint();

        // Before init the only config-attributable state is the two authored tags.
        before.ShouldBeLessThan(after);
        after.ShouldBeGreaterThan(0);
    }

    [Fact]
    public async Task Flush_drops_the_probe_cache_and_keeps_the_observation_index()
    {
        var (driver, _) = await InitializedDriverAsync();
        var before = driver.GetMemoryFootprint();

        await driver.FlushOptionalCachesAsync(CancellationToken.None);

        driver.GetMemoryFootprint().ShouldBeLessThan(before);
        driver.CachedProbeModel.ShouldBeNull();

        // The index is required for correctness (it IS the read surface) — never flushable.
        driver.ObservationIndex.Count.ShouldBe(FixtureDataItemCount);
        driver.ObservationIndex.Get("dev1_pos").StatusCode.ShouldBe(0u);
        driver.GetHealth().State.ShouldBe(DriverState.Healthy);
    }

    [Fact]
    public async Task Flush_does_not_wedge_the_driver_the_probe_model_is_refetched_on_demand()
    {
        // Task 12's DiscoverAsync reads the probe model. If flushing left no way back to it, a
        // cache-budget breach would permanently disable browse on that driver instance.
        var (driver, client) = await InitializedDriverAsync();
        await driver.FlushOptionalCachesAsync(CancellationToken.None);

        client.ProbeCallCount.ShouldBe(1);

        var model = await driver.GetOrFetchProbeModelAsync(CancellationToken.None);

        model.Devices.Count.ShouldBe(1);
        client.ProbeCallCount.ShouldBe(2);
        driver.CachedProbeModel.ShouldNotBeNull();
    }

    [Fact]
    public async Task Probe_model_is_served_from_cache_while_it_is_warm()
    {
        var (driver, client) = await InitializedDriverAsync();

        _ = await driver.GetOrFetchProbeModelAsync(CancellationToken.None);

        client.ProbeCallCount.ShouldBe(1);
    }

    [Fact]
    public async Task Fetching_the_probe_model_before_initialize_is_rejected_rather_than_silently_empty()
    {
        var (driver, _) = NewDriver();

        await Should.ThrowAsync<InvalidOperationException>(
            () => driver.GetOrFetchProbeModelAsync(CancellationToken.None));
    }

    [Fact]
    public async Task Fetching_the_probe_model_after_shutdown_is_rejected()
    {
        var (driver, _) = await InitializedDriverAsync();
        await driver.ShutdownAsync(CancellationToken.None);

        await Should.ThrowAsync<InvalidOperationException>(
            () => driver.GetOrFetchProbeModelAsync(CancellationToken.None));
    }

    // ---- lock-free session snapshot: the probe fetch races the lifecycle ----

    [Fact]
    public async Task A_shutdown_landing_mid_fetch_surfaces_as_not_connected_not_as_object_disposed()
    {
        // Browse is deliberately lock-free (it awaits the network; holding the lifecycle lock would
        // let it stall a shutdown for a whole RequestTimeoutMs), so a fetch CAN outlive the client
        // it captured. What must not happen is a raw ObjectDisposedException escaping into browse —
        // the caller already handles "not connected" and should have exactly one failure mode.
        var (driver, client) = await InitializedDriverAsync();
        await driver.FlushOptionalCachesAsync(CancellationToken.None);

        // Held locally: the gate is one-shot, so the client has already cleared its own reference by
        // the time the request is parked on it.
        var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        client.ProbeGate = gate;
        var inFlight = driver.GetOrFetchProbeModelAsync(CancellationToken.None);

        await driver.ShutdownAsync(CancellationToken.None);
        client.IsDisposed.ShouldBeTrue();
        gate.TrySetResult();

        var thrown = await Should.ThrowAsync<InvalidOperationException>(() => inFlight);
        thrown.InnerException.ShouldBeOfType<ObjectDisposedException>();
    }

    [Fact]
    public async Task A_fetch_that_completes_after_a_reinitialize_does_not_overwrite_the_newer_cache()
    {
        // The probe cache is re-published only onto the session it was fetched against. Without
        // that check, a slow browse would install a device model belonging to a superseded
        // configuration over the one the re-initialize just established — stale browse output that
        // nothing would ever correct.
        var built = new List<CannedAgentClient>();
        var driver = new MTConnectDriver(Opts(), "mt1", _ =>
        {
            var c = CannedAgentClient.FromFixtures();
            built.Add(c);

            return c;
        });

        await driver.InitializeAsync("{}", CancellationToken.None);
        await driver.FlushOptionalCachesAsync(CancellationToken.None);

        var client = built[0];
        var staleModel = client.Probe;

        // Park a fetch on the OLD (about to be superseded) session; it captures staleModel now.
        // Held locally — the gate is one-shot and clears the client's own reference when it parks.
        var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        client.ProbeGate = gate;
        var inFlight = driver.GetOrFetchProbeModelAsync(CancellationToken.None);

        // A config-only re-initialize: same client (so nothing is disposed), brand-new session.
        var freshModel = new MTConnectProbeModel(staleModel.Devices);
        client.Probe = freshModel;
        await driver.ReinitializeAsync(
            $$"""{"agentUri":"{{AgentUri}}","tags":[{"fullName":"dev1_pos","driverDataType":"Float64"}]}""",
            CancellationToken.None);

        built.Count.ShouldBe(1);
        driver.CachedProbeModel.ShouldBeSameAs(freshModel);

        // Now let the stale fetch finish. It still returns what the Agent gave it...
        gate.TrySetResult();
        (await inFlight).ShouldBeSameAs(staleModel);

        // ...but it must not have published that over the newer session's cache.
        driver.CachedProbeModel.ShouldBeSameAs(freshModel);
    }

    [Fact]
    public async Task Flush_after_shutdown_is_a_no_op_and_does_not_resurrect_the_session()
    {
        // Core polls the footprint and asks for a flush on its own schedule, so a flush arriving
        // after the driver was torn down is ordinary, not exotic. It must observe the retired
        // session and do nothing — publishing a "flushed" copy of it would reinstate a session
        // holding an already-disposed client for every later caller.
        //
        // NOTE: this pins the retired-session guard, NOT the CompareExchange beneath it. Flush has
        // no await point, so nothing can be interleaved between its read and its publish from a
        // single-threaded test; the CAS is defence-in-depth against a genuinely concurrent
        // lifecycle call and is deliberately claimed as such rather than as tested behaviour. The
        // fetch-side CAS, which does have an await point, is covered by the test above.
        var (driver, client) = await InitializedDriverAsync();

        await driver.ShutdownAsync(CancellationToken.None);
        await driver.FlushOptionalCachesAsync(CancellationToken.None);

        driver.CachedProbeModel.ShouldBeNull();
        driver.GetHealth().State.ShouldBe(DriverState.Unknown);
        client.DisposeCount.ShouldBe(1);

        await Should.ThrowAsync<InvalidOperationException>(
            () => driver.GetOrFetchProbeModelAsync(CancellationToken.None));
    }
}
