{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

{- | Per-stream cursor tests

Tests per-stream cursor functionality for multi-stream transactions:
- Cursor extraction from multi-stream transactions
- Cursor independence across streams
- Stale cursor detection in multi-stream transactions
- Cursor completeness (all streams get cursors)
- Empty stream cursor handling (edge case)
-}
module Test.Hindsight.Store.CursorTests (cursorTests) where

import Control.Monad (forM_)
import Data.Map.Strict qualified as Map
import Data.UUID.V4 qualified as UUID
import Hindsight.Store
import Test.Hindsight.Examples (makeUserEvent)
import Test.Hindsight.Store.TestRunner (EventStoreTestRunner (..))
import Test.Tasty
import Test.Tasty.HUnit

-- | Per-stream cursor test suite for event store backends
cursorTests ::
    forall backend.
    (EventStore backend, StoreConstraints backend IO, Show (Cursor backend), Ord (Cursor backend)) =>
    EventStoreTestRunner backend ->
    [TestTree]
cursorTests :: forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend), Ord (Cursor backend)) =>
EventStoreTestRunner backend -> [TestTree]
cursorTests EventStoreTestRunner backend
runner =
    [ TestName -> Assertion -> TestTree
testCase TestName
"Per-Stream Cursor Extraction" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
forall backend.
EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
withStore EventStoreTestRunner backend
runner BackendHandle backend -> Assertion
forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testPerStreamCursorExtraction
    , TestName -> Assertion -> TestTree
testCase TestName
"Cursor Independence" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
forall backend.
EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
withStore EventStoreTestRunner backend
runner BackendHandle backend -> Assertion
forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testCursorIndependence
    , TestName -> Assertion -> TestTree
testCase TestName
"Stale Cursor Per Stream" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
forall backend.
EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
withStore EventStoreTestRunner backend
runner BackendHandle backend -> Assertion
forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testStaleCursorPerStream
    , TestName -> Assertion -> TestTree
testCase TestName
"Cursor Completeness" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
forall backend.
EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
withStore EventStoreTestRunner backend
runner BackendHandle backend -> Assertion
forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend), Ord (Cursor backend)) =>
BackendHandle backend -> Assertion
testCursorCompleteness
    , TestName -> Assertion -> TestTree
testCase TestName
"Empty Stream Cursor Handling" (Assertion -> TestTree) -> Assertion -> TestTree
forall a b. (a -> b) -> a -> b
$ EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
forall backend.
EventStoreTestRunner backend
-> forall a. (BackendHandle backend -> IO a) -> Assertion
withStore EventStoreTestRunner backend
runner BackendHandle backend -> Assertion
forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testEmptyStreamCursorHandling
    ]

-- * Test Implementations

{- | Test that per-stream cursors can be extracted from multi-stream transactions
and used for optimistic locking on specific streams.

This is the core use case from Tutorial 08: when inserting events to multiple
streams in a single transaction, we need to get back individual cursors for
each stream to use in subsequent optimistic locking operations.
-}
testPerStreamCursorExtraction :: forall backend. (EventStore backend, StoreConstraints backend IO, Show (Cursor backend)) => BackendHandle backend -> IO ()
testPerStreamCursorExtraction :: forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testPerStreamCursorExtraction BackendHandle backend
store = do
    streamA <- UUID -> StreamId
StreamId (UUID -> StreamId) -> IO UUID -> IO StreamId
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO UUID
UUID.nextRandom
    streamB <- StreamId <$> UUID.nextRandom
    streamC <- StreamId <$> UUID.nextRandom

    -- Multi-stream transaction
    result1 <-
        insertEvents store Nothing $
            Transaction
                ( Map.fromList
                    [ (streamA, StreamWrite NoStream [makeUserEvent 1])
                    , (streamB, StreamWrite NoStream [makeUserEvent 10])
                    , (streamC, StreamWrite NoStream [makeUserEvent 100])
                    ]
                )

    case result1 of
        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Initial multi-stream write failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
        SuccessfulInsertion (InsertionSuccess{Map StreamId (Cursor backend)
streamCursors :: Map StreamId (Cursor backend)
streamCursors :: forall backend.
InsertionSuccess backend -> Map StreamId (Cursor backend)
streamCursors}) -> do
            -- Extract cursor for stream A
            case StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamA Map StreamId (Cursor backend)
streamCursors of
                Maybe (Cursor backend)
Nothing -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure TestName
"Stream A cursor missing from streamCursors"
                Just Cursor backend
