using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;
using ZB.MOM.WW.OtOpcUa.Client.UI.Tests.Fakes;
using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult;
using ConnectionState = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.ConnectionState;

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

/// <summary>
/// Verifies the main UI shell behavior for connection state, settings persistence, browsing, subscriptions, and history navigation.
/// </summary>
public class MainWindowViewModelTests
{
    private readonly FakeOpcUaClientService _service;
    private readonly FakeSettingsService _settingsService;
    private readonly MainWindowViewModel _vm;

    /// <summary>Initializes test fixtures with default client and settings services.</summary>
    public MainWindowViewModelTests()
    {
        _service = new FakeOpcUaClientService
        {
            ConnectResult = new ConnectionInfo(
                "opc.tcp://localhost:4840",
                "TestServer",
                "None",
                "http://opcfoundation.org/UA/SecurityPolicy#None",
                "session-1",
                "TestSession"),
            BrowseResults =
            [
                new BrowseResult("ns=2;s=Root", "Root", "Object", true)
            ],
            RedundancyResult = new RedundancyInfo("None", 200, ["urn:test"], "urn:test")
        };

        _settingsService = new FakeSettingsService();
        var factory = new FakeOpcUaClientServiceFactory(_service);
        var dispatcher = new SynchronousUiDispatcher();
        _vm = new MainWindowViewModel(factory, dispatcher, _settingsService);
    }

    /// <summary>
    /// Verifies that the shell starts disconnected with the default endpoint and status text.
    /// </summary>
    [Fact]
    public void DefaultState_IsDisconnected()
    {
        _vm.ConnectionState.ShouldBe(ConnectionState.Disconnected);
        _vm.IsConnected.ShouldBeFalse();
        _vm.EndpointUrl.ShouldBe("opc.tcp://localhost:4840");
        _vm.StatusMessage.ShouldBe("Disconnected");
    }

    /// <summary>
    /// Verifies that the connect command is available before a session is established.
    /// </summary>
    [Fact]
    public void ConnectCommand_CanExecute_WhenDisconnected()
    {
        _vm.ConnectCommand.CanExecute(null).ShouldBeTrue();
    }

    /// <summary>
    /// Verifies that disconnect is disabled until a server session is active.
    /// </summary>
    [Fact]
    public void DisconnectCommand_CannotExecute_WhenDisconnected()
    {
        _vm.DisconnectCommand.CanExecute(null).ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that a successful connect command updates the shell into the connected state.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_TransitionsToConnected()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        _vm.ConnectionState.ShouldBe(ConnectionState.Connected);
        _vm.IsConnected.ShouldBeTrue();
        _service.ConnectCallCount.ShouldBe(1);
    }

