using Opc.Ua;
using ZB.MOM.WW.OtOpcUa.Client.Shared;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult;

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

/// <summary>
///     Fake implementation of <see cref="IOpcUaClientService" /> for unit testing commands.
///     Records all method calls and returns configurable results.
/// </summary>
public sealed class FakeOpcUaClientService : IOpcUaClientService
{
    // Track calls
    /// <summary>Gets a value indicating whether ConnectAsync was called.</summary>
    public bool ConnectCalled { get; private set; }

    /// <summary>Gets the connection settings from the most recent ConnectAsync call.</summary>
    public ConnectionSettings? LastConnectionSettings { get; private set; }

    /// <summary>Gets a value indicating whether DisconnectAsync was called.</summary>
    public bool DisconnectCalled { get; private set; }

    /// <summary>Gets a value indicating whether Dispose was called.</summary>
    public bool DisposeCalled { get; private set; }

    /// <summary>Gets the list of node IDs passed to ReadValueAsync calls.</summary>
    public List<NodeId> ReadNodeIds { get; } = [];

    /// <summary>Gets the list of (NodeId, value) pairs passed to WriteValueAsync calls.</summary>
    public List<(NodeId NodeId, object Value)> WriteValues { get; } = [];

    /// <summary>Gets the list of parent node IDs passed to BrowseAsync calls.</summary>
    public List<NodeId?> BrowseNodeIds { get; } = [];

    /// <summary>Gets the list of (NodeId, intervalMs) pairs from SubscribeAsync calls.</summary>
    public List<(NodeId NodeId, int IntervalMs)> SubscribeCalls { get; } = [];

    /// <summary>Gets the list of node IDs passed to UnsubscribeAsync calls.</summary>
    public List<NodeId> UnsubscribeCalls { get; } = [];

    /// <summary>Gets the list of (SourceNodeId, intervalMs) pairs from SubscribeAlarmsAsync calls.</summary>
    public List<(NodeId? SourceNodeId, int IntervalMs)> SubscribeAlarmsCalls { get; } = [];

    /// <summary>Gets a value indicating whether UnsubscribeAlarmsAsync was called.</summary>
    public bool UnsubscribeAlarmsCalled { get; private set; }

    // Readiness signals (arch-review 07/S-4): tests that start a long-running command on a
    // background task previously slept a fixed `await Task.Delay(100)` to let it reach its
    // subscribe call before cancelling — a race that flakes under CI load. These TCS complete
    // the instant the command actually invokes the corresponding subscribe, so tests can await
    // the real signal (with a generous timeout) instead of guessing a delay.
    private readonly TaskCompletionSource _subscribeInvoked = new(TaskCreationOptions.RunContinuationsAsynchronously);
    private readonly TaskCompletionSource _subscribeAlarmsInvoked = new(TaskCreationOptions.RunContinuationsAsynchronously);

    /// <summary>Completes when the command under test first calls <see cref="SubscribeAsync"/>.</summary>
    public Task SubscribeInvoked => _subscribeInvoked.Task;

    /// <summary>Completes when the command under test first calls <see cref="SubscribeAlarmsAsync"/>.</summary>
    public Task SubscribeAlarmsInvoked => _subscribeAlarmsInvoked.Task;

    /// <summary>Gets a value indicating whether RequestConditionRefreshAsync was called.</summary>
    public bool RequestConditionRefreshCalled { get; private set; }

    /// <summary>Gets the list of (NodeId, start, end, maxValues) tuples from HistoryReadRawAsync calls.</summary>
    public List<(NodeId NodeId, DateTime Start, DateTime End, int MaxValues)> HistoryReadRawCalls { get; } = [];

    /// <summary>Gets the list of history read aggregate call parameters.</summary>
    public List<(NodeId NodeId, DateTime Start, DateTime End, AggregateType Aggregate, double IntervalMs)>
        HistoryReadAggregateCalls { get; } =
        [];

    /// <summary>Gets a value indicating whether GetRedundancyInfoAsync was called.</summary>
    public bool GetRedundancyInfoCalled { get; private set; }

    // Configurable results
    /// <summary>Gets or sets the connection info returned by ConnectAsync.</summary>
    public ConnectionInfo ConnectionInfoResult { get; set; } = new(
        "opc.tcp://localhost:4840",
        "TestServer",
        "None",
        "http://opcfoundation.org/UA/SecurityPolicy#None",
        "session-1",
        "TestSession");

    /// <summary>Gets or sets the data value returned by ReadValueAsync.</summary>
    public DataValue ReadValueResult { get; set; } = new(
        new Variant(42),
        StatusCodes.Good,
        DateTime.UtcNow,
        DateTime.UtcNow);

    /// <summary>Gets or sets the status code returned by WriteValueAsync.</summary>
    public StatusCode WriteStatusCodeResult { get; set; } = StatusCodes.Good;

