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
{
    /// <summary>Gets the tag creation parameters.</summary>
    public AbCipTagCreateParams CreationParams { get; }
    /// <summary>Gets or sets the mock tag value.</summary>
    public object? Value { get; set; }
    /// <summary>Gets or sets the simulated libplctag status code.</summary>
    public int Status { get; set; }
    /// <summary>Gets or sets a value indicating whether to throw on <see cref="InitializeAsync"/>.</summary>
    public bool ThrowOnInitialize { get; set; }
    /// <summary>Gets or sets a value indicating whether to throw on <see cref="ReadAsync"/>.</summary>
    public bool ThrowOnRead { get; set; }
    /// <summary>Gets or sets the exception to throw when simulation flags are set.</summary>
    public Exception? Exception { get; set; }
    /// <summary>Gets the count of <see cref="InitializeAsync"/> invocations.</summary>
    public int InitializeCount { get; private set; }
    /// <summary>Gets the count of <see cref="ReadAsync"/> invocations.</summary>
    public int ReadCount { get; private set; }
    /// <summary>Gets the count of <see cref="WriteAsync"/> invocations.</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 <see cref="FakeAbCipTag"/> class.</summary>
    /// <param name="createParams">The tag creation parameters.</param>
    public FakeAbCipTag(AbCipTagCreateParams createParams) => CreationParams = createParams;

    /// <summary>Increments the initialize count and simulates initialization.</summary>
    /// <param name="cancellationToken">The cancellation token for the operation.</param>
    public virtual Task InitializeAsync(CancellationToken cancellationToken)
    {
        InitializeCount++;
        if (ThrowOnInitialize) throw Exception ?? new InvalidOperationException("fake initialize failure");
        return Task.CompletedTask;
    }

    /// <summary>Increments the read count and simulates a read operation.</summary>
    /// <param name="cancellationToken">The cancellation token for the operation.</param>
    public virtual Task ReadAsync(CancellationToken cancellationToken)
    {
        ReadCount++;
        if (ThrowOnRead) throw Exception ?? new InvalidOperationException("fake read failure");
        return Task.CompletedTask;
    }

    /// <summary>Increments the write count and simulates a write operation.</summary>
    /// <param name="cancellationToken">The cancellation token for the operation.</param>
    public virtual Task WriteAsync(CancellationToken cancellationToken)
    {
        WriteCount++;
        return Task.CompletedTask;
    }

    /// <summary>Returns the simulated status code.</summary>
    public virtual int GetStatus() => Status;

    /// <summary>Returns the mock tag value.</summary>
    /// <param name="type">The data type being decoded.</param>
    /// <param name="bitIndex">The optional bit index for bit operations.</param>
    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();

    /// <summary>Returns the mock value at the specified offset.</summary>
    /// <param name="type">The data type being decoded.</param>
    /// <param name="offset">The byte offset into the tag storage.</param>
    /// <param name="bitIndex">The optional bit index for bit operations.</param>
    public virtual object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex)
    {
        if (ValuesByOffset.TryGetValue(offset, out var v)) return v;
        return offset == 0 ? Value : null;
    }

    /// <summary>
    ///     Phase 4c array-read seam. Returns <see cref="ArrayValue"/> (the boxed element-typed
    ///     CLR array a test seeds) regardless of <paramref name="type"/> / <paramref name="count"/>,
    ///     so a test asserts that the driver routes a 1-D array tag through this method instead of
    ///     the scalar <see cref="DecodeValue"/>. <see cref="ArrayFakeAbCipTag"/> is the convenient
    ///     constructor-seeded variant.
    /// </summary>
    /// <param name="type">The element data type being decoded.</param>
    /// <param name="count">The number of elements to decode.</param>
    public virtual object? DecodeArray(AbCipDataType type, int count) => ArrayValue;

    /// <summary>Gets or sets the boxed array value returned from <see cref="DecodeArray"/>.</summary>
    public object? ArrayValue { get; set; }

    /// <summary>Encodes a value into the mock tag storage.</summary>
    /// <param name="type">The data type being encoded.</param>
    /// <param name="bitIndex">The optional bit index for bit operations.</param>
    /// <param name="value">The value to encode.</param>
    public virtual void EncodeValue(AbCipDataType type, int? bitIndex, object? value) => Value = value;

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

/// <summary>
///     A <see cref="FakeAbCipTag"/> pre-seeded with a boxed element-typed CLR array, returned
///     from <see cref="DecodeArray"/>. Mirrors a real libplctag array read where
///     <c>elem_count</c> elements come back in one transaction.
/// </summary>
internal sealed class ArrayFakeAbCipTag : FakeAbCipTag
{
    /// <summary>Initializes the fake with a boxed array value.</summary>
    /// <param name="createParams">The tag creation parameters.</param>
    /// <param name="array">The boxed element-typed CLR array the read returns.</param>
    public ArrayFakeAbCipTag(AbCipTagCreateParams createParams, object array) : base(createParams)
        => ArrayValue = array;
}

/// <summary>Test factory that produces <see cref="FakeAbCipTag"/>s and indexes them for assertion.</summary>
internal sealed class FakeAbCipTagFactory : IAbCipTagFactory
{
    /// <summary>Gets a dictionary of created tags indexed by tag name for assertion.</summary>
    public Dictionary<string, FakeAbCipTag> Tags { get; } = new(StringComparer.OrdinalIgnoreCase);
    /// <summary>Gets or sets an optional customization function to override the tag creation.</summary>
    public Func<AbCipTagCreateParams, FakeAbCipTag>? Customise { get; set; }

    /// <summary>Creates a new fake tag and indexes it by name.</summary>
    /// <param name="createParams">The tag creation parameters.</param>
    public IAbCipTagRuntime Create(AbCipTagCreateParams createParams)
    {
        var fake = Customise?.Invoke(createParams) ?? new FakeAbCipTag(createParams);
        Tags[createParams.TagName] = fake;
        return fake;
    }
}
