using System.Collections.Concurrent;
using System.Text.Json;
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;

/// <summary>
///     H5d (DI seam): pins that <see cref="DriverHostActor.Props"/> accepts an
///     <see cref="IHistoryWriter"/> and threads it through to the REAL spawned
///     <see cref="VirtualTagHostActor"/> (the <c>virtualTagHostOverride</c> is deliberately NOT used,
///     so the production spawn path that wires <c>historyWriter</c> is exercised).
/// </summary>
/// <remarks>
///     This is a construction/threading smoke test, not a record-behaviour test. The record path —
///     "a Historize=true result is recorded with the IHistoryWriter under the folder-scoped NodeId" —
///     is already covered end-to-end against the host directly by
///     <c>VirtualTagHostActorTests.Historized_vtag_result_is_recorded_with_the_history_writer</c> (H5c).
///     Driving a real spawned child to emit a synthetic <see cref="VirtualTagActor.EvaluationResult"/>
///     from inside <see cref="DriverHostActor"/> would require reaching the auto-named grandchild and a
///     live evaluator/mux feed — heavy and brittle for no extra coverage. So here we assert the writer
///     threads through cleanly: a real apply carrying a Historize=true Equipment VirtualTag is Applied
///     with the real host spawned and the capturing writer wired in, with no construction error.
/// </remarks>
public sealed class DriverHostActorHistoryWriterTests : RuntimeActorTestBase
{
    private static readonly NodeId TestNode = NodeId.Parse("driver-hw-test");
    private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));

    /// <summary>Spawning a real DriverHostActor (no host override, publish actor wired so the real
    /// VirtualTagHostActor spawns) with a capturing IHistoryWriter applies a Historize=true VirtualTag
    /// deployment cleanly — proving Props/ctor accept the writer and thread it through the real spawn.</summary>
    [Fact]
    public void Props_accepts_history_writer_and_applies_historized_vtag_deployment()
    {
        var db = NewInMemoryDbFactory();
        var deploymentId = SeedDeploymentWithHistorizedVirtualTag(db, RevA);

        var coordinator = CreateTestProbe();
        // A publish actor MUST be wired for SpawnVirtualTagHost to spawn the real host (its ctor
        // requires a non-null sink). No virtualTagHostOverride ⇒ the real VirtualTagHostActor is
        // spawned with _historyWriter threaded in.
        var publish = CreateTestProbe();
        var writer = new CapturingHistoryWriter();

        var actor = Sys.ActorOf(DriverHostActor.Props(
            db, TestNode, coordinator.Ref,
            localRoles: new HashSet<string> { "driver" },
            opcUaPublishActor: publish.Ref,
            virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
            historyWriter: writer));

        actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));

        // The deployment Applies — the real VirtualTagHostActor spawned and accepted ApplyVirtualTags
        // with the writer threaded through (a construction/threading failure would have faulted PreStart
        // or the apply turn). Record behaviour itself is covered by VirtualTagHostActorTests (H5c).
        coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
    }

    private static DeploymentId SeedDeploymentWithHistorizedVirtualTag(
        IDbContextFactory<OtOpcUaConfigDbContext> db, RevisionHash rev)
    {
        var artifact = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Scripts = new[]
            {
                new
                {
                    ScriptId = "scr-1",
                    SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
                },
            },
            VirtualTags = new[]
            {
                new
                {
                    VirtualTagId = "vt-1",
                    EquipmentId = "eq-1",
                    Name = "Doubled",
                    DataType = "Float",
                    ScriptId = "scr-1",
                    Historize = true,
                },
            },
        });

        var id = DeploymentId.NewId();
        using var ctx = db.CreateDbContext();
        ctx.Deployments.Add(new Deployment
        {
            DeploymentId = id.Value,
            RevisionHash = rev.Value,
            Status = DeploymentStatus.Sealed,
            CreatedBy = "test",
            SealedAtUtc = DateTime.UtcNow,
            ArtifactBlob = artifact,
        });
        ctx.SaveChanges();
        return id;
    }

    /// <summary>Capturing <see cref="IHistoryWriter"/>: records every Record call so the threading
    /// smoke test can be tightened later if a record assertion becomes feasible here.</summary>
    private sealed class CapturingHistoryWriter : IHistoryWriter
    {
        public readonly ConcurrentQueue<(string Path, DataValueSnapshot Value)> Calls = new();

        /// <summary>Captures the path + snapshot of a Record call.</summary>
        /// <param name="path">The virtual tag path.</param>
        /// <param name="value">The data value snapshot.</param>
        public void Record(string path, DataValueSnapshot value) => Calls.Enqueue((path, value));
    }
}