cursorA -> do
                    -- Use cursor A to append to stream A with optimistic locking
                    result2 <-
                        BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                            StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamA (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorA) (Int -> SomeLatestEvent
makeUserEvent Int
2)

                    case result2 of
                        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Append with stream A cursor failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
                        SuccessfulInsertion InsertionSuccess backend
_ -> do
                            -- Try to use the same cursor again - should fail (stale)
                            result3 <-
                                BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                                    StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamA (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorA) (Int -> SomeLatestEvent
makeUserEvent Int
3)

                            case result3 of
                                FailedInsertion (ConsistencyError ConsistencyErrorInfo backend
_) -> () -> Assertion
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure () -- Expected!
                                InsertionResult backend
_ -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure TestName
"Should not be able to reuse stale cursor"

{- | Test that cursors for different streams are independent.

Updating stream A should not invalidate stream B's cursor. This is critical
for concurrent multi-stream operations where different processes may be
updating different streams.
-}
testCursorIndependence :: forall backend. (EventStore backend, StoreConstraints backend IO, Show (Cursor backend)) => BackendHandle backend -> IO ()
testCursorIndependence :: forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testCursorIndependence BackendHandle backend
store = do
    streamA <- UUID -> StreamId
StreamId (UUID -> StreamId) -> IO UUID -> IO StreamId
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO UUID
UUID.nextRandom
    streamB <- StreamId <$> UUID.nextRandom

    -- Tx1: Initialize both streams
    result1 <-
        insertEvents store Nothing $
            Transaction
                ( Map.fromList
                    [ (streamA, StreamWrite NoStream [makeUserEvent 1])
                    , (streamB, StreamWrite NoStream [makeUserEvent 10])
                    ]
                )

    case result1 of
        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Initial write failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
        SuccessfulInsertion (InsertionSuccess{Map StreamId (Cursor backend)
streamCursors :: forall backend.
InsertionSuccess backend -> Map StreamId (Cursor backend)
streamCursors :: Map StreamId (Cursor backend)
streamCursors}) -> do
            case (StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamA Map StreamId (Cursor backend)
streamCursors, StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamB Map StreamId (Cursor backend)
streamCursors) of
                (Just Cursor backend
cursorA, Just Cursor backend
cursorB) -> do
                    -- Tx2: Update stream A (should NOT affect cursorB)
                    result2 <-
                        BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                            StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamA (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorA) (Int -> SomeLatestEvent
makeUserEvent Int
2)

                    case result2 of
                        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Stream A update failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
                        SuccessfulInsertion InsertionSuccess backend
_ -> do
                            -- Tx3: Use cursorB to append to stream B
                            -- This should succeed because stream B hasn't been modified
                            result3 <-
                                BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                                    StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamB (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorB) (Int -> SomeLatestEvent
makeUserEvent Int
11)

                            case result3 of
                                FailedInsertion EventStoreError backend
err ->
                                    TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Stream B cursor should still be valid after stream A update: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
                                SuccessfulInsertion InsertionSuccess backend
_ -> () -> Assertion
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure () -- Success! Independence verified
                (Maybe (Cursor backend), Maybe (Cursor backend))
_ -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure TestName
"Missing cursors from initial transaction"

{- | Test that using a stale cursor for one stream in a multi-stream transaction
causes the entire transaction to fail atomically.
-}
testStaleCursorPerStream :: forall backend. (EventStore backend, StoreConstraints backend IO, Show (Cursor backend)) => BackendHandle backend -> IO ()
testStaleCursorPerStream :: forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testStaleCursorPerStream BackendHandle backend
store = do
    streamA <- UUID -> StreamId
StreamId (UUID -> StreamId) -> IO UUID -> IO StreamId
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO UUID
UUID.nextRandom
    streamB <- StreamId <$> UUID.nextRandom

    -- Initialize streams
    result1 <-
        insertEvents store Nothing $
            Transaction
                ( Map.fromList
                    [ (streamA, StreamWrite NoStream [makeUserEvent 1])
                    , (streamB, StreamWrite NoStream [makeUserEvent 10])
                    ]
                )

    case result1 of
        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Initial write failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
        SuccessfulInsertion (InsertionSuccess{Map StreamId (Cursor backend)
streamCursors :: forall backend.
InsertionSuccess backend -> Map StreamId (Cursor backend)
streamCursors :: Map StreamId (Cursor backend)
streamCursors}) -> do
            case (StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamA Map StreamId (Cursor backend)
streamCursors, StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamB Map StreamId (Cursor backend)
streamCursors) of
                (Just Cursor backend
cursorA, Just Cursor backend
cursorB) -> do
                    -- Update stream A separately (makes cursorA stale)
                    result2 <-
                        BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                            StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamA (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorA) (Int -> SomeLatestEvent
makeUserEvent Int
2)

                    case result2 of
                        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Stream A update failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
                        SuccessfulInsertion InsertionSuccess backend