    /// <summary>Gets or sets the browse results returned by BrowseAsync.</summary>
    public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = new List<BrowseResult>
    {
        new("ns=2;s=Node1", "Node1", "Object", true),
        new("ns=2;s=Node2", "Node2", "Variable", false)
    };

    /// <summary>
    /// Optional per-parent-node browse results. When a key matches the requested parent's
    /// <see cref="NodeId.ToString" />, this dictionary takes precedence over <see cref="BrowseResults" />.
    /// Tests exercising recursive walks (Client.CLI-010) use it to model a real subtree whose
    /// child node ids do not collide on descent.
    /// </summary>
    public Dictionary<string, IReadOnlyList<BrowseResult>> BrowseResultsByParent { get; } = new();

    /// <summary>Gets or sets the history read result returned by HistoryReadRawAsync and HistoryReadAggregateAsync.</summary>
    public IReadOnlyList<DataValue> HistoryReadResult { get; set; } = new List<DataValue>
    {
        new(new Variant(10.0), StatusCodes.Good, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow),
        new(new Variant(20.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow)
    };

    /// <summary>Gets or sets the redundancy info returned by GetRedundancyInfoAsync.</summary>
    public RedundancyInfo RedundancyInfoResult { get; set; } = new(
        "Warm", 200, ["urn:server1", "urn:server2"], "urn:app:test");

    /// <summary>Gets or sets the exception thrown by ConnectAsync.</summary>
    public Exception? ConnectException { get; set; }

    /// <summary>Gets or sets the exception thrown by ReadValueAsync.</summary>
    public Exception? ReadException { get; set; }

    /// <summary>Gets or sets the exception thrown by WriteValueAsync.</summary>
    public Exception? WriteException { get; set; }

    /// <summary>Gets or sets the exception thrown by RequestConditionRefreshAsync.</summary>
    public Exception? ConditionRefreshException { get; set; }

    /// <summary>Gets or sets the exception thrown by SubscribeAsync.</summary>
    public Exception? SubscribeException { get; set; }

    /// <inheritdoc />
    public bool IsConnected => ConnectCalled && !DisconnectCalled;

    /// <inheritdoc />
    public ConnectionInfo? CurrentConnectionInfo => ConnectCalled ? ConnectionInfoResult : null;

    /// <inheritdoc />
    public event EventHandler<DataChangedEventArgs>? DataChanged;

    /// <inheritdoc />
    public event EventHandler<AlarmEventArgs>? AlarmEvent;

    /// <inheritdoc />
    public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;

    /// <summary>True when at least one handler is attached to <see cref="DataChanged" />.</summary>
    public bool HasDataChangedSubscribers => DataChanged != null;

    /// <summary>True when at least one handler is attached to <see cref="AlarmEvent" />.</summary>
    public bool HasAlarmEventSubscribers => AlarmEvent != null;

    /// <inheritdoc />
    public Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
    {
        ConnectCalled = true;
        LastConnectionSettings = settings;
        if (ConnectException != null) throw ConnectException;
        return Task.FromResult(ConnectionInfoResult);
    }

    /// <inheritdoc />
    public Task DisconnectAsync(CancellationToken ct = default)
    {
        DisconnectCalled = true;
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default)
    {
        ReadNodeIds.Add(nodeId);
        if (ReadException != null) throw ReadException;
        return Task.FromResult(ReadValueResult);
    }

    /// <inheritdoc />
    public Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default)
    {
        WriteValues.Add((nodeId, value));
        if (WriteException != null) throw WriteException;
        return Task.FromResult(WriteStatusCodeResult);
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default)
    {
        BrowseNodeIds.Add(parentNodeId);
        var key = parentNodeId?.ToString();
        if (key != null && BrowseResultsByParent.TryGetValue(key, out var keyed))
            return Task.FromResult(keyed);
        return Task.FromResult(BrowseResults);
    }

