unit Myc.Test.Signals.Latch;

interface

uses
    DUnitX.TestFramework,
    System.SysUtils,
    Myc.Signals; // Unit under test, using TMycLatch static members

type
    // Helper class to mock a subscriber.
    TMockSubscriber = class(TInterfacedObject, TSignal.ISubscriber)
    public
        NotifyCount: Integer;
        NotifyShouldReturn: Boolean; // Determines what this mock's Notify method returns
        WasCalled: Boolean;

        constructor Create(AShouldReturn: Boolean = True); // Parameter name AShouldReturn for clarity
        function Notify: Boolean; // TSignal.ISubscriber implementation.
    end;

    [TestFixture]
    TTestMycLatch = class(TObject)
    public
        [Setup]
        procedure Setup;
        [TearDown]
        procedure TearDown;

        // --- Category 1: Basic Counter and State Functionality (Parameterized) ---
        [TestCase('InitialPositive', '1,False')]
        [TestCase('InitialZero_ReturnsNullLatch', '0,True')]
        [TestCase('InitialNegative_ReturnsNullLatch', '-1,True')]
        [TestCase('InitialLargePositive', '5,False')]
        procedure TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean);

        [TestCase('DecrementPositiveToZero', '1,False,True')] // InitialCount, ExpectedReturnFromLatchNotify, ExpectedIsSetAfterNotify
        [TestCase('DecrementPositiveToPositive', '2,True,False')]
        [TestCase('DecrementZeroToNegative', '0,False,True')] // Latch.Notify on count 0 returns false (0 > 0 is false)
        [TestCase('DecrementNegativeToNegative', '-1,False,True')] // Latch.Notify on count -1 returns false (-1 > 0 is false)
        procedure TestNotify_ReturnValueAndState_AfterOneNotify(InitialCount: Integer; ExpectedReturn: Boolean; ExpectedIsSet: Boolean);

        [Test]
        procedure TestNotify_DecrementsCounter_StateChanges;
        [Test]
        procedure TestNotify_MultipleNotifies_CounterBecomesNegativeAndStaysSet;

        // --- Category 2: Notification to Subscribers ---
        [Test]
        procedure TestSubscriber_Single_IsNotifiedWhenLatchSet;
        [Test]
        procedure TestSubscriber_Multiple_AreNotifiedWhenLatchSet;
        [Test]
        procedure TestSubscriber_NotNotified_BeforeLatchSet;
        [Test]
        procedure TestSubscriber_NotifiedOnlyOnce_AfterLatchSetAndUnadvised; // Clarified name
        [Test]
        procedure TestSubscribe_ToAlreadySetLatch_NotifiesImmediately;

        // --- Category 3: Chaining and Cascading Latches ---
        [Test]
        procedure TestChaining_A_Notifies_B;
        [Test]
        procedure TestCascade_A_Notifies_B_Notifies_C;
        [Test]
        procedure TestChaining_B_NeedsMultipleNotifiesFromA_WithSubscriberOnB;

        // --- Category 4: Special Cases ---
        [Test]
        procedure TestInitiallySetLatch_NotifiesNewSubscriberImmediately; // Using CreateLatch(0)

        [Test]
        procedure TestUnsubscribe_SubscriberNotNotified;

        // --- Category 5: Counter Integrity / State for Later Subscribers ---
        [Test]
        procedure TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber;

        // --- Category 6: UnadviseAll Behavior on Latch Set ---
        [Test]
        procedure TestLatchSet_ClearsSubscribers_NoFurtherNotification;
        [Test]
        procedure TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll;
        [Test]
        procedure TestLatchSet_NewSubscriberToSetLatch_NotifiedImmediatelyButNotPersistedForLatchNotify;
    end;

implementation

{ TMockSubscriber }

constructor TMockSubscriber.Create(AShouldReturn: Boolean = True);
begin
    inherited Create;
    NotifyCount := 0;
    NotifyShouldReturn := AShouldReturn;
    WasCalled := False;
end;

function TMockSubscriber.Notify: Boolean;
begin
    Inc(NotifyCount);
    WasCalled := True;
    Result := NotifyShouldReturn; // This mock subscriber returns what it's configured to.
end;

{ TTestMycLatch }

procedure TTestMycLatch.Setup;
begin
    // Per-test setup code can go here (if any).
end;

procedure TTestMycLatch.TearDown;
begin
    // Per-test teardown code can go here (if any).
end;