_ -> do
                            -- Try multi-stream transaction with stale cursorA
                            -- Even though cursorB is still valid, the transaction should fail
                            result3 <-
                                BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                                    Map StreamId (StreamWrite [] SomeLatestEvent backend)
-> Transaction [] backend
forall (t :: * -> *) backend.
Map StreamId (StreamWrite t SomeLatestEvent backend)
-> Transaction t backend
Transaction
                                        ( [(StreamId, StreamWrite [] SomeLatestEvent backend)]
-> Map StreamId (StreamWrite [] SomeLatestEvent backend)
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList
                                            [ (StreamId
streamA, ExpectedVersion backend
-> [SomeLatestEvent] -> StreamWrite [] SomeLatestEvent backend
forall {k} (t :: k -> *) (e :: k) backend.
ExpectedVersion backend -> t e -> StreamWrite t e backend
StreamWrite (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorA) [Int -> SomeLatestEvent
makeUserEvent Int
3]) -- stale!
                                            , (StreamId
streamB, ExpectedVersion backend
-> [SomeLatestEvent] -> StreamWrite [] SomeLatestEvent backend
forall {k} (t :: k -> *) (e :: k) backend.
ExpectedVersion backend -> t e -> StreamWrite t e backend
StreamWrite (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorB) [Int -> SomeLatestEvent
makeUserEvent Int
11]) -- valid
                                            ]
                                        )

                            case result3 of
                                FailedInsertion (ConsistencyError ConsistencyErrorInfo backend
_) -> () -> Assertion
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure () -- Expected! Atomic failure
                                InsertionResult backend
_ -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure TestName
"Transaction with stale cursor should fail atomically"
                (Maybe (Cursor backend), Maybe (Cursor backend))
_ -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure TestName
"Missing cursors from initial transaction"

{- | Test that all streams in a multi-stream transaction get cursors,
and that the cursors map is complete.
-}
testCursorCompleteness :: forall backend. (EventStore backend, StoreConstraints backend IO, Show (Cursor backend), Ord (Cursor backend)) => BackendHandle backend -> IO ()
testCursorCompleteness :: forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend), Ord (Cursor backend)) =>
BackendHandle backend -> Assertion
testCursorCompleteness BackendHandle backend
store = do
    streamA <- UUID -> StreamId
StreamId (UUID -> StreamId) -> IO UUID -> IO StreamId
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO UUID
UUID.nextRandom
    streamB <- StreamId <$> UUID.nextRandom
    streamC <- StreamId <$> UUID.nextRandom

    -- Multi-stream transaction to 3 streams
    result <-
        insertEvents store Nothing $
            Transaction
                ( Map.fromList
                    [ (streamA, StreamWrite NoStream [makeUserEvent 1, makeUserEvent 2])
                    , (streamB, StreamWrite NoStream [makeUserEvent 10])
                    , (streamC, StreamWrite NoStream [makeUserEvent 100, makeUserEvent 101, makeUserEvent 102])
                    ]
                )

    case result of
        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Multi-stream write failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
        SuccessfulInsertion (InsertionSuccess{Cursor backend
finalCursor :: Cursor backend
finalCursor :: forall backend. InsertionSuccess backend -> Cursor backend
finalCursor, Map StreamId (Cursor backend)
streamCursors :: forall backend.
InsertionSuccess backend -> Map StreamId (Cursor backend)
streamCursors :: Map StreamId (Cursor backend)
streamCursors}) -> do
            -- Verify all 3 streams have cursors
            Map StreamId (Cursor backend) -> Int
forall k a. Map k a -> Int
Map.size Map StreamId (Cursor backend)
streamCursors Int -> Int -> Assertion
forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
@?= Int
3

            -- Verify all stream IDs are present
            HasCallStack => TestName -> Bool -> Assertion
TestName -> Bool -> Assertion
assertBool TestName
"Stream A missing from cursors" (StreamId -> Map StreamId (Cursor backend) -> Bool
forall k a. Ord k => k -> Map k a -> Bool
Map.member StreamId
streamA Map StreamId (Cursor backend)
streamCursors)
            HasCallStack => TestName -> Bool -> Assertion
