using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult;

namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;

/// <summary>
/// Regression tests for SubscribeCommand summary bucketing, --duration, --quiet, --summary-file,
/// and recursive collection. Covers Client.CLI-002, -009, and -010.
/// </summary>
public class SubscribeCommandSummaryTests
{
    /// <summary>Verifies that nodes with no updates are counted separately from suspects.</summary>
    [Fact]
    public async Task Summary_NodeWithNoUpdate_IsCountedAsNeverNotAsNeverWentBad()
    {
        // Client.CLI-002: A node that received no updates at all is "never received an update",
        // NOT a "suspect that never went bad".
        var fakeService = new FakeOpcUaClientService();
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var command = new SubscribeCommand(factory)
        {
            Url = "opc.tcp://localhost:4840",
            NodeId = "ns=2;s=SilentNode",
            DurationSeconds = 1
        };

        using var console = TestConsoleHelper.CreateConsole();
        await command.ExecuteAsync(console);

        var output = TestConsoleHelper.GetOutput(console);
        output.ShouldContain("No update received at all:      1");
        output.ShouldContain("NEVER went bad (suspect):       0");
        // The "suspect" detail header should not appear when the suspect list is empty.
        output.ShouldNotContain("--- Nodes that NEVER received a bad-quality update (suspect) ---");
        // The "never received update" detail header should appear.
        output.ShouldContain("--- Nodes that never received an update at all ---");
    }

    /// <summary>Verifies that nodes with only good values are counted as never went bad.</summary>
    [Fact]
    public async Task Summary_NodeReceivedOnlyGoodValues_IsCountedAsNeverWentBad()
    {
        var fakeService = new FakeOpcUaClientService();
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var command = new SubscribeCommand(factory)
        {
            Url = "opc.tcp://localhost:4840",
            NodeId = "ns=2;s=GoodNode",
            // Use --duration so the command auto-exits.
            DurationSeconds = 2
        };

        using var console = TestConsoleHelper.CreateConsole();
        var runTask = Task.Run(async () => await command.ExecuteAsync(console));

        // Wait until the subscription is registered by the fake.
        await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);