// --- Category 1: Basic Counter and State Functionality (Parameterized) ---

procedure TTestMycLatch.TestCreate_InitialState(InitialCount: Integer; ExpectedIsSet: Boolean);
var
    latch: TLatch.ILatch;
begin
    // TMycLatch.CreateLatch returns TMycLatch.Null if InitialCount <= 0
    latch := TLatch.CreateLatch(InitialCount);
    Assert.AreEqual(ExpectedIsSet, latch.State.IsSet, 'Latch initial IsSet state mismatch for count ' + IntToStr(InitialCount) + '.');
end;

procedure TTestMycLatch.TestNotify_ReturnValueAndState_AfterOneNotify(
    InitialCount: Integer;
    ExpectedReturn: Boolean;
    ExpectedIsSet: Boolean
);
var
    latch: TLatch.ILatch;
    returnedValueFromNotify: Boolean;
begin
    latch := TLatch.CreateLatch(InitialCount);
    returnedValueFromNotify := latch.Notify; // This is TLatch.ILatch (as TSignal.ISubscriber).Notify

    Assert.AreEqual(
        ExpectedReturn,
        returnedValueFromNotify,
        'Unexpected return value from Latch.Notify call for initial count ' + IntToStr(InitialCount) + '.'
    );
    Assert.AreEqual(
        ExpectedIsSet,
        latch.State.IsSet,
        'Unexpected IsSet state after Latch.Notify call for initial count ' + IntToStr(InitialCount) + '.'
    );
end;

procedure TTestMycLatch.TestNotify_DecrementsCounter_StateChanges;
var
    latch: TLatch.ILatch;
begin
    latch := TLatch.CreateLatch(1);
    Assert.IsFalse(latch.State.IsSet, 'Latch should initially not be set (Count=1).');
    latch.Notify; // Count becomes 0
    Assert.IsTrue(latch.State.IsSet, 'Latch should be set after Notify (Count became 0).');
end;

procedure TTestMycLatch.TestNotify_MultipleNotifies_CounterBecomesNegativeAndStaysSet;
var
    latch: TLatch.ILatch;
begin
    latch := TLatch.CreateLatch(1);
    latch.Notify; // Count becomes 0, IsSet = True
    Assert.IsTrue(latch.State.IsSet, 'Latch should be set after first Notify.');
    latch.Notify; // Count becomes -1, IsSet should remain True
    Assert.IsTrue(latch.State.IsSet, 'Latch should remain set after second Notify.');
end;

// --- Category 2: Notification to Subscribers ---

procedure TTestMycLatch.TestSubscriber_Single_IsNotifiedWhenLatchSet;
var
    latch: TLatch.ILatch;
    mockSub: TMockSubscriber;
    subscription: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(1);
    mockSub := TMockSubscriber.Create;
    subscription := latch.State.Signal.Subscribe(mockSub); // Subscribing to the Latch's state

    Assert.IsFalse(latch.State.IsSet, 'Latch should initially not be set.');
    Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not have been notified yet (before latch set).');

    latch.Notify; // Latch becomes set, notifies subscribers

    Assert.IsTrue(latch.State.IsSet, 'Latch should be set after its Notify is called.');
    Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified once when latch becomes set.');
end;

procedure TTestMycLatch.TestSubscriber_Multiple_AreNotifiedWhenLatchSet;
var
    latch: TLatch.ILatch;
    mockSub1, mockSub2, mockSub3: TMockSubscriber;
    sub1, sub2, sub3: TSignal.TSubscription; // Keep subscriptions in scope
begin
    latch := TLatch.CreateLatch(1);
    mockSub1 := TMockSubscriber.Create;
    mockSub2 := TMockSubscriber.Create;
    mockSub3 := TMockSubscriber.Create;

    sub1 := latch.State.Signal.Subscribe(mockSub1);
    sub2 := latch.State.Signal.Subscribe(mockSub2);
    sub3 := latch.State.Signal.Subscribe(mockSub3);

    latch.Notify; // Latch becomes set

    Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 notification count mismatch.');
    Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 notification count mismatch.');
    Assert.AreEqual(1, mockSub3.NotifyCount, 'MockSub3 notification count mismatch.');
end;

procedure TTestMycLatch.TestSubscriber_NotNotified_BeforeLatchSet;
var
    latch: TLatch.ILatch;
    mockSub: TMockSubscriber;
    subscription: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(2); // Count = 2
    mockSub := TMockSubscriber.Create;
    subscription := latch.State.Signal.Subscribe(mockSub);

    Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified upon subscription if latch is not set.');
    latch.Notify; // Count becomes 1, still not set
    Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should still not be notified as latch is not yet set (Count=1).');