TestName -> Bool -> Assertion
assertBool TestName
"Stream B missing from cursors" (StreamId -> Map StreamId (Cursor backend) -> Bool
forall k a. Ord k => k -> Map k a -> Bool
Map.member StreamId
streamB Map StreamId (Cursor backend)
streamCursors)
            HasCallStack => TestName -> Bool -> Assertion
TestName -> Bool -> Assertion
assertBool TestName
"Stream C missing from cursors" (StreamId -> Map StreamId (Cursor backend) -> Bool
forall k a. Ord k => k -> Map k a -> Bool
Map.member StreamId
streamC Map StreamId (Cursor backend)
streamCursors)

            -- Verify ordering invariant: all stream cursors <= finalCursor
            [(StreamId, Cursor backend)]
-> ((StreamId, Cursor backend) -> Assertion) -> Assertion
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
t a -> (a -> m b) -> m ()
forM_ (Map StreamId (Cursor backend) -> [(StreamId, Cursor backend)]
forall k a. Map k a -> [(k, a)]
Map.toList Map StreamId (Cursor backend)
streamCursors) (((StreamId, Cursor backend) -> Assertion) -> Assertion)
-> ((StreamId, Cursor backend) -> Assertion) -> Assertion
forall a b. (a -> b) -> a -> b
$ \(StreamId
sid, Cursor backend
cursor) ->
                HasCallStack => TestName -> Bool -> Assertion
TestName -> Bool -> Assertion
assertBool
                    (TestName
"Cursor for " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ StreamId -> TestName
forall a. Show a => a -> TestName
show StreamId
sid TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ TestName
" violates ordering invariant")
                    (Cursor backend
cursor Cursor backend -> Cursor backend -> Bool
forall a. Ord a => a -> a -> Bool
<= Cursor backend
finalCursor)

            -- Verify we can actually use each cursor
            case (StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamA Map StreamId (Cursor backend)
streamCursors, StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamB Map StreamId (Cursor backend)
streamCursors, StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamC Map StreamId (Cursor backend)
streamCursors) of
                (Just Cursor backend
cursorA, Just Cursor backend
cursorB, Just Cursor backend
cursorC) -> do
                    -- Try using each cursor
                    resultA <- BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$ StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamA (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorA) (Int -> SomeLatestEvent
makeUserEvent Int
3)
                    resultB <- insertEvents store Nothing $ singleEvent streamB (ExactVersion cursorB) (makeUserEvent 11)
                    resultC <- insertEvents store Nothing $ singleEvent streamC (ExactVersion cursorC) (makeUserEvent 103)

                    case (resultA, resultB, resultC) of
                        (SuccessfulInsertion InsertionSuccess backend
_, SuccessfulInsertion InsertionSuccess backend
_, SuccessfulInsertion InsertionSuccess backend
_) -> () -> Assertion
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
                        (InsertionResult backend, InsertionResult backend,
 InsertionResult backend)
_ -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure TestName
"One or more stream cursors were not usable"
                (Maybe (Cursor backend), Maybe (Cursor backend),
 Maybe (Cursor backend))
_ -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure TestName
"Failed to extract all cursors from map"

{- | Test that streams with EMPTY event lists still get proper cursor handling
in multi-stream transactions. This is a critical edge case - implementations
might fail to track cursors for streams that have no events written.

The user specifically flagged this: "Check that we get the proper cursor event
for EMPTY stream writes - that's the kind of places where nasty stupid bugs may lie."
-}
testEmptyStreamCursorHandling :: forall backend. (EventStore backend, StoreConstraints backend IO, Show (Cursor backend)) => BackendHandle backend -> IO ()
testEmptyStreamCursorHandling :: forall backend.
(EventStore backend, StoreConstraints backend IO,
 Show (Cursor backend)) =>
BackendHandle backend -> Assertion
testEmptyStreamCursorHandling BackendHandle backend
store = do
    streamA <- UUID -> StreamId
