using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
{
    /// <summary>
    ///     PR C.1 — pins the trinary outcome → IPC bool[] mapping that the sidecar uses
    ///     on the WriteAlarmEvents reply. Per-event outcomes:
    ///       Ack → true,  RetryPlease → false,  PermanentFail → false.
    ///     The sender's B.4 widens the IPC bool back into the trinary outcome at the
    ///     IPC boundary using structured diagnostics; the wire intentionally collapses
    ///     to "ok / not-ok".
    /// </summary>
    [Trait("Category", "Unit")]
    public sealed class AahClientManagedAlarmEventWriterTests
    {
        /// <summary>Verifies that an empty batch returns an empty array without invoking the backend.</summary>
        [Fact]
        public async Task Empty_batch_returns_empty_array_without_invoking_backend()
        {
            var backend = new RecordingBackend(_ => throw new InvalidOperationException("must not invoke for empty input"));
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var result = await writer.WriteAsync(Array.Empty<AlarmHistorianEventDto>(), CancellationToken.None);

            result.ShouldBeEmpty();
            backend.Calls.ShouldBe(0);
        }

        /// <summary>Verifies that a single acknowledgment outcome maps to true.</summary>
        [Fact]
        public async Task Single_ack_outcome_maps_to_true()
        {
            var backend = new RecordingBackend(events => events.Select(_ => AlarmHistorianWriteOutcome.Ack).ToArray());
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var result = await writer.WriteAsync(new[] { Event("E1") }, CancellationToken.None);

            result.ShouldBe(new[] { true });
        }

        /// <summary>Verifies that a mixed batch preserves per-slot outcome ordering.</summary>
        [Fact]
        public async Task Mixed_batch_preserves_per_slot_ordering()
        {
            // Ack / Retry / Permanent / Ack — the sender uses positional matching against
            // its queue, so every slot must hit the exact bool corresponding to its input.
            var backend = new RecordingBackend(_ => new[]
            {
                AlarmHistorianWriteOutcome.Ack,
                AlarmHistorianWriteOutcome.RetryPlease,
                AlarmHistorianWriteOutcome.PermanentFail,
                AlarmHistorianWriteOutcome.Ack,
            });
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var result = await writer.WriteAsync(
                new[] { Event("E1"), Event("E2"), Event("E3"), Event("E4") },
                CancellationToken.None);

            result.ShouldBe(new[] { true, false, false, true });
        }

        /// <summary>Verifies that backend exceptions mark the whole batch as RetryPlease.</summary>
        [Fact]
        public async Task Backend_exception_marks_whole_batch_RetryPlease()
        {
            var backend = new RecordingBackend(_ => throw new InvalidOperationException("cluster unreachable"));
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var result = await writer.WriteAsync(
                new[] { Event("E1"), Event("E2"), Event("E3") },
                CancellationToken.None);

            // Whole batch must end up as "not ok" (RetryPlease at the trinary layer) —
            // dropping a transiently-failed batch corrupts the sender's queue.
            result.ShouldBe(new[] { false, false, false });
        }

        /// <summary>Verifies that cancellation propagates from the backend.</summary>
        [Fact]
        public async Task Cancellation_propagates_from_backend()
        {
            var backend = new RecordingBackend(_ => throw new OperationCanceledException());
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var ex = await Should.ThrowAsync<OperationCanceledException>(() =>
                writer.WriteAsync(new[] { Event("E1") }, CancellationToken.None));
            ex.ShouldNotBeNull();
        }

        /// <summary>Verifies that a backend returning the wrong outcome count degrades to RetryPlease.</summary>
        [Fact]
        public async Task Backend_returning_wrong_count_degrades_to_RetryPlease()
        {
            // Backend returns more outcomes than inputs — defensive degrade rather than
            // letting a backend bug desync the sender's queue accounting.
            var backend = new RecordingBackend(_ => new[]
            {
                AlarmHistorianWriteOutcome.Ack,
                AlarmHistorianWriteOutcome.Ack,
            });
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var result = await writer.WriteAsync(new[] { Event("E1") }, CancellationToken.None);

            result.ShouldBe(new[] { false });
        }

        /// <summary>Verifies that a large batch with all acknowledgments returns all true outcomes.</summary>
        /// <param name="batchSize">The batch size to test.</param>
        [Theory]
        [InlineData(100)]
        [InlineData(1000)]
        public async Task Large_batch_all_ack_returns_all_true(int batchSize)
        {
            // Spec: "1 / 100 / 1000 events through a fake aahClientManaged writer;
            // assert per-row outcome list parallel to input order."
            var backend = new RecordingBackend(events => events.Select(_ => AlarmHistorianWriteOutcome.Ack).ToArray());
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var batch = Enumerable.Range(0, batchSize)
                .Select(i => Event($"E{i}"))
                .ToArray();

            var result = await writer.WriteAsync(batch, CancellationToken.None);

            result.Length.ShouldBe(batchSize);
            result.ShouldAllBe(ok => ok);
            backend.Calls.ShouldBe(1);
        }

        /// <summary>Verifies that a large batch with alternating outcomes preserves positional ordering.</summary>
        /// <param name="batchSize">The batch size to test.</param>
        [Theory]
        [InlineData(100)]
        [InlineData(1000)]
        public async Task Large_batch_alternating_outcomes_are_positionally_correct(int batchSize)
        {
            // Verifies that per-row outcome ordering is preserved for large batches;
            // a backend that returns the outcomes in a different allocation order would
            // fail this test if the writer incorrectly indexing outcomes.
            var backend = new RecordingBackend(events =>
                events.Select((_, i) => i % 2 == 0
                    ? AlarmHistorianWriteOutcome.Ack
                    : AlarmHistorianWriteOutcome.RetryPlease).ToArray());
            var writer = new AahClientManagedAlarmEventWriter(backend);

            var batch = Enumerable.Range(0, batchSize).Select(i => Event($"E{i}")).ToArray();
            var result = await writer.WriteAsync(batch, CancellationToken.None);

            result.Length.ShouldBe(batchSize);
            for (var i = 0; i < result.Length; i++)
            {
                var expected = i % 2 == 0;
                result[i].ShouldBe(expected, $"slot {i}: expected {expected}");
            }
        }

        /// <summary>Verifies that retry then succeed correctly simulates cluster failover.</summary>
        [Fact]
        public async Task Backend_retry_then_succeed_simulates_cluster_failover()
        {
            // Spec: "Cluster failover: primary node returns BadCommunicationError;
            // picker rotates to secondary; assert eventual success."
            //
            // The real cluster-failover path is internal to SdkAlarmHistorianWriteBackend
            // (which is rig-gated) and is exercised at the HistorianClusterEndpointPicker
            // level in HistorianClusterEndpointPickerTests. Here we test the
            // AahClientManagedAlarmEventWriter's handling of a backend that returns
            // RetryPlease on the first call (primary-node failure) and Ack on the
            // second call (secondary-node success), confirming the IPC layer correctly
            // propagates the trinary outcome across two separate drain ticks.
            var callCount = 0;
            var backend = new RecordingBackend(events =>
            {
                callCount++;
                if (callCount == 1)
                {
                    // First call: simulate communication error (isCommunicationError=true)
                    // which produces RetryPlease — equivalent to primary node failing.
                    return events.Select(_ => AlarmHistorianWriteOutcome.RetryPlease).ToArray();
                }
                // Second call (after cluster picker has rotated to secondary): Ack.
                return events.Select(_ => AlarmHistorianWriteOutcome.Ack).ToArray();
            });
            var writer = new AahClientManagedAlarmEventWriter(backend);
            var batch = new[] { Event("E1"), Event("E2") };

            // First drain tick: primary "fails" → all RetryPlease (false at IPC layer).
            var firstResult = await writer.WriteAsync(batch, CancellationToken.None);
            firstResult.ShouldBe(new[] { false, false });

            // Second drain tick: secondary succeeds → all Ack (true at IPC layer).
            var secondResult = await writer.WriteAsync(batch, CancellationToken.None);
            secondResult.ShouldBe(new[] { true, true });

            backend.Calls.ShouldBe(2);
        }

        /// <summary>Verifies outcome mapping across various HRESULT and error condition combinations.</summary>
        /// <param name="hresult">The HRESULT code to test.</param>
        /// <param name="isCommunicationError">Whether the error is a communication error.</param>
        /// <param name="isMalformedInput">Whether the input is malformed.</param>
        /// <param name="expected">The expected outcome.</param>
        [Theory]
        // hresult 0 + clean → Ack
        [InlineData(0, false, false, AlarmHistorianWriteOutcome.Ack)]
        // hresult 0 but malformed → PermanentFail (malformed wins)
        [InlineData(0, false, true, AlarmHistorianWriteOutcome.PermanentFail)]
        // non-zero hresult + comm error → RetryPlease
        [InlineData(unchecked((int)0x80131500), true, false, AlarmHistorianWriteOutcome.RetryPlease)]
        // non-zero hresult, no comm flag, no malformed → conservative RetryPlease
        [InlineData(unchecked((int)0x80131500), false, false, AlarmHistorianWriteOutcome.RetryPlease)]
        // any malformed input → PermanentFail regardless of hresult
        [InlineData(unchecked((int)0x80131500), true, true, AlarmHistorianWriteOutcome.PermanentFail)]
        public void MapOutcome_table(int hresult, bool isCommunicationError, bool isMalformedInput, AlarmHistorianWriteOutcome expected)
        {
            AahClientManagedAlarmEventWriter
                .MapOutcome(hresult, isCommunicationError, isMalformedInput)
                .ShouldBe(expected);
        }

        private static AlarmHistorianEventDto Event(string id) => new AlarmHistorianEventDto
        {
            EventId = id,
            SourceName = "Tank01",
            ConditionId = "Tank01.Level.HiHi",
            AlarmType = "AnalogLimitAlarm.HiHi",
            Message = "Tank 01 high-high level",
            Severity = 750,
            EventTimeUtcTicks = DateTime.UtcNow.Ticks,
            AckComment = null,
        };

        /// <summary>Test double that records calls and returns outcomes via a delegate.</summary>
        private sealed class RecordingBackend : IAlarmHistorianWriteBackend
        {
            private readonly Func<AlarmHistorianEventDto[], AlarmHistorianWriteOutcome[]> _produce;

            /// <summary>Gets the number of calls recorded.</summary>
            public int Calls { get; private set; }

            /// <summary>Initializes a new instance of the <see cref="RecordingBackend"/> class.</summary>
            /// <param name="produce">A delegate that produces outcomes for the given events.</param>
            public RecordingBackend(Func<AlarmHistorianEventDto[], AlarmHistorianWriteOutcome[]> produce)
            {
                _produce = produce;
            }

            /// <summary>Records a call and returns outcomes from the delegate.</summary>
            /// <param name="events">The events to write.</param>
            /// <param name="cancellationToken">Cancellation token.</param>
            /// <returns>The outcomes produced by the delegate.</returns>
            public Task<AlarmHistorianWriteOutcome[]> WriteBatchAsync(
                AlarmHistorianEventDto[] events, CancellationToken cancellationToken)
            {
                Calls++;
                return Task.FromResult(_produce(events));
            }
        }
    }
}