end;

procedure TTestMycLatch.TestSubscriber_NotifiedOnlyOnce_AfterLatchSetAndUnadvised;
var
    latch: TLatch.ILatch;
    mockSub: TMockSubscriber;
    subscription: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(1);
    mockSub := TMockSubscriber.Create;
    subscription := latch.State.Signal.Subscribe(mockSub);

    latch.Notify; // Sets latch, notifies MockSub. Subscribers are then unadvised by UnadviseAll.
    Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should be 1 after latch set.');

    latch.Notify; // Call Notify on latch again (FCount becomes more negative).
    Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub notify count should remain 1 (not notified again as it was unadvised).');
end;

procedure TTestMycLatch.TestSubscribe_ToAlreadySetLatch_NotifiesImmediately;
var
    latch: TLatch.ILatch;
    mockSub1, mockSub2: TMockSubscriber;
    subscription1, subscription2: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(1); // Create with count 1
    mockSub1 := TMockSubscriber.Create;
    subscription1 := latch.State.Signal.Subscribe(mockSub1); // Subscribe before set

    latch.Notify; // Latch becomes set. mockSub1 is notified.

    Assert.IsTrue(latch.State.IsSet, 'Latch should be set after initial trigger.');
    Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should have been notified once.');

    // Attempt to subscribe mockSub2 after the latch is already set.
    mockSub2 := TMockSubscriber.Create;
    subscription2 :=
        latch.State.Signal.Subscribe(mockSub2); // TMycLatch.Subscribe (via TMycAbstractFlag) notifies immediately if already set.

    Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should be notified immediately upon subscribing to an already set latch.');

    latch.Notify; // Call Notify on latch again (FCount becomes more negative).
    Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 notify count should remain 1 (was unadvised).');
    Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 notify count should remain 1 (was only notified on subscribe, not added to list).');
end;

// --- Category 3: Chaining and Cascading Latches ---

procedure TTestMycLatch.TestChaining_A_Notifies_B;
var
    latchA, latchB: TLatch.ILatch;
    mockSubForB: TMockSubscriber;
    subHandle_B_listens_A, subHandle_Mock_listens_B: TSignal.TSubscription;
begin
    latchA := TLatch.CreateLatch(1);
    latchB := TLatch.CreateLatch(1); // latchB is an TSignal.ISubscriber
    mockSubForB := TMockSubscriber.Create;

    subHandle_Mock_listens_B := latchB.State.Signal.Subscribe(mockSubForB);
    // LatchA's state is an IMycSignal. LatchB (as TSignal.ISubscriber) subscribes to it.
    subHandle_B_listens_A := latchA.State.Signal.Subscribe(latchB);

    Assert.IsFalse(latchA.State.IsSet, 'LatchA initially not set.');
    Assert.IsFalse(latchB.State.IsSet, 'LatchB initially not set.');
    Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB initially not notified.');

    latchA.Notify; // Call Notify on LatchA (as TSignal.ISubscriber)

    Assert.IsTrue(latchA.State.IsSet, 'LatchA should be set after notify.');
    Assert.IsTrue(latchB.State.IsSet, 'LatchB should be set after being notified by LatchA.');
    Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by LatchB.');
end;

procedure TTestMycLatch.TestCascade_A_Notifies_B_Notifies_C;
var
    latchA, latchB, latchC: TLatch.ILatch;
    mockSubForC: TMockSubscriber;
    sub_Mock_C, sub_C_B, sub_B_A: TSignal.TSubscription;
begin
    latchA := TLatch.CreateLatch(1);
    latchB := TLatch.CreateLatch(1);
    latchC := TLatch.CreateLatch(1);
    mockSubForC := TMockSubscriber.Create;

    sub_Mock_C := latchC.State.Signal.Subscribe(mockSubForC);
    sub_C_B := latchB.State.Signal.Subscribe(latchC); // LatchC subscribes to LatchB's state
    sub_B_A := latchA.State.Signal.Subscribe(latchB); // LatchB subscribes to LatchA's state

    latchA.Notify; // Call Notify on LatchA

    Assert.IsTrue(latchA.State.IsSet, 'LatchA state mismatch in cascade.');
    Assert.IsTrue(latchB.State.IsSet, 'LatchB state mismatch in cascade.');
    Assert.IsTrue(latchC.State.IsSet, 'LatchC state mismatch in cascade.');
    Assert.AreEqual(1, mockSubForC.NotifyCount, 'MockSubForC notification count mismatch in cascade.');
