using System.Buffers.Binary;
using System.Reflection;
using System.Text;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

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

[Trait("Category", "Unit")]
public sealed class AbCipFetchUdtShapeTests
{
    /// <summary>Test implementation of IAbCipTemplateReader.</summary>
    private sealed class FakeTemplateReader : IAbCipTemplateReader
    {
        /// <summary>Gets or sets the response bytes to return.</summary>
        public byte[] Response { get; set; } = [];

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

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

        /// <summary>Gets the last template ID read.</summary>
        public uint LastTemplateId { get; private set; }

        /// <summary>Reads the template data for the specified device and template ID.</summary>
        /// <param name="deviceParams">The device parameters.</param>
        /// <param name="templateInstanceId">The template instance ID.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>A task that returns the template response bytes.</returns>
        public Task<byte[]> ReadAsync(AbCipTagCreateParams deviceParams, uint templateInstanceId, CancellationToken ct)
        {
            ReadCount++;
            LastTemplateId = templateInstanceId;
            return Task.FromResult(Response);
        }

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

    /// <summary>Test factory for creating fake template readers.</summary>
    private sealed class FakeTemplateReaderFactory : IAbCipTemplateReaderFactory
    {
        /// <summary>Gets the list of created readers.</summary>
        public List<IAbCipTemplateReader> Readers { get; } = new();

        /// <summary>Gets or sets an optional customization function for reader creation.</summary>
        public Func<IAbCipTemplateReader>? Customise { get; set; }

        /// <summary>Creates a new template reader.</summary>
        /// <returns>The created reader.</returns>
        public IAbCipTemplateReader Create()
        {
            var r = Customise?.Invoke() ?? new FakeTemplateReader();
            Readers.Add(r);
            return r;
        }
    }