StreamId (UUID -> StreamId) -> IO UUID -> IO StreamId
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO UUID
UUID.nextRandom
    streamB <- StreamId <$> UUID.nextRandom
    streamC <- StreamId <$> UUID.nextRandom

    -- Multi-stream transaction with one EMPTY stream (streamB has no events)
    result <-
        insertEvents store Nothing $
            Transaction
                ( Map.fromList
                    [ (streamA, StreamWrite NoStream [makeUserEvent 1])
                    , (streamB, StreamWrite NoStream []) -- EMPTY! This is the edge case
                    , (streamC, StreamWrite NoStream [makeUserEvent 100])
                    ]
                )

    case result of
        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Multi-stream with empty stream failed: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
        SuccessfulInsertion (InsertionSuccess{Map StreamId (Cursor backend)
streamCursors :: forall backend.
InsertionSuccess backend -> Map StreamId (Cursor backend)
streamCursors :: Map StreamId (Cursor backend)
streamCursors}) -> do
            -- Critical question: Does streamB get a cursor even though it has no events?
            -- Behavior expectation: streamB should NOT appear in streamCursors because
            -- it didn't actually write any events. Only streams with events get cursors.

            -- Verify streams A and C have cursors (they have events)
            HasCallStack => TestName -> Bool -> Assertion
TestName -> Bool -> Assertion
assertBool TestName
"Stream A should have cursor (has events)" (StreamId -> Map StreamId (Cursor backend) -> Bool
forall k a. Ord k => k -> Map k a -> Bool
Map.member StreamId
streamA Map StreamId (Cursor backend)
streamCursors)
            HasCallStack => TestName -> Bool -> Assertion
TestName -> Bool -> Assertion
assertBool TestName
"Stream C should have cursor (has events)" (StreamId -> Map StreamId (Cursor backend) -> Bool
forall k a. Ord k => k -> Map k a -> Bool
Map.member StreamId
streamC Map StreamId (Cursor backend)
streamCursors)

            -- Verify streamB does NOT have cursor (no events written)
            -- An empty stream write is effectively a no-op
            case StreamId -> Map StreamId (Cursor backend) -> Maybe (Cursor backend)
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup StreamId
streamB Map StreamId (Cursor backend)
streamCursors of
                Maybe (Cursor backend)
Nothing -> do
                    -- Expected: streamB shouldn't get a cursor for empty write
                    -- Verify streamB stream doesn't exist (empty write is a no-op)
                    resultB <-
                        BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                            StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamB ExpectedVersion backend
forall backend. ExpectedVersion backend
NoStream (Int -> SomeLatestEvent
makeUserEvent Int
10)

                    case resultB of
                        FailedInsertion EventStoreError backend
err -> TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"Stream B should not exist after empty write: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
                        SuccessfulInsertion InsertionSuccess backend
_ -> () -> Assertion
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure () -- Good! StreamB was never created
                Just Cursor backend
cursorB -> do
                    -- Alternative behavior: backend gave cursor for empty stream
                    -- In this case, verify the cursor is still usable and stream was created
                    resultB <-
                        BackendHandle backend
-> Maybe CorrelationId
-> Transaction [] backend
-> IO (InsertionResult backend)
forall backend (t :: * -> *) (m :: * -> *).
(EventStore backend, Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
forall (t :: * -> *) (m :: * -> *).
(Traversable t, StoreConstraints backend m) =>
BackendHandle backend
-> Maybe CorrelationId
-> Transaction t backend
-> m (InsertionResult backend)
insertEvents BackendHandle backend
store Maybe CorrelationId
forall a. Maybe a
Nothing (Transaction [] backend -> IO (InsertionResult backend))
-> Transaction [] backend -> IO (InsertionResult backend)
forall a b. (a -> b) -> a -> b
$
                            StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
forall backend.
StreamId
-> ExpectedVersion backend
-> SomeLatestEvent
-> Transaction [] backend
singleEvent StreamId
streamB (Cursor backend -> ExpectedVersion backend
forall backend. Cursor backend -> ExpectedVersion backend
ExactVersion Cursor backend
cursorB) (Int -> SomeLatestEvent
makeUserEvent Int
10)

                    case resultB of
                        FailedInsertion EventStoreError backend
err ->
                            TestName -> Assertion
forall a. HasCallStack => TestName -> IO a
assertFailure (TestName -> Assertion) -> TestName -> Assertion
forall a b. (a -> b) -> a -> b
$ TestName
"If empty stream gets cursor, it should be usable: " TestName -> TestName -> TestName
forall a. [a] -> [a] -> [a]
++ EventStoreError backend -> TestName
forall a. Show a => a -> TestName
show EventStoreError backend
err
                        SuccessfulInsertion InsertionSuccess backend
_ -> () -> Assertion
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure () -- Alternative behavior is OK if consistent