using System.Reflection;
using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.DebugView;

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

/// <summary>
/// DV-5: the Debug View renders a tabbed (Attributes / Alarms) layout, each tab
/// hosting a composition <c>TreeView</c>. Computed alarms and attributes nest as
/// leaves under their composition-member branches; native conditions nest under
/// their source-binding node. Branch nodes surface a roll-up (active-count /
/// bad-quality) badge. These tests assert the tree structure + roll-up, not just
/// that markup exists, and that the enriched native-alarm badge set
/// (kind / severity / Unacked / Shelved) still renders inside the tree leaf.
/// </summary>
public class DebugViewAlarmTableTests : BunitContext
{
    private IRenderedComponent<DebugViewPage> RenderPage()
    {
        JSInterop.Mode = JSRuntimeMode.Loose;

        var repo = Substitute.For<ITemplateEngineRepository>();
        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetAllSitesAsync().Returns(new List<Site>());
        Services.AddSingleton(repo);
        Services.AddSingleton(siteRepo);

        var comms = new CommunicationService(
            Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance);
        Services.AddSingleton(comms);

        var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance);
        var debugStream = new DebugStreamService(
            comms, new ServiceCollection().BuildServiceProvider(), grpcFactory,
            NullLogger<DebugStreamService>.Instance);
        Services.AddSingleton(debugStream);

        var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie");
        var stubAuth = new StubAuthStateProvider(new AuthenticationState(new ClaimsPrincipal(identity)));
        Services.AddSingleton<AuthenticationStateProvider>(stubAuth);
        Services.AddScoped(_ => new SiteScopeService(stubAuth));

