using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components;

/// <summary>
/// Covers the M7-B6 (T15/T16) additions to <c>NodeBrowserDialog</c>: the
/// address-space search box that renders <see cref="IBrowseService.SearchAsync"/>
/// matches as a flat selectable list, and that selecting a search result feeds
/// the SAME selection mechanism the tree uses (so the dialog's existing
/// <c>OnSelected</c> callback fires the chosen node id on confirm).
/// </summary>
public class NodeBrowserDialogSearchTests : BunitContext
{
    private readonly IBrowseService _browse = Substitute.For<IBrowseService>();

    public NodeBrowserDialogSearchTests()
    {
        Services.AddSingleton(_browse);
        // The root load fires on ShowAsync; give it an empty (successful) result
        // so the dialog renders without a failure banner and the tree is empty.
        _browse.BrowseChildrenAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Any<string?>(), Arg.Any<CancellationToken>())
            .Returns(new BrowseNodeResult(Array.Empty<BrowseNode>(), Truncated: false, Failure: null));
    }

    private static SearchAddressSpaceResult TwoMatches() => new(
        Matches: new[]
        {
            new AddressSpaceMatch(
                new BrowseNode("ns=2;s=Pump1.Speed", "Speed", BrowseNodeClass.Variable, HasChildren: false, DataType: "Double"),
                Path: "Devices/Pump1/Speed"),
            new AddressSpaceMatch(
                new BrowseNode("ns=2;s=Pump1.Flow", "Flow", BrowseNodeClass.Variable, HasChildren: false, DataType: "Float"),
                Path: "Devices/Pump1/Flow"),
        },
        CapReached: false,
        Failure: null);

    private IRenderedComponent<NodeBrowserDialog> RenderShown(out string? selected)
    {
        string? captured = null;
        var cut = Render<NodeBrowserDialog>(p => p
            .Add(c => c.SiteId, "plant-a")
            .Add(c => c.ConnectionName, "PLC-OPC")
            .Add(c => c.OnSelected, EventCallback.Factory.Create<string>(this, id => captured = id)));

        cut.InvokeAsync(() => cut.Instance.ShowAsync("plant-a", "PLC-OPC", null));
        cut.Render();

        // capture box is read back by the caller after assertions
        _capturedSelection = () => captured;
        selected = captured;
        return cut;
    }

    private Func<string?> _capturedSelection = () => null;

    [Fact]
    public void Search_RendersMatchRows_WithDataTestHooks()
    {
        _browse.SearchAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(TwoMatches());

        var cut = RenderShown(out _);

        var input = cut.Find("[data-test=node-search-input]");
        input.Input("Pump1");
        cut.Find("[data-test=node-search-button]").Click();

        var rows = cut.FindAll("[data-test=node-search-result]");
        Assert.Equal(2, rows.Count);
        Assert.Contains("Speed", cut.Markup);
        Assert.Contains("Devices/Pump1/Speed", cut.Markup);
        Assert.Contains("Double", cut.Markup);
    }

    [Fact]
    public void ClickingSearchResult_RaisesSelectionCallback_WithNodeId()
    {
        _browse.SearchAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(TwoMatches());

        var cut = RenderShown(out _);

        cut.Find("[data-test=node-search-input]").Input("Pump1");
        cut.Find("[data-test=node-search-button]").Click();

        // Click the first result's link — this drives the SAME selection
        // mechanism the tree uses, so the footer Select button confirms and
        // OnSelected fires.
        cut.FindAll("[data-test=node-search-result] button.btn-link")[0].Click();
        cut.Find(".modal-footer .btn-primary").Click();

        Assert.Equal("ns=2;s=Pump1.Speed", _capturedSelection());
    }

    [Fact]
    public void BlankQuery_ClearsResults()
    {
        _browse.SearchAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(TwoMatches());

        var cut = RenderShown(out _);

        cut.Find("[data-test=node-search-input]").Input("Pump1");
        cut.Find("[data-test=node-search-button]").Click();
        Assert.Equal(2, cut.FindAll("[data-test=node-search-result]").Count);

        cut.Find("[data-test=node-search-input]").Input("");
        cut.Find("[data-test=node-search-button]").Click();
        Assert.Empty(cut.FindAll("[data-test=node-search-result]"));
    }

    [Fact]
    public void BlankQueryAfterFailure_ClearsStaleFailureAlert()
    {
        // First search returns a failure so the alert banner appears.
        _browse.SearchAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(new SearchAddressSpaceResult(
                Matches: Array.Empty<AddressSpaceMatch>(),
                CapReached: false,
                Failure: new BrowseFailure(BrowseFailureKind.Timeout, "timed out")));

        var cut = RenderShown(out _);

        cut.Find("[data-test=node-search-input]").Input("Pump1");
        cut.Find("[data-test=node-search-button]").Click();

        // The failure alert must be visible after the failed search.
        Assert.NotEmpty(cut.FindAll(".alert-danger"));

        // User clears the query and searches again (blank).
        cut.Find("[data-test=node-search-input]").Input("");
        cut.Find("[data-test=node-search-button]").Click();

        // Stale failure alert must be gone.
        Assert.Empty(cut.FindAll(".alert-danger"));
    }
}