    /// <summary>
    /// Verifies that the initial browse tree is loaded after a successful connect.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_LoadsRootNodes()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        _vm.BrowseTree.RootNodes.Count.ShouldBe(1);
        _vm.BrowseTree.RootNodes[0].DisplayName.ShouldBe("Root");
    }

    /// <summary>
    /// Verifies that redundancy details are fetched and exposed after connecting.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_FetchesRedundancyInfo()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        _vm.RedundancyInfo.ShouldNotBeNull();
        _vm.RedundancyInfo!.Mode.ShouldBe("None");
        _vm.RedundancyInfo.ServiceLevel.ShouldBe((byte)200);
    }

    /// <summary>
    /// Verifies that the session label shows the connected server and session identity.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_SetsSessionLabel()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        _vm.SessionLabel.ShouldContain("TestServer");
        _vm.SessionLabel.ShouldContain("TestSession");
    }

    /// <summary>
    /// Verifies that disconnect returns the shell to the disconnected state.
    /// </summary>
    [Fact]
    public async Task DisconnectCommand_TransitionsToDisconnected()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);
        await _vm.DisconnectCommand.ExecuteAsync(null);

        _vm.ConnectionState.ShouldBe(ConnectionState.Disconnected);
        _vm.IsConnected.ShouldBeFalse();
        _service.DisconnectCallCount.ShouldBe(1);
    }

    /// <summary>
    /// Verifies that disconnect clears session-specific UI state such as browse data and redundancy details.
    /// </summary>
    [Fact]
    public async Task Disconnect_ClearsStateAndChildren()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);
        await _vm.DisconnectCommand.ExecuteAsync(null);

        _vm.SessionLabel.ShouldBe(string.Empty);
        _vm.RedundancyInfo.ShouldBeNull();
        _vm.BrowseTree.RootNodes.ShouldBeEmpty();
        _vm.SubscriptionCount.ShouldBe(0);
    }

    /// <summary>
    /// Verifies that connection-state events from the client update the shell status text and state.
    /// </summary>
    [Fact]
    public async Task ConnectionStateChangedEvent_UpdatesState()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        _service.RaiseConnectionStateChanged(
            new ConnectionStateChangedEventArgs(ConnectionState.Connected, ConnectionState.Reconnecting,
                "opc.tcp://localhost:4840"));

        _vm.ConnectionState.ShouldBe(ConnectionState.Reconnecting);
        _vm.StatusMessage.ShouldBe("Reconnecting...");
    }

    /// <summary>
    /// Verifies that selecting a tree node updates the dependent read/write and history panels.
    /// </summary>
    [Fact]
    public async Task SelectedTreeNode_PropagatesToChildViewModels()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        var node = _vm.BrowseTree.RootNodes[0];
        _vm.SelectedTreeNode = node;

        _vm.ReadWrite.SelectedNodeId.ShouldBe(node.NodeId);
        _vm.History.SelectedNodeId.ShouldBe(node.NodeId);
    }

    /// <summary>
    /// Verifies that a successful connect propagates connected state into the child tabs.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_PropagatesIsConnectedToChildViewModels()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        _vm.ReadWrite.IsConnected.ShouldBeTrue();
        _vm.Subscriptions.IsConnected.ShouldBeTrue();
        _vm.Alarms.IsConnected.ShouldBeTrue();
        _vm.History.IsConnected.ShouldBeTrue();
    }

    /// <summary>
    /// Verifies that disconnect propagates disconnected state into the child tabs.
    /// </summary>
    [Fact]
    public async Task DisconnectCommand_PropagatesIsConnectedFalseToChildViewModels()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);
        await _vm.DisconnectCommand.ExecuteAsync(null);

        _vm.ReadWrite.IsConnected.ShouldBeFalse();
        _vm.Subscriptions.IsConnected.ShouldBeFalse();
        _vm.Alarms.IsConnected.ShouldBeFalse();
        _vm.History.IsConnected.ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that failed connection attempts restore the disconnected shell state and surface the error text.
    /// </summary>
    [Fact]
    public async Task ConnectFailure_RevertsToDisconnected()
    {
        _service.ConnectException = new Exception("Connection refused");

        await _vm.ConnectCommand.ExecuteAsync(null);

        _vm.ConnectionState.ShouldBe(ConnectionState.Disconnected);
        _vm.StatusMessage.ShouldContain("Connection refused");
    }

    /// <summary>
    /// Verifies that connection-state transitions raise property-changed notifications for UI binding updates.
    /// </summary>
    [Fact]
    public async Task PropertyChanged_FiredForConnectionState()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        var changed = new List<string>();
        _vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!);

        _service.RaiseConnectionStateChanged(
            new ConnectionStateChangedEventArgs(ConnectionState.Connected, ConnectionState.Reconnecting,
                "opc.tcp://localhost:4840"));

        changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
    }

    /// <summary>
    /// Verifies that the shell initializes advanced connection settings with the expected defaults.
    /// </summary>
    [Fact]
    public void DefaultState_HasCorrectAdvancedSettings()
    {
        _vm.FailoverUrls.ShouldBeNull();
        _vm.SessionTimeoutSeconds.ShouldBe(60);
        _vm.AutoAcceptCertificates.ShouldBeTrue();
        _vm.CertificateStorePath.ShouldContain("OtOpcUaClient");
        _vm.CertificateStorePath.ShouldContain("pki");
    }

    /// <summary>
    /// Verifies that failover endpoint text is parsed into connection settings on connect.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_MapsFailoverUrlsToSettings()
    {
        _vm.FailoverUrls = "opc.tcp://backup1:4840, opc.tcp://backup2:4840";
        await _vm.ConnectCommand.ExecuteAsync(null);

        _service.LastConnectionSettings.ShouldNotBeNull();
        _service.LastConnectionSettings!.FailoverUrls.ShouldNotBeNull();
        _service.LastConnectionSettings.FailoverUrls!.Length.ShouldBe(2);
        _service.LastConnectionSettings.FailoverUrls[0].ShouldBe("opc.tcp://backup1:4840");
        _service.LastConnectionSettings.FailoverUrls[1].ShouldBe("opc.tcp://backup2:4840");
    }

    /// <summary>
    /// Verifies that empty failover text is normalized to no configured failover endpoints.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull()
    {
        _vm.FailoverUrls = "";
        await _vm.ConnectCommand.ExecuteAsync(null);

        _service.LastConnectionSettings.ShouldNotBeNull();
        _service.LastConnectionSettings!.FailoverUrls.ShouldBeNull();
    }

    /// <summary>
    /// Verifies that the configured session timeout is passed into the connection settings.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_MapsSessionTimeoutToSettings()
    {
        _vm.SessionTimeoutSeconds = 120;
        await _vm.ConnectCommand.ExecuteAsync(null);

        _service.LastConnectionSettings.ShouldNotBeNull();
        _service.LastConnectionSettings!.SessionTimeoutSeconds.ShouldBe(120);
    }

    /// <summary>
    /// Verifies that the auto-accept certificate toggle is passed into the connection settings.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings()
    {
        _vm.AutoAcceptCertificates = false;
        await _vm.ConnectCommand.ExecuteAsync(null);

        _service.LastConnectionSettings.ShouldNotBeNull();
        _service.LastConnectionSettings!.AutoAcceptCertificates.ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that a custom certificate store path is passed into the connection settings.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_MapsCertificateStorePathToSettings()
    {
        _vm.CertificateStorePath = "/custom/pki/path";
        await _vm.ConnectCommand.ExecuteAsync(null);

        _service.LastConnectionSettings.ShouldNotBeNull();
        _service.LastConnectionSettings!.CertificateStorePath.ShouldBe("/custom/pki/path");
    }

    /// <summary>
    /// Verifies that subscribing selected nodes adds subscriptions and switches the shell to the subscriptions tab.
    /// </summary>
    [Fact]
    public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        // Use a Variable node so recursive subscribe subscribes it directly
        var varNode = new TreeNodeViewModel(
            "ns=2;s=TestVar", "TestVar", "Variable", false, _service, new SynchronousUiDispatcher());
        _vm.SelectedTreeNodes.Add(varNode);

        await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);

        _vm.Subscriptions!.ActiveSubscriptions.Count.ShouldBe(1);
        _vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=TestVar");
        _vm.SelectedTabIndex.ShouldBe(1);
    }

    /// <summary>
    /// Verifies that subscribing selected nodes is a no-op when nothing is selected.
    /// </summary>
    [Fact]
    public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);

        _vm.Subscriptions.ActiveSubscriptions.ShouldBeEmpty();
    }

    /// <summary>
    /// Verifies that the history command targets the selected node and switches the shell to the history tab.
    /// </summary>
    [Fact]
    public async Task ViewHistoryForSelectedNodeCommand_SetsNodeAndSwitchesToTab()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        var node = _vm.BrowseTree.RootNodes[0];
        _vm.SelectedTreeNodes.Add(node);

        _vm.ViewHistoryForSelectedNodeCommand.Execute(null);

        _vm.History.SelectedNodeId.ShouldBe(node.NodeId);
        _vm.SelectedTabIndex.ShouldBe(3);
    }

    /// <summary>
    /// Verifies that history actions are enabled when a variable node is selected.
    /// </summary>
    [Fact]
    public async Task UpdateHistoryEnabledForSelection_TrueForVariableNode()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        var variableNode = new TreeNodeViewModel(
            "ns=2;s=Var1", "Var1", "Variable", false, _service, new SynchronousUiDispatcher());
        _vm.SelectedTreeNodes.Add(variableNode);

        _vm.UpdateHistoryEnabledForSelection();

        _vm.IsHistoryEnabledForSelection.ShouldBeTrue();
    }

    /// <summary>
    /// Verifies that history actions stay disabled when an object node rather than a variable is selected.
    /// </summary>
    [Fact]
    public async Task UpdateHistoryEnabledForSelection_FalseForObjectNode()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        var objectNode = new TreeNodeViewModel(
            "ns=2;s=Obj1", "Obj1", "Object", true, _service, new SynchronousUiDispatcher());
        _vm.SelectedTreeNodes.Add(objectNode);

        _vm.UpdateHistoryEnabledForSelection();

        _vm.IsHistoryEnabledForSelection.ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that history actions stay disabled when no server connection is active.
    /// </summary>
    [Fact]
    public void UpdateHistoryEnabledForSelection_FalseWhenDisconnected()
    {
        _vm.UpdateHistoryEnabledForSelection();

        _vm.IsHistoryEnabledForSelection.ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that saved user settings are loaded during shell construction.
    /// </summary>
    [Fact]
    public void Constructor_LoadsSettingsFromService()
    {
        _settingsService.LoadCallCount.ShouldBe(1);
    }

    /// <summary>
    /// Verifies that persisted connection and security settings are applied to the shell on startup.
    /// </summary>
    [Fact]
    public void Constructor_AppliesSavedSettings()
    {
        var saved = new UserSettings
        {
            EndpointUrl = "opc.tcp://saved:5555",
            Username = "savedUser",
            SecurityMode = SecurityMode.Sign,
            FailoverUrls = "opc.tcp://backup:5555",
            SessionTimeoutSeconds = 120,
            AutoAcceptCertificates = false,
            CertificateStorePath = "/custom/path"
        };

        var settingsService = new FakeSettingsService { Settings = saved };
        var service = new FakeOpcUaClientService
        {
            ConnectResult = _service.ConnectResult,
            BrowseResults = _service.BrowseResults,
            RedundancyResult = _service.RedundancyResult
        };
        var factory = new FakeOpcUaClientServiceFactory(service);
        var vm = new MainWindowViewModel(factory, new SynchronousUiDispatcher(), settingsService);

        vm.EndpointUrl.ShouldBe("opc.tcp://saved:5555");
        vm.Username.ShouldBe("savedUser");
        // Password is intentionally not persisted: re-prompt each launch
        vm.Password.ShouldBeNull();
        vm.SelectedSecurityMode.ShouldBe(SecurityMode.Sign);
        vm.FailoverUrls.ShouldBe("opc.tcp://backup:5555");
        vm.SessionTimeoutSeconds.ShouldBe(120);
        vm.AutoAcceptCertificates.ShouldBeFalse();
        vm.CertificateStorePath.ShouldBe("/custom/path");
    }

    /// <summary>
    /// Verifies that successful connections persist the current connection settings.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_SavesSettingsOnSuccess()
    {
        _vm.EndpointUrl = "opc.tcp://myserver:4840";
        _vm.Username = "admin";

        await _vm.ConnectCommand.ExecuteAsync(null);

        _settingsService.SaveCallCount.ShouldBe(1);
        _settingsService.LastSaved.ShouldNotBeNull();
        _settingsService.LastSaved!.EndpointUrl.ShouldBe("opc.tcp://myserver:4840");
        _settingsService.LastSaved.Username.ShouldBe("admin");
    }

    /// <summary>
    /// Verifies that failed connection attempts do not overwrite saved settings.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_DoesNotSaveOnFailure()
    {
        _service.ConnectException = new Exception("Connection refused");

        await _vm.ConnectCommand.ExecuteAsync(null);

        _settingsService.SaveCallCount.ShouldBe(0);
    }

    /// <summary>
    /// Verifies that active subscriptions are persisted when the shell disconnects.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_SavesSubscribedNodes()
    {
        await _vm.ConnectCommand.ExecuteAsync(null);

        // Add a subscription
        _vm.Subscriptions.IsConnected = true;
        await _vm.Subscriptions.AddSubscriptionForNodeAsync("ns=2;s=TestSub");

        // Disconnect saves settings including subscriptions
        await _vm.DisconnectCommand.ExecuteAsync(null);

        _settingsService.LastSaved.ShouldNotBeNull();
        _settingsService.LastSaved!.SubscribedNodes.ShouldContain("ns=2;s=TestSub");
    }

    /// <summary>
    /// Regression test for Client.UI-006 — when GetRedundancyInfoAsync throws (the server
    /// does not implement the redundancy facet) the connection must still succeed and the
    /// view model must leave RedundancyInfo null without crashing or hiding the diagnostic.
    /// The Status text is expected to remain "Connected" (redundancy is optional).
    /// </summary>
    [Fact]
    public async Task ConnectCommand_RedundancyFailure_DoesNotBreakConnection()
    {
        _service.RedundancyException = new Exception("BadServiceUnsupported");

        await _vm.ConnectCommand.ExecuteAsync(null);

        _vm.ConnectionState.ShouldBe(ConnectionState.Connected);
        _vm.RedundancyInfo.ShouldBeNull();
        // Connection succeeded; status reflects connection, not the optional redundancy probe failure
        _vm.StatusMessage.ShouldContain("Connected");
    }

    /// <summary>
    /// Verifies that saved subscriptions are restored after reconnecting the shell.
    /// </summary>
    [Fact]
    public async Task ConnectCommand_RestoresSavedSubscriptions()
    {
        _settingsService.Settings.SubscribedNodes = ["ns=2;s=Restored1", "ns=2;s=Restored2"];

        var service = new FakeOpcUaClientService
        {
            ConnectResult = _service.ConnectResult,
            BrowseResults = _service.BrowseResults,
            RedundancyResult = _service.RedundancyResult
        };
        var factory = new FakeOpcUaClientServiceFactory(service);
        var vm = new MainWindowViewModel(factory, new SynchronousUiDispatcher(), _settingsService);

        await vm.ConnectCommand.ExecuteAsync(null);

        vm.Subscriptions.ActiveSubscriptions.Count.ShouldBe(2);
        vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=Restored1");
        vm.Subscriptions.ActiveSubscriptions[1].NodeId.ShouldBe("ns=2;s=Restored2");
        vm.SubscriptionCount.ShouldBe(2);
    }
}