    /// <inheritdoc />
    public Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default)
    {
        SubscribeCalls.Add((nodeId, intervalMs));
        _subscribeInvoked.TrySetResult();
        if (SubscribeException != null) throw SubscribeException;
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default)
    {
        UnsubscribeCalls.Add(nodeId);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default)
    {
        SubscribeAlarmsCalls.Add((sourceNodeId, intervalMs));
        _subscribeAlarmsInvoked.TrySetResult();
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task UnsubscribeAlarmsAsync(CancellationToken ct = default)
    {
        UnsubscribeAlarmsCalled = true;
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task RequestConditionRefreshAsync(CancellationToken ct = default)
    {
        RequestConditionRefreshCalled = true;
        if (ConditionRefreshException != null) throw ConditionRefreshException;
        return Task.CompletedTask;
    }

    /// <summary>Gets the list of (conditionNodeId, eventId, comment) tuples from AcknowledgeAlarmAsync calls.</summary>
    public List<(string ConditionNodeId, byte[] EventId, string Comment)> AcknowledgeAlarmCalls { get; } = [];

    /// <summary>Gets or sets the status code returned by AcknowledgeAlarmAsync.</summary>
    public StatusCode AcknowledgeAlarmResult { get; set; } = StatusCodes.Good;

    /// <inheritdoc />
    public Task<StatusCode> AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
        CancellationToken ct = default)
    {
        AcknowledgeAlarmCalls.Add((conditionNodeId, eventId, comment));
        return Task.FromResult(AcknowledgeAlarmResult);
    }

    /// <summary>Gets the list of (conditionNodeId, eventId, comment) tuples from ConfirmAlarmAsync calls.</summary>
    public List<(string ConditionNodeId, byte[] EventId, string Comment)> ConfirmAlarmCalls { get; } = [];

    /// <summary>Gets or sets the status code returned by ConfirmAlarmAsync.</summary>
    public StatusCode ConfirmAlarmResult { get; set; } = StatusCodes.Good;

    /// <inheritdoc />
    public Task<StatusCode> ConfirmAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
        CancellationToken ct = default)
    {
        ConfirmAlarmCalls.Add((conditionNodeId, eventId, comment));
        return Task.FromResult(ConfirmAlarmResult);
    }

    /// <summary>Gets the list of (conditionNodeId, kind, durationSeconds) tuples from ShelveAlarmAsync calls.</summary>
    public List<(string ConditionNodeId, ShelveKind Kind, double DurationSeconds)> ShelveAlarmCalls { get; } = [];

    /// <summary>Gets or sets the status code returned by ShelveAlarmAsync.</summary>
    public StatusCode ShelveAlarmResult { get; set; } = StatusCodes.Good;

    /// <inheritdoc />
    public Task<StatusCode> ShelveAlarmAsync(string conditionNodeId, ShelveKind kind, double shelvingTimeSeconds = 0,
        CancellationToken ct = default)
    {
        ShelveAlarmCalls.Add((conditionNodeId, kind, shelvingTimeSeconds));
        return Task.FromResult(ShelveAlarmResult);
    }

    /// <summary>Gets the list of condition node IDs passed to EnableAsync calls.</summary>
    public List<string> EnableCalls { get; } = [];

    /// <summary>Gets or sets the status code returned by EnableAsync.</summary>
    public StatusCode EnableResult { get; set; } = StatusCodes.Good;

    /// <inheritdoc />
    public Task<StatusCode> EnableAsync(string conditionNodeId, CancellationToken ct = default)
    {
        EnableCalls.Add(conditionNodeId);
        return Task.FromResult(EnableResult);
    }

    /// <summary>Gets the list of condition node IDs passed to DisableAsync calls.</summary>
    public List<string> DisableCalls { get; } = [];

    /// <summary>Gets or sets the status code returned by DisableAsync.</summary>
    public StatusCode DisableResult { get; set; } = StatusCodes.Good;

    /// <inheritdoc />
    public Task<StatusCode> DisableAsync(string conditionNodeId, CancellationToken ct = default)
    {
        DisableCalls.Add(conditionNodeId);
        return Task.FromResult(DisableResult);
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(
        NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default)
    {
        HistoryReadRawCalls.Add((nodeId, startTime, endTime, maxValues));
        return Task.FromResult(HistoryReadResult);
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
        NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate,
        double intervalMs = 3600000, CancellationToken ct = default)
    {
        HistoryReadAggregateCalls.Add((nodeId, startTime, endTime, aggregate, intervalMs));
        return Task.FromResult(HistoryReadResult);
    }

    /// <inheritdoc />
    public Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default)
    {
        GetRedundancyInfoCalled = true;
        return Task.FromResult(RedundancyInfoResult);
    }

    /// <summary>
    /// Marks the fake client as disposed so CLI command tests can assert cleanup behavior.
    /// </summary>
    public void Dispose()
    {
        DisposeCalled = true;
    }

    /// <summary>Raises the DataChanged event for testing subscribe commands.</summary>
    /// <param name="nodeId">The node ID string that changed.</param>
    /// <param name="value">The new data value.</param>
    public void RaiseDataChanged(string nodeId, DataValue value)
    {
        DataChanged?.Invoke(this, new DataChangedEventArgs(nodeId, value));
    }

    /// <summary>Raises the AlarmEvent for testing alarm commands.</summary>
    /// <param name="args">The alarm event arguments.</param>
    public void RaiseAlarmEvent(AlarmEventArgs args)
    {
        AlarmEvent?.Invoke(this, args);
    }

    /// <summary>Raises the ConnectionStateChanged event for testing.</summary>
    /// <param name="oldState">The previous connection state.</param>
    /// <param name="newState">The new connection state.</param>
    /// <param name="endpointUrl">The endpoint URL.</param>
    public void RaiseConnectionStateChanged(ConnectionState oldState, ConnectionState newState, string endpointUrl)
    {
        ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(oldState, newState, endpointUrl));
    }
}