    private static byte[] BuildSimpleTemplate(string name, uint instanceSize, params (string n, ushort info, ushort arr, uint off)[] members)
    {
        var headerSize = 12;
        var blockSize = 8;
        var strings = new MemoryStream();
        void Add(string s) { var b = Encoding.ASCII.GetBytes(s + ";\0"); strings.Write(b, 0, b.Length); }
        Add(name);
        foreach (var m in members) Add(m.n);
        var stringsArr = strings.ToArray();

        var buf = new byte[headerSize + blockSize * members.Length + stringsArr.Length];
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), (ushort)members.Length);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(2), 0x1234);
        BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), instanceSize);
        for (var i = 0; i < members.Length; i++)
        {
            var o = headerSize + i * blockSize;
            BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), members[i].info);
            BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o + 2), members[i].arr);
            BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), members[i].off);
        }
        Buffer.BlockCopy(stringsArr, 0, buf, headerSize + blockSize * members.Length, stringsArr.Length);
        return buf;
    }

    private static Task<AbCipUdtShape?> InvokeFetch(AbCipDriver drv, string deviceHostAddress, uint templateId)
    {
        var mi = typeof(AbCipDriver).GetMethod("FetchUdtShapeAsync",
            BindingFlags.NonPublic | BindingFlags.Instance)!;
        return (Task<AbCipUdtShape?>)mi.Invoke(drv, [deviceHostAddress, templateId, CancellationToken.None])!;
    }

    /// <summary>Verifies that FetchUdtShapeAsync decodes a blob and caches the result.</summary>
    [Fact]
    public async Task FetchUdtShapeAsync_decodes_blob_and_caches_result()
    {
        var factory = new FakeTemplateReaderFactory
        {
            Customise = () => new FakeTemplateReader
            {
                Response = BuildSimpleTemplate("MotorUdt", 8,
                    ("Speed", 0xC4, 0, 0),
                    ("Enabled", 0xC1, 0, 4)),
            },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        }, "drv-1", templateReaderFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var shape = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 42);

        shape.ShouldNotBeNull();
        shape.TypeName.ShouldBe("MotorUdt");
        shape.Members.Count.ShouldBe(2);

        // Second fetch must hit the cache — no second reader created.
        _ = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 42);
        factory.Readers.Count.ShouldBe(1);
    }

    /// <summary>Verifies that different template IDs result in separate fetch operations.</summary>
    [Fact]
    public async Task FetchUdtShapeAsync_different_templateIds_each_fetch()
    {
        var callCount = 0;
        var factory = new FakeTemplateReaderFactory
        {
            Customise = () =>
            {
                callCount++;
                var name = callCount == 1 ? "UdtA" : "UdtB";
                return new FakeTemplateReader
                {
                    Response = BuildSimpleTemplate(name, 4, ("X", 0xC4, 0, 0)),
                };
            },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        }, "drv-1", templateReaderFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var a = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
        var b = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 2);

        a!.TypeName.ShouldBe("UdtA");
        b!.TypeName.ShouldBe("UdtB");
        factory.Readers.Count.ShouldBe(2);
    }

    /// <summary>Verifies that FetchUdtShapeAsync returns null for an unknown device.</summary>
    [Fact]
    public async Task FetchUdtShapeAsync_unknown_device_returns_null()
    {
        var factory = new FakeTemplateReaderFactory();
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        }, "drv-1", templateReaderFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var shape = await InvokeFetch(drv, "ab://10.0.0.99/1,0", 1);
        shape.ShouldBeNull();
        factory.Readers.ShouldBeEmpty();
    }

    /// <summary>Verifies that a decode failure returns null and does not cache the result.</summary>
    [Fact]
    public async Task FetchUdtShapeAsync_decode_failure_returns_null_and_does_not_cache()
    {
        var factory = new FakeTemplateReaderFactory
        {
            Customise = () => new FakeTemplateReader { Response = [0x00, 0x00] }, // too short
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        }, "drv-1", templateReaderFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var shape = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
        shape.ShouldBeNull();

        // Next call retries (not cached as a failure).
        var shape2 = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
        shape2.ShouldBeNull();
        factory.Readers.Count.ShouldBe(2);
    }

    /// <summary>Verifies that a reader exception returns null.</summary>
    [Fact]
    public async Task FetchUdtShapeAsync_reader_exception_returns_null()
    {
        var factory = new FakeTemplateReaderFactory
        {
            Customise = () => new ThrowingTemplateReader(),
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        }, "drv-1", templateReaderFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var shape = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 1);
        shape.ShouldBeNull();
    }

    /// <summary>Verifies that FlushOptionalCachesAsync empties the template cache.</summary>
    [Fact]
    public async Task FlushOptionalCachesAsync_empties_template_cache()
    {
        var factory = new FakeTemplateReaderFactory
        {
            Customise = () => new FakeTemplateReader
            {
                Response = BuildSimpleTemplate("U", 4, ("X", 0xC4, 0, 0)),
            },
        };
        var drv = new AbCipDriver(new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
        }, "drv-1", templateReaderFactory: factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        _ = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 99);
        drv.TemplateCache.Count.ShouldBe(1);

        await drv.FlushOptionalCachesAsync(CancellationToken.None);
        drv.TemplateCache.Count.ShouldBe(0);

        // Next fetch hits the network again.
        _ = await InvokeFetch(drv, "ab://10.0.0.5/1,0", 99);
        factory.Readers.Count.ShouldBe(2);
    }

    /// <summary>Test implementation of IAbCipTemplateReader that throws on read.</summary>
    private sealed class ThrowingTemplateReader : IAbCipTemplateReader
    {
        /// <summary>Throws an exception when read is attempted.</summary>
        /// <param name="p">The device parameters.</param>
        /// <param name="id">The template ID.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>Never returns; throws instead.</returns>
        public Task<byte[]> ReadAsync(AbCipTagCreateParams p, uint id, CancellationToken ct) =>
            throw new InvalidOperationException("fake read failure");

        /// <summary>Disposes the reader.</summary>
        public void Dispose() { }
    }
}