        fakeService.RaiseDataChanged(
            "ns=2;s=GoodNode",
            new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow));

        await runTask;

        var output = TestConsoleHelper.GetOutput(console);
        output.ShouldContain("NEVER went bad (suspect):       1");
        output.ShouldContain("Last status GOOD:               1");
        output.ShouldContain("No update received at all:      0");
    }

    /// <summary>Verifies that nodes with bad values are counted as ever went bad.</summary>
    [Fact]
    public async Task Summary_NodeReceivedBadValue_IsCountedAsEverWentBad()
    {
        var fakeService = new FakeOpcUaClientService();
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var command = new SubscribeCommand(factory)
        {
            Url = "opc.tcp://localhost:4840",
            NodeId = "ns=2;s=BadNode",
            DurationSeconds = 2
        };

        using var console = TestConsoleHelper.CreateConsole();
        var runTask = Task.Run(async () => await command.ExecuteAsync(console));

        await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);

        fakeService.RaiseDataChanged(
            "ns=2;s=BadNode",
            new DataValue(Variant.Null, StatusCodes.BadDeviceFailure, DateTime.UtcNow, DateTime.UtcNow));

        await runTask;

        var output = TestConsoleHelper.GetOutput(console);
        output.ShouldContain("Ever went BAD during window:    1");
        output.ShouldContain("NEVER went bad (suspect):       0");
    }

    /// <summary>Verifies that subscription auto-exits when duration expires.</summary>
    [Fact]
    public async Task Duration_ZeroOrPositive_AutoExits()
    {
        // --duration > 0 should make the command exit on its own without needing cancellation.
        var fakeService = new FakeOpcUaClientService();
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var command = new SubscribeCommand(factory)
        {
            Url = "opc.tcp://localhost:4840",
            NodeId = "ns=2;s=AutoExit",
            DurationSeconds = 1
        };

        using var console = TestConsoleHelper.CreateConsole();

        var sw = System.Diagnostics.Stopwatch.StartNew();
        await command.ExecuteAsync(console);
        sw.Stop();

        sw.Elapsed.ShouldBeGreaterThanOrEqualTo(TimeSpan.FromMilliseconds(900));
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10));

        var output = TestConsoleHelper.GetOutput(console);
        output.ShouldContain("==================== SUMMARY ====================");
    }

    /// <summary>Verifies that --quiet suppresses updates but prints summary.</summary>
    [Fact]
    public async Task Quiet_SuppressesPerUpdateOutputButPrintsSummary()
    {
        var fakeService = new FakeOpcUaClientService();
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var command = new SubscribeCommand(factory)
        {
            Url = "opc.tcp://localhost:4840",
            NodeId = "ns=2;s=Quiet",
            Quiet = true,
            DurationSeconds = 2
        };

        using var console = TestConsoleHelper.CreateConsole();
        var runTask = Task.Run(async () => await command.ExecuteAsync(console));

        await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);
        fakeService.RaiseDataChanged(
            "ns=2;s=Quiet",
            new DataValue(new Variant(1.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow));

        await runTask;

        var output = TestConsoleHelper.GetOutput(console);
        // No per-update "value = ..." line should appear because --quiet was set.
        output.ShouldNotContain(" = 1 (");
        // Summary section is still printed.
        output.ShouldContain("==================== SUMMARY ====================");
    }

    /// <summary>Verifies that summary is written to disk when summary file is specified.</summary>
    [Fact]
    public async Task SummaryFile_WritesSummaryToDisk()
    {
        var fakeService = new FakeOpcUaClientService();
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var tempFile = Path.Combine(Path.GetTempPath(), $"otopcua-cli-summary-{Guid.NewGuid():N}.txt");
        try
        {
            var command = new SubscribeCommand(factory)
            {
                Url = "opc.tcp://localhost:4840",
                NodeId = "ns=2;s=SummaryFileNode",
                DurationSeconds = 1,
                SummaryFile = tempFile
            };

            using var console = TestConsoleHelper.CreateConsole();
            await command.ExecuteAsync(console);

            File.Exists(tempFile).ShouldBeTrue();
            var contents = await File.ReadAllTextAsync(tempFile);
            contents.ShouldContain("SUMMARY");
            contents.ShouldContain("Total subscribed: 1");
        }
        finally
        {
            if (File.Exists(tempFile)) File.Delete(tempFile);
        }
    }

    /// <summary>Verifies that recursive flag browses subtree and subscribes every variable.</summary>
    [Fact]
    public async Task Recursive_BrowsesSubtreeAndSubscribesEveryVariable()
    {
        // Root has a Folder child and a top-level Variable; the Folder contains a second Variable.
        // Each node id is distinct so the ToDictionary(t => t.nodeId.ToString()) collapse
        // doesn't trip a duplicate-key error.
        var fakeService = new FakeOpcUaClientService();
        fakeService.BrowseResultsByParent["ns=2;s=Root"] = new List<BrowseResult>
        {
            new("ns=2;s=Folder", "Folder", "Object", true),
            new("ns=2;s=Tag1", "Tag1", "Variable", false)
        };
        fakeService.BrowseResultsByParent["ns=2;s=Folder"] = new List<BrowseResult>
        {
            new("ns=2;s=Tag2", "Tag2", "Variable", false)
        };
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var command = new SubscribeCommand(factory)
        {
            Url = "opc.tcp://localhost:4840",
            NodeId = "ns=2;s=Root",
            Recursive = true,
            MaxDepth = 3,
            DurationSeconds = 1
        };

        using var console = TestConsoleHelper.CreateConsole();
        await command.ExecuteAsync(console);

        // Both Variables (depth 1 + depth 2) should have been subscribed.
        fakeService.SubscribeCalls.Count.ShouldBeGreaterThanOrEqualTo(2);
        fakeService.SubscribeCalls.ShouldContain(c => c.NodeId.Identifier.ToString() == "Tag1");
        fakeService.SubscribeCalls.ShouldContain(c => c.NodeId.Identifier.ToString() == "Tag2");

        var output = TestConsoleHelper.GetOutput(console);
        output.ShouldContain("Browsing subtree of ns=2;s=Root");
    }

    /// <summary>Verifies that subscription failures are handled gracefully.</summary>
    [Fact]
    public async Task SubscribeFailure_PrintsFailedMessage_DoesNotCrash()
    {
        var fakeService = new FakeOpcUaClientService
        {
            SubscribeException = new InvalidOperationException("forced failure")
        };
        var factory = new FakeOpcUaClientServiceFactory(fakeService);
        var command = new SubscribeCommand(factory)
        {
            Url = "opc.tcp://localhost:4840",
            NodeId = "ns=2;s=FailNode",
            DurationSeconds = 1
        };

        using var console = TestConsoleHelper.CreateConsole();
        await command.ExecuteAsync(console);

        var output = TestConsoleHelper.GetOutput(console);
        output.ShouldContain("FAILED to subscribe");
        output.ShouldContain("forced failure");
        // The summary block is still printed.
        output.ShouldContain("==================== SUMMARY ====================");
    }

    private static async Task WaitForAsync(Func<bool> predicate, int timeoutMs = 5000)
    {
        var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
        while (!predicate() && DateTime.UtcNow < deadline)
            await Task.Delay(25);
        if (!predicate())
            throw new TimeoutException("Condition not met within timeout.");
    }
}
