using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;

/// <summary>
///     End-to-end verification that the driver's <c>MapModbusExceptionToStatus</c>
///     translation is wire-correct for every exception code in the mapping table —
///     not just 0x02, which is the only code the pymodbus simulator naturally emits.
///     Drives the standalone <c>exception_injector.py</c> server (<c>exception_injection</c>
///     compose profile) at each of the rule addresses in
///     <c>Docker/profiles/exception_injection.json</c> and asserts the driver surfaces
///     the expected OPC UA StatusCode.
/// </summary>
/// <remarks>
///     Why integration coverage on top of the unit tests: the unit tests prove the
///     translation function is correct; these prove the driver wires it through on
///     the read + write paths unchanged, after the MBAP header + PDU round-trip
///     (where a subtle framing bug could swallow or misclassify the exception).
/// </remarks>
[Collection(ModbusSimulatorCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Device", "ExceptionInjection")]
public sealed class ExceptionInjectionTests(ModbusSimulatorFixture sim)
{
    private const uint StatusGood = 0u;
    private const uint StatusBadOutOfRange = 0x803C0000u;
    private const uint StatusBadNotSupported = 0x803D0000u;
    private const uint StatusBadDeviceFailure = 0x808B0000u;
    private const uint StatusBadCommunicationError = 0x80050000u;

    private void SkipUnlessInjectorLive()
    {
        if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
        var profile = Environment.GetEnvironmentVariable("MODBUS_SIM_PROFILE");
        if (!string.Equals(profile, "exception_injection", StringComparison.OrdinalIgnoreCase))
            Assert.Skip("MODBUS_SIM_PROFILE != exception_injection — skipping. " +
                        "Start the fixture with --profile exception_injection.");
    }

    private async Task<IReadOnlyList<DataValueSnapshot>> ReadSingleAsync(int address, string tagName)
    {
        var opts = new ModbusDriverOptions
        {
            Host = sim.Host,
            Port = sim.Port,
            UnitId = 1,
            Timeout = TimeSpan.FromSeconds(2),
            RawTags = ModbusRawTags.Entries([
                new ModbusTagDefinition(tagName,
                    ModbusRegion.HoldingRegisters, Address: (ushort)address,
                    DataType: ModbusDataType.UInt16, Writable: false),
            ]),
            Probe = new ModbusProbeOptions { Enabled = false },
        };
        await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-exc");
        await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
        return await driver.ReadAsync([tagName], TestContext.Current.CancellationToken);
    }

    /// <summary>Verifies that Modbus FC03 reads at injected exception addresses surface the expected OPC UA status codes.</summary>
    /// <param name="address">The Modbus register address to read.</param>
    /// <param name="expectedStatus">The expected OPC UA status code.</param>
    /// <param name="scenario">Scenario description for assertion messages.</param>
    [Theory]
    [InlineData(1000, StatusBadNotSupported,        "exc 0x01 (Illegal Function) -> BadNotSupported")]
    [InlineData(1001, StatusBadOutOfRange,          "exc 0x02 (Illegal Data Address) -> BadOutOfRange")]
    [InlineData(1002, StatusBadOutOfRange,          "exc 0x03 (Illegal Data Value) -> BadOutOfRange")]
    [InlineData(1003, StatusBadDeviceFailure,       "exc 0x04 (Server Failure) -> BadDeviceFailure")]
    [InlineData(1004, StatusBadDeviceFailure,       "exc 0x05 (Acknowledge / long op) -> BadDeviceFailure")]
    [InlineData(1005, StatusBadDeviceFailure,       "exc 0x06 (Server Busy) -> BadDeviceFailure")]
    [InlineData(1006, StatusBadCommunicationError,  "exc 0x0A (Gateway Path Unavailable) -> BadCommunicationError")]
    [InlineData(1007, StatusBadCommunicationError,  "exc 0x0B (Gateway Target No Response) -> BadCommunicationError")]
    public async Task FC03_read_at_injection_address_surfaces_expected_status(
        int address, uint expectedStatus, string scenario)
    {
        SkipUnlessInjectorLive();
        var results = await ReadSingleAsync(address, $"Injected_{address}");
        results[0].StatusCode.ShouldBe(expectedStatus, scenario);
    }

    /// <summary>Verifies that reads at non-injected addresses return Good status.</summary>
    [Fact]
    public async Task FC03_read_at_non_injected_address_returns_Good()
    {
        // Sanity: HR[0..31] are seeded with address-as-value in the profile. A read at
        // one of those addresses must come back Good (0) — otherwise the injector is
        // misbehaving and every other assertion in this class is uninformative.
        SkipUnlessInjectorLive();
        var results = await ReadSingleAsync(address: 5, tagName: "Healthy_5");
        results[0].StatusCode.ShouldBe(StatusGood);
        results[0].Value.ShouldBe((ushort)5);
    }

    /// <summary>Verifies that Modbus FC06 writes at injected exception addresses surface the expected OPC UA status codes.</summary>
    /// <param name="address">The Modbus register address to write.</param>
    /// <param name="expectedStatus">The expected OPC UA status code.</param>
    /// <param name="scenario">Scenario description for assertion messages.</param>
    [Theory]
    [InlineData(2000, StatusBadDeviceFailure, "exc 0x04 on FC06 -> BadDeviceFailure (CPU in PROGRAM mode)")]
    [InlineData(2001, StatusBadDeviceFailure, "exc 0x06 on FC06 -> BadDeviceFailure (Server Busy)")]
    public async Task FC06_write_at_injection_address_surfaces_expected_status(
        int address, uint expectedStatus, string scenario)
    {
        SkipUnlessInjectorLive();
        var tag = $"InjectedWrite_{address}";
        var opts = new ModbusDriverOptions
        {
            Host = sim.Host,
            Port = sim.Port,
            UnitId = 1,
            Timeout = TimeSpan.FromSeconds(2),
            RawTags = ModbusRawTags.Entries([
                new ModbusTagDefinition(tag,
                    ModbusRegion.HoldingRegisters, Address: (ushort)address,
                    DataType: ModbusDataType.UInt16, Writable: true),
            ]),
            Probe = new ModbusProbeOptions { Enabled = false },
        };
        await using var driver = new ModbusDriver(opts, driverInstanceId: "modbus-exc-write");
        await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var writes = await driver.WriteAsync(
            [new WriteRequest(tag, (ushort)42)],
            TestContext.Current.CancellationToken);
        writes[0].StatusCode.ShouldBe(expectedStatus, scenario);
    }
}