        return Render<DebugViewPage>();
    }

    private sealed class StubAuthStateProvider : AuthenticationStateProvider
    {
        private readonly AuthenticationState _state;
        public StubAuthStateProvider(AuthenticationState state) => _state = state;
        public override Task<AuthenticationState> GetAuthenticationStateAsync() => Task.FromResult(_state);
    }

    private static void SetField(DebugViewPage c, string name, object? value) =>
        typeof(DebugViewPage).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(c, value);

    private static Dictionary<string, AlarmStateChanged> AlarmStates(DebugViewPage c) =>
        (Dictionary<string, AlarmStateChanged>)typeof(DebugViewPage)
            .GetField("_alarmStates", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(c)!;

    private static Dictionary<string, AttributeValueChanged> AttributeValues(DebugViewPage c) =>
        (Dictionary<string, AttributeValueChanged>)typeof(DebugViewPage)
            .GetField("_attributeValues", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(c)!;

    /// <summary>
    /// Mark the page connected with the given snapshot and seed the latest-per-name
    /// dictionaries, then re-render. Mirrors what <c>Connect()</c> does from a real
    /// snapshot without driving the gRPC stream.
    /// </summary>
    private static void Connect(
        IRenderedComponent<DebugViewPage> cut,
        IReadOnlyList<AttributeValueChanged> attributes,
        IReadOnlyList<AlarmStateChanged> alarms)
    {
        cut.InvokeAsync(() =>
        {
            SetField(cut.Instance, "_connected", true);
            SetField(cut.Instance, "_snapshot",
                new DebugViewSnapshot("inst", attributes.ToList(), alarms.ToList(), DateTimeOffset.UtcNow));
            foreach (var a in attributes) AttributeValues(cut.Instance)[a.AttributeName] = a;
            foreach (var a in alarms) AlarmStates(cut.Instance)[a.AlarmName] = a;
        });
        cut.Render();
    }

    [Fact]
    public void BothTabsPresent_AttributesTabIsDefault_AndShowsAttributeTree()
    {
        var cut = RenderPage();

        var speed = new AttributeValueChanged(
            "inst", "Motor1.Speed", "Motor1.Speed", 1450, "Good", DateTimeOffset.UtcNow);
        var alarm = new AlarmStateChanged("inst", "Motor1.HighTemp", AlarmState.Active, 700, DateTimeOffset.UtcNow);

        Connect(cut, new[] { speed }, new[] { alarm });

        // Both tab hooks render.
        var attrTab = cut.Find("[data-test='debug-tab-attributes']");
        var alarmTab = cut.Find("[data-test='debug-tab-alarms']");
        Assert.Contains("active", attrTab.GetAttribute("class")!);       // Attributes is the default active tab
        Assert.DoesNotContain("active", alarmTab.GetAttribute("class")!);

        // The Attributes pane is visible (not d-none); the Alarms pane is hidden.
        var attrPane = cut.Find("[data-test='debug-pane-attributes']");
        var alarmPane = cut.Find("[data-test='debug-pane-alarms']");
        Assert.DoesNotContain("d-none", attrPane.GetAttribute("class")!);
        Assert.Contains("d-none", alarmPane.GetAttribute("class")!);

        // The attribute tree shows the Motor1 branch and the Speed leaf nested under it.
        var attrItems = attrPane.QuerySelectorAll("li[role='treeitem']");
        Assert.Contains(attrItems, li => li.TextContent.Contains("Motor1"));
        Assert.Contains(attrItems, li => li.TextContent.Contains("Speed") && li.TextContent.Contains("1450"));

        // The leaf for Speed must be nested inside the Motor1 branch's group, not a root.
        var motor1 = attrItems.Single(li =>
            li.QuerySelector(".tv-content")!.TextContent.Trim().StartsWith("Motor1"));
        Assert.NotNull(motor1.QuerySelector("ul[role='group'] li[role='treeitem']"));
    }

    [Fact]
    public void AlarmsTab_GroupsComputedAlarmUnderBranch_WithActiveRollUpBadge()
    {
        var cut = RenderPage();

        // Two computed alarms under the same Motor1 module — one active, one normal.
        var high = new AlarmStateChanged("inst", "Motor1.HighTemp", AlarmState.Active, 700, DateTimeOffset.UtcNow);
        var ok = new AlarmStateChanged("inst", "Motor1.Overload", AlarmState.Normal, 200, DateTimeOffset.UtcNow);

        Connect(cut, Array.Empty<AttributeValueChanged>(), new[] { high, ok });

        // Switch to the Alarms tab.
        cut.Find("[data-test='debug-tab-alarms']").Click();

        var alarmPane = cut.Find("[data-test='debug-pane-alarms']");
        Assert.DoesNotContain("d-none", alarmPane.GetAttribute("class")!);

        var items = alarmPane.QuerySelectorAll("li[role='treeitem']");

        // Motor1 branch carries the roll-up "1 active" danger badge.
        var motor1 = items.Single(li =>
            li.QuerySelector(".tv-content")!.TextContent.Trim().StartsWith("Motor1"));
        var rollup = motor1.QuerySelector(".tv-content .badge.bg-danger");
        Assert.NotNull(rollup);
        Assert.Contains("1 active", rollup!.TextContent);

        // The active alarm leaf nests under Motor1 and shows the Active state badge.
        var group = motor1.QuerySelector("ul[role='group']");
        Assert.NotNull(group);
        var leaves = group!.QuerySelectorAll("li[role='treeitem']");
        Assert.Contains(leaves, li => li.TextContent.Contains("HighTemp"));
        Assert.Contains(leaves, li => li.TextContent.Contains("Overload"));
        var highTemp = leaves.Single(li => li.TextContent.Contains("HighTemp"));
        Assert.NotNull(highTemp.QuerySelector(".badge.bg-danger"));   // Active state badge
    }

    [Fact]
    public void AlarmsTab_RendersNativeCondition_WithEnrichedBadgesUnderBindingNode()
    {
        var cut = RenderPage();

        // A native OPC UA condition belonging to the Motor1.MotorAlarms binding.
        var native = new AlarmStateChanged("inst", "T01.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow)
        {
            Kind = AlarmKind.NativeOpcUa,
            NativeSourceCanonicalName = "Motor1.MotorAlarms",
            SourceReference = "ns=2;s=Tank01.Level.HiHi",
            Condition = new AlarmConditionState(
                Active: true, Acknowledged: false, Confirmed: null,
                Shelve: AlarmShelveState.OneShotShelved, Suppressed: false, Severity: 800)
        };

        Connect(cut, Array.Empty<AttributeValueChanged>(), new[] { native });

        cut.Find("[data-test='debug-tab-alarms']").Click();

        var alarmPane = cut.Find("[data-test='debug-pane-alarms']");
        var items = alarmPane.QuerySelectorAll("li[role='treeitem']");

        // The source-binding node renders with its roll-up active badge.
        var binding = items.Single(li =>
            li.QuerySelector(".tv-content")!.TextContent.Trim().StartsWith("MotorAlarms"));
        var bindingBadge = binding.QuerySelector(".tv-content .badge");
        Assert.NotNull(bindingBadge);
        Assert.Contains("1 active", bindingBadge!.TextContent);

        // The native condition leaf nests under the binding and surfaces the enriched badge set.
        var leaf = binding.QuerySelector("ul[role='group'] li[role='treeitem']");
        Assert.NotNull(leaf);
        var leafMarkup = leaf!.OuterHtml;
        Assert.Contains("sev 800", leafMarkup);         // severity surfaced
        Assert.Contains("Shelved", leafMarkup);        // composite condition badge
        Assert.Contains("OPC UA", leafMarkup);         // native kind badge
        Assert.Contains("Unacked", leafMarkup);        // active + unacknowledged
        Assert.Contains("ns=2;s=Tank01.Level.HiHi", leaf.TextContent); // source reference as the leaf label
    }
}
