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

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

internal class FakeAbLegacyTag : IAbLegacyTagRuntime
{
    /// <summary>Gets the tag creation parameters.</summary>
    public AbLegacyTagCreateParams CreationParams { get; }

    /// <summary>Gets or sets the tag value.</summary>
    public object? Value { get; set; }

    /// <summary>
    ///     Gets or sets the typed CLR array returned by <see cref="DecodeArray"/>. Set this to a
    ///     <c>short[]</c> / <c>int[]</c> / <c>float[]</c> / <c>bool[]</c> to simulate a libplctag
    ///     multi-element PCCC file read; the driver boxes it straight through.
    /// </summary>
    public object? ArrayValue { get; set; }

    /// <summary>Gets or sets the tag status code (returned by <see cref="GetStatus"/> for reads).</summary>
    public int Status { get; set; }

    /// <summary>
    ///     Gets or sets an optional status code that overrides <see cref="Status"/> once a write
    ///     has been performed (i.e. after <see cref="WriteCount"/> becomes &gt; 0). Lets tests seed
    ///     a write-rejection status without also failing the preceding read.
    /// </summary>
    public int? WriteStatusOverride { get; set; }

    /// <summary>Gets or sets a value indicating whether to throw on initialization.</summary>
    public bool ThrowOnInitialize { get; set; }

    /// <summary>Gets or sets a value indicating whether to throw on read.</summary>
    public bool ThrowOnRead { get; set; }

    /// <summary>Gets or sets a value indicating whether to throw on write.</summary>
    public bool ThrowOnWrite { get; set; }

    /// <summary>Gets or sets the exception to throw.</summary>
    public Exception? Exception { get; set; }

    /// <summary>Gets the count of initialization calls.</summary>
    public int InitializeCount { get; private set; }

    /// <summary>Gets the count of read calls.</summary>
    public int ReadCount { get; private set; }

    /// <summary>Gets the count of write calls.</summary>
    public int WriteCount { get; private set; }

    /// <summary>Gets a value indicating whether the tag has been disposed.</summary>
    public bool Disposed { get; private set; }

    /// <summary>Initializes a new instance of the FakeAbLegacyTag class.</summary>
    /// <param name="p">The tag creation parameters.</param>
    public FakeAbLegacyTag(AbLegacyTagCreateParams p) => CreationParams = p;

    /// <summary>Initializes the tag asynchronously.</summary>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public virtual Task InitializeAsync(CancellationToken ct)
    {
        InitializeCount++;
        if (ThrowOnInitialize) throw Exception ?? new InvalidOperationException();
        return Task.CompletedTask;
    }

    /// <summary>Reads the tag value asynchronously.</summary>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public virtual Task ReadAsync(CancellationToken ct)
    {
        ReadCount++;
        if (ThrowOnRead) throw Exception ?? new InvalidOperationException();
        return Task.CompletedTask;
    }

    /// <summary>Writes the tag value asynchronously.</summary>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public virtual Task WriteAsync(CancellationToken ct)
    {
        WriteCount++;
        if (ThrowOnWrite) throw Exception ?? new InvalidOperationException();
        return Task.CompletedTask;
    }

    /// <summary>Gets the current tag status.</summary>
    /// <returns>The status code.</returns>
    public virtual int GetStatus() =>
        WriteStatusOverride.HasValue && WriteCount > 0 ? WriteStatusOverride.Value : Status;

    /// <summary>Decodes the tag value based on the specified data type and bit index.</summary>
    /// <param name="type">The AbLegacy data type.</param>
    /// <param name="bitIndex">The bit index if applicable.</param>
    /// <returns>The decoded value.</returns>
    public virtual object? DecodeValue(AbLegacyDataType type, int? bitIndex) => Value;

    /// <summary>Decodes <paramref name="count"/> elements as a typed CLR array.</summary>
    /// <param name="type">The AbLegacy data type.</param>
    /// <param name="count">The element count.</param>
    /// <returns>The pre-set <see cref="ArrayValue"/>.</returns>
    public virtual object? DecodeArray(AbLegacyDataType type, int count) => ArrayValue;

    /// <summary>Encodes the tag value based on the specified data type and bit index.</summary>
    /// <param name="type">The AbLegacy data type.</param>
    /// <param name="bitIndex">The bit index if applicable.</param>
    /// <param name="value">The value to encode.</param>
    public virtual void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value) => Value = value;

    /// <summary>Disposes the tag.</summary>
    public virtual void Dispose() => Disposed = true;
}

/// <summary>Test factory for creating fake AbLegacy tags.</summary>
internal sealed class FakeAbLegacyTagFactory : IAbLegacyTagFactory
{
    /// <summary>Gets the collection of created tags, keyed by tag name.</summary>
    public Dictionary<string, FakeAbLegacyTag> Tags { get; } = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>Gets or sets an optional customization function for tag creation.</summary>
    public Func<AbLegacyTagCreateParams, FakeAbLegacyTag>? Customise { get; set; }

    /// <summary>Creates a new AbLegacy tag with the specified parameters.</summary>
    /// <param name="p">The tag creation parameters.</param>
    /// <returns>The created tag.</returns>
    public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams p)
    {
        var fake = Customise?.Invoke(p) ?? new FakeAbLegacyTag(p);
        Tags[p.TagName] = fake;
        return fake;
    }
}