end;

procedure TTestMycLatch.TestChaining_B_NeedsMultipleNotifiesFromA_WithSubscriberOnB;
var
    latchA, latchB: TLatch.ILatch;
    mockSubForB: TMockSubscriber;
    sub_Mock_B, sub_B_A: TSignal.TSubscription;
begin
    // LatchA needs 2 notifies to become set. LatchB needs 1 notify to become set.
    // LatchB subscribes to LatchA. So LatchB will be notified once LatchA becomes set.
    latchA := TLatch.CreateLatch(2);
    latchB := TLatch.CreateLatch(1);
    mockSubForB := TMockSubscriber.Create;

    sub_Mock_B := latchB.State.Signal.Subscribe(mockSubForB);
    sub_B_A := latchA.State.Signal.Subscribe(latchB);

    latchA.Notify; // First notify for LatchA (count becomes 1)
    Assert.IsFalse(latchA.State.IsSet, 'LatchA should not be set after first notify of two.');
    Assert.IsFalse(latchB.State.IsSet, 'LatchB should not have been triggered yet by LatchA.');
    Assert.AreEqual(0, mockSubForB.NotifyCount, 'MockSubForB should not have been notified yet.');

    latchA.Notify; // Second notify for LatchA (count becomes 0, LatchA becomes set)
    Assert.IsTrue(latchA.State.IsSet, 'LatchA should be set after second notify.');
    // When LatchA becomes set, it notifies its subscribers (LatchB). LatchB.Notify is called.
    Assert.IsTrue(latchB.State.IsSet, 'LatchB should now be set by LatchA.');
    Assert.AreEqual(1, mockSubForB.NotifyCount, 'MockSubForB should have been notified by LatchB.');
end;

// --- Category 4: Special Cases ---

procedure TTestMycLatch.TestInitiallySetLatch_NotifiesNewSubscriberImmediately;
var
    latch: TLatch.ILatch; // Will be TMycLatch.Null
    mockSub: TMockSubscriber;
    subscription: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(0); // Returns TMycLatch.Null which is set.
    mockSub := TMockSubscriber.Create;

    subscription := latch.State.Signal.Subscribe(mockSub); // TMycLatch.Subscribe notifies immediately if already set.

    Assert.IsTrue(latch.State.IsSet, 'Latch IsSet property mismatch (should be true for Null latch).');
    Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should be notified immediately upon subscribing to an initially set latch.');
end;

procedure TTestMycLatch.TestUnsubscribe_SubscriberNotNotified;
var
    latch: TLatch.ILatch;
    mockSub: TMockSubscriber;
    subscription: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(1);
    mockSub := TMockSubscriber.Create;

    subscription := latch.State.Signal.Subscribe(mockSub);
    subscription.Unsubscribe; // Explicitly call Unsubscribe on the record

    latch.Notify; // Latch becomes set

    Assert.AreEqual(0, mockSub.NotifyCount, 'Unsubscribed MockSub should not be notified.');
end;

// --- Category 5: Counter Integrity / State for Later Subscribers ---

procedure TTestMycLatch.TestMultipleTriggersNoSubscriber_StateCorrectForLaterSubscriber;
var
    latch: TLatch.ILatch;
    mockSub: TMockSubscriber;
    subscription: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(3);

    latch.Notify; // Count -> 2
    latch.Notify; // Count -> 1
    Assert.IsFalse(latch.State.IsSet, 'Latch should not be set yet after partial notifies.');

    mockSub := TMockSubscriber.Create;
    subscription := latch.State.Signal.Subscribe(mockSub);
    Assert.AreEqual(0, mockSub.NotifyCount, 'MockSub should not be notified on subscribe if latch not yet set.');

    latch.Notify; // Count -> 0, Latch becomes set and notifies mockSub
    Assert.IsTrue(latch.State.IsSet, 'Latch should now be set.');
    Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should have been notified when latch becomes set.');
end;

// --- Category 6: UnadviseAll Behavior on Latch Set ---

