using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;

/// <summary>
///     Test fake for <see cref="IAbCipTagRuntime"/>. Stores the mock PLC value in
///     <see cref="Value"/> + returns it from <see cref="DecodeValue"/>. Use
///     <see cref="Status"/> to simulate libplctag error codes,
///     <see cref="ThrowOnInitialize"/> / <see cref="ThrowOnRead"/> to simulate exceptions.
/// </summary>
internal class FakeAbCipTag : IAbCipTagRuntime
{
    public AbCipTagCreateParams CreationParams { get; }
    public object? Value { get; set; }
    public int Status { get; set; }
    public bool ThrowOnInitialize { get; set; }
    public bool ThrowOnRead { get; set; }
    public Exception? Exception { get; set; }
    public int InitializeCount { get; private set; }
    public int ReadCount { get; private set; }
    public int WriteCount { get; private set; }
    public bool Disposed { get; private set; }

    public FakeAbCipTag(AbCipTagCreateParams createParams) => CreationParams = createParams;

    public virtual Task InitializeAsync(CancellationToken cancellationToken)
    {
        InitializeCount++;
        if (ThrowOnInitialize) throw Exception ?? new InvalidOperationException("fake initialize failure");
        return Task.CompletedTask;
    }

    public virtual Task ReadAsync(CancellationToken cancellationToken)
    {
        ReadCount++;
        if (ThrowOnRead) throw Exception ?? new InvalidOperationException("fake read failure");
        return Task.CompletedTask;
    }

    public virtual Task WriteAsync(CancellationToken cancellationToken)
    {
        WriteCount++;
        return Task.CompletedTask;
    }

    public virtual int GetStatus() => Status;

    public virtual object? DecodeValue(AbCipDataType type, int? bitIndex) => Value;

    /// <summary>
    ///     Task #194 whole-UDT read support. Tests drive multi-member decoding by setting
    ///     <see cref="ValuesByOffset"/> — keyed by member byte offset — before invoking
    ///     <see cref="AbCipDriver.ReadAsync"/>. Falls back to <see cref="Value"/> when the
    ///     offset is zero or unmapped so existing tests that never set the offset map keep
    ///     working unchanged.
    /// </summary>
    public Dictionary<int, object?> ValuesByOffset { get; } = new();

    public virtual object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex)
    {
        if (ValuesByOffset.TryGetValue(offset, out var v)) return v;
        return offset == 0 ? Value : null;
    }

    public virtual void EncodeValue(AbCipDataType type, int? bitIndex, object? value) => Value = value;

    public virtual void Dispose() => Disposed = true;
}

/// <summary>Test factory that produces <see cref="FakeAbCipTag"/>s and indexes them for assertion.</summary>
internal sealed class FakeAbCipTagFactory : IAbCipTagFactory
{
    public Dictionary<string, FakeAbCipTag> Tags { get; } = new(StringComparer.OrdinalIgnoreCase);
    public Func<AbCipTagCreateParams, FakeAbCipTag>? Customise { get; set; }

    public IAbCipTagRuntime Create(AbCipTagCreateParams createParams)
    {
        var fake = Customise?.Invoke(createParams) ?? new FakeAbCipTag(createParams);
        Tags[createParams.TagName] = fake;
        return fake;
    }
}