procedure TTestMycLatch.TestLatchSet_ClearsSubscribers_NoFurtherNotification;
var
    latch: TLatch.ILatch;
    mockSub1, mockSub2: TMockSubscriber;
    subscription1, subscription2: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(1);
    mockSub1 := TMockSubscriber.Create;
    mockSub2 := TMockSubscriber.Create;

    subscription1 := latch.State.Signal.Subscribe(mockSub1);
    subscription2 := latch.State.Signal.Subscribe(mockSub2);

    Assert.AreEqual(0, mockSub1.NotifyCount, 'MockSub1 initial NotifyCount should be 0.');
    Assert.AreEqual(0, mockSub2.NotifyCount, 'MockSub2 initial NotifyCount should be 0.');

    latch.Notify; // FCount becomes 0. Subscribers notified, then UnadviseAll called.

    Assert.IsTrue(latch.State.IsSet, 'Latch should be set.');
    Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should have been notified once when latch became set.');
    Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should have been notified once when latch became set.');

    latch.Notify; // Call Notify on the latch again (FCount becomes -1)

    Assert.AreEqual(1, mockSub1.NotifyCount, 'MockSub1 should NOT be notified again after UnadviseAll.');
    Assert.AreEqual(1, mockSub2.NotifyCount, 'MockSub2 should NOT be notified again after UnadviseAll.');
end;

procedure TTestMycLatch.TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll;
var
    latch: TLatch.ILatch;
    mockSub: TMockSubscriber;
// 'subscription' will be declared in an inner scope to control its finalization
begin
    latch := TLatch.CreateLatch(1);
    mockSub := TMockSubscriber.Create;

    var noException: Boolean := True;
    try
        begin // Inner block to control lifetime of 'subscription'
            var subscription: TSignal.TSubscription; // Local to this block
            subscription := latch.State.Signal.Subscribe(mockSub);

            latch.Notify; // Sets the latch, mockSub is notified, UnadviseAll is called internally.
            Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub was notified when latch set.');

            // When 'subscription' goes out of scope at the end of this 'begin..end' block,
            // its Finalize operator will be called, which in turn calls subscription.Unsubscribe.
            // This happens *after* UnadviseAll was triggered by latch.Notify.
        end; // <-- subscription.Finalize (and thus Unsubscribe) is called here.
    except
        on E: Exception do
        begin
            noException := False;
            // Optional: Log.Error('Exception in TestLatchSet_SubscriptionFinalize_IsSafePostUnadviseAll: ' + E.Message);
        end;
    end;
    Assert.IsTrue(noException, 'Finalizing subscription after Latch set (and UnadviseAll) should not raise an exception.');

    // Further check: calling Notify on latch again should not affect the original mockSub
    latch.Notify;
    Assert.AreEqual(1, mockSub.NotifyCount, 'MockSub should not be notified after its subscription was finalized post UnadviseAll.');
end;

procedure TTestMycLatch.TestLatchSet_NewSubscriberToSetLatch_NotifiedImmediatelyButNotPersistedForLatchNotify;
var
    latch: TLatch.ILatch;
    mockSubInitial, mockSubNew: TMockSubscriber;
    subscriptionInitial, subscriptionNew: TSignal.TSubscription;
begin
    latch := TLatch.CreateLatch(1);
    mockSubInitial := TMockSubscriber.Create;
    subscriptionInitial := latch.State.Signal.Subscribe(mockSubInitial);

    // Set the latch; mockSubInitial is notified, UnadviseAll is called.
    latch.Notify;
    Assert.AreEqual(1, mockSubInitial.NotifyCount, 'Initial subscriber notified.');

    // Subscribe a new subscriber to the already set latch
    mockSubNew := TMockSubscriber.Create;
    subscriptionNew := latch.State.Signal.Subscribe(mockSubNew);

    // New subscriber should be notified immediately upon subscription to an already set latch
    Assert.AreEqual(1, mockSubNew.NotifyCount, 'New subscriber should be notified immediately upon subscription.');
    // The direct check of subscriptionNew.FTag was removed as FTag is private.
    // The correct behavior (no persistent subscription) is verified by the next step.

    // Call Notify on the latch again.
    latch.Notify;

    // mockSubInitial should not be notified again (was unadvised).
    Assert.AreEqual(1, mockSubInitial.NotifyCount, 'Initial subscriber (unadvised) not notified again.');
    // mockSubNew should also not be notified again, as it was only notified immediately
    // and not persistently added to FSubscribers for subsequent Latch.Notify calls.
    Assert
        .AreEqual(1, mockSubNew.NotifyCount, 'New subscriber (notified once on subscribe) not notified by subsequent Latch.Notify calls.');
end;

initialization
    TDUnitX.RegisterTestFixture(TTestMycLatch);
end.
