packages feed

pgmq-hasql-0.1.0.0: test/AdvancedOpsSpec.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Tests for advanced message operations:
-- - pop with qty
-- - batch set_vt
-- - read_with_poll
-- - FIFO index functions
-- - read_grouped functions
module AdvancedOpsSpec (tests) where

import Control.Concurrent (threadDelay)
import Data.Aeson (object, (.=))
import Data.Time.Clock (addUTCTime, getCurrentTime)
import Data.Vector qualified as V
import EphemeralDb (TestFixture (..), withTestFixture)
import Hasql.Pool qualified as Pool
import Pgmq.Hasql.Sessions qualified as Sessions
import Pgmq.Hasql.Statements.Types
  ( BatchMessageQuery (..),
    BatchSendMessage (..),
    BatchSendMessageWithHeaders (..),
    BatchVisibilityTimeoutAtQuery (..),
    BatchVisibilityTimeoutQuery (..),
    PopMessage (..),
    ReadGrouped (..),
    ReadGroupedWithPoll (..),
    ReadMessage (..),
    ReadWithPollMessage (..),
    SendMessage (..),
    VisibilityTimeoutAtQuery (..),
  )
import Pgmq.Types (MessageBody (..), MessageHeaders (..), MessageId (..))
import Pgmq.Types qualified as PgmqTypes
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, testCase)
import TestUtils (assertSession, cleanupQueue)

-- | All advanced operation tests
tests :: Pool.Pool -> TestTree
tests p =
  testGroup
    "Advanced Operations"
    [ testPopSingle p,
      testPopBatch p,
      testBatchChangeVisibilityTimeout p,
      testSetVisibilityTimeoutAt p,
      testBatchSetVisibilityTimeoutAt p,
      testReadWithPoll p,
      testReadWithPollEmpty p,
      testCreateFifoIndex p,
      testReadGrouped p,
      testReadGroupedRoundRobin p
    ]

-- | Test pop with default qty (single message)
testPopSingle :: Pool.Pool -> TestTree
testPopSingle p = testCase "pop returns and deletes single message" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send messages
    let msgs =
          BatchSendMessage
            { queueName = queueName,
              messageBodies =
                [ MessageBody (object ["pop" .= (1 :: Int)]),
                  MessageBody (object ["pop" .= (2 :: Int)])
                ],
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.batchSendMessage msgs)
    -- Pop single message
    let popQuery = PopMessage {queueName = queueName, qty = Just 1}
    popped <- assertSession pool (Sessions.pop popQuery)
    assertEqual "Should pop 1 message" 1 (V.length popped)
    -- Verify only 1 message remains
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 10,
              conditional = Nothing
            }
    remaining <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should have 1 message remaining" 1 (V.length remaining)
    cleanupQueue pool queueName

-- | Test pop with qty > 1 (batch pop)
testPopBatch :: Pool.Pool -> TestTree
testPopBatch p = testCase "pop with qty > 1 returns multiple messages" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send 5 messages
    let msgs =
          BatchSendMessage
            { queueName = queueName,
              messageBodies = [MessageBody (object ["pop" .= i]) | i <- [1 .. 5 :: Int]],
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.batchSendMessage msgs)
    -- Pop 3 messages
    let popQuery = PopMessage {queueName = queueName, qty = Just 3}
    popped <- assertSession pool (Sessions.pop popQuery)
    assertEqual "Should pop 3 messages" 3 (V.length popped)
    -- Verify 2 messages remain
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 10,
              conditional = Nothing
            }
    remaining <- assertSession pool (Sessions.readMessage readQuery)
    assertEqual "Should have 2 messages remaining" 2 (V.length remaining)
    cleanupQueue pool queueName

-- | Test batchChangeVisibilityTimeout
testBatchChangeVisibilityTimeout :: Pool.Pool -> TestTree
testBatchChangeVisibilityTimeout p = testCase "batchChangeVisibilityTimeout updates multiple messages" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send messages
    let msgs =
          BatchSendMessage
            { queueName = queueName,
              messageBodies =
                [ MessageBody (object ["vt" .= (1 :: Int)]),
                  MessageBody (object ["vt" .= (2 :: Int)]),
                  MessageBody (object ["vt" .= (3 :: Int)])
                ],
              delay = Nothing
            }
    msgIds <- assertSession pool (Sessions.batchSendMessage msgs)
    -- Read messages to set VT
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 5,
              batchSize = Just 10,
              conditional = Nothing
            }
    _ <- assertSession pool (Sessions.readMessage readQuery)
    -- Change VT for all messages
    let vtQuery =
          BatchVisibilityTimeoutQuery
            { queueName = queueName,
              messageIds = msgIds,
              visibilityTimeoutOffset = 60
            }
    updated <- assertSession pool (Sessions.batchChangeVisibilityTimeout vtQuery)
    assertEqual "Should update 3 messages" 3 (V.length updated)
    cleanupQueue pool queueName

-- | Test setVisibilityTimeoutAt (pgmq 1.10.0+)
testSetVisibilityTimeoutAt :: Pool.Pool -> TestTree
testSetVisibilityTimeoutAt p = testCase "setVisibilityTimeoutAt sets VT to absolute timestamp" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send a message
    let msg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["vt_at" .= ("test" :: String)]),
              delay = Nothing
            }
    msgId <- assertSession pool (Sessions.sendMessage msg)
    -- Read message to set initial VT
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 5,
              batchSize = Just 1,
              conditional = Nothing
            }
    _ <- assertSession pool (Sessions.readMessage readQuery)
    -- Set VT to 60 seconds in the future using absolute timestamp
    futureTime <- addUTCTime 60 <$> getCurrentTime
    let vtQuery =
          VisibilityTimeoutAtQuery
            { queueName = queueName,
              messageId = msgId,
              visibilityTime = futureTime
            }
    updated <- assertSession pool (Sessions.setVisibilityTimeoutAt vtQuery)
    assertEqual "Should return the updated message" msgId (PgmqTypes.messageId updated)
    cleanupQueue pool queueName

-- | Test batchSetVisibilityTimeoutAt (pgmq 1.10.0+)
testBatchSetVisibilityTimeoutAt :: Pool.Pool -> TestTree
testBatchSetVisibilityTimeoutAt p = testCase "batchSetVisibilityTimeoutAt sets VT for multiple messages" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send messages
    let msgs =
          BatchSendMessage
            { queueName = queueName,
              messageBodies =
                [ MessageBody (object ["vt_at" .= (1 :: Int)]),
                  MessageBody (object ["vt_at" .= (2 :: Int)]),
                  MessageBody (object ["vt_at" .= (3 :: Int)])
                ],
              delay = Nothing
            }
    msgIds <- assertSession pool (Sessions.batchSendMessage msgs)
    -- Read messages to set initial VT
    let readQuery =
          ReadMessage
            { queueName = queueName,
              delay = 5,
              batchSize = Just 10,
              conditional = Nothing
            }
    _ <- assertSession pool (Sessions.readMessage readQuery)
    -- Set VT to 120 seconds in the future using absolute timestamp
    futureTime <- addUTCTime 120 <$> getCurrentTime
    let vtQuery =
          BatchVisibilityTimeoutAtQuery
            { queueName = queueName,
              messageIds = msgIds,
              visibilityTime = futureTime
            }
    updated <- assertSession pool (Sessions.batchSetVisibilityTimeoutAt vtQuery)
    assertEqual "Should update 3 messages" 3 (V.length updated)
    cleanupQueue pool queueName

-- | Test readWithPoll - messages available immediately
testReadWithPoll :: Pool.Pool -> TestTree
testReadWithPoll p = testCase "readWithPoll returns messages immediately when available" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Send a message
    let msg =
          SendMessage
            { queueName = queueName,
              messageBody = MessageBody (object ["poll" .= ("test" :: String)]),
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.sendMessage msg)
    -- Poll for messages
    let pollQuery =
          ReadWithPollMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              maxPollSeconds = 5,
              pollIntervalMs = 100,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readWithPoll pollQuery)
    assertEqual "Should read 1 message" 1 (V.length messages)
    cleanupQueue pool queueName

-- | Test readWithPoll - empty queue with short timeout
testReadWithPollEmpty :: Pool.Pool -> TestTree
testReadWithPollEmpty p = testCase "readWithPoll returns empty when queue is empty" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Poll on empty queue with short timeout
    let pollQuery =
          ReadWithPollMessage
            { queueName = queueName,
              delay = 30,
              batchSize = Just 1,
              maxPollSeconds = 1, -- Short timeout
              pollIntervalMs = 100,
              conditional = Nothing
            }
    messages <- assertSession pool (Sessions.readWithPoll pollQuery)
    assertEqual "Should return empty" 0 (V.length messages)
    cleanupQueue pool queueName

-- | Test createFifoIndex
testCreateFifoIndex :: Pool.Pool -> TestTree
testCreateFifoIndex p = testCase "createFifoIndex creates GIN index on headers" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Create FIFO index
    assertSession pool (Sessions.createFifoIndex queueName)
    -- Verify by sending a message with headers (index should be used)
    let msg =
          BatchSendMessageWithHeaders
            { queueName = queueName,
              messageBodies = [MessageBody (object ["fifo" .= ("test" :: String)])],
              messageHeaders = [MessageHeaders (object ["x-pgmq-group" .= ("group1" :: String)])],
              delay = Nothing
            }
    msgIds <- assertSession pool (Sessions.batchSendMessageWithHeaders msg)
    assertEqual "Should send 1 message" 1 (length msgIds)
    cleanupQueue pool queueName

-- | Test readGrouped - fills batch from same message group
testReadGrouped :: Pool.Pool -> TestTree
testReadGrouped p = testCase "readGrouped fills batch from same message group" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Create FIFO index for better performance
    assertSession pool (Sessions.createFifoIndex queueName)
    -- Send messages from multiple groups
    let msgs =
          BatchSendMessageWithHeaders
            { queueName = queueName,
              messageBodies =
                [ MessageBody (object ["msg" .= (1 :: Int)]),
                  MessageBody (object ["msg" .= (2 :: Int)]),
                  MessageBody (object ["msg" .= (3 :: Int)]),
                  MessageBody (object ["msg" .= (4 :: Int)])
                ],
              messageHeaders =
                [ MessageHeaders (object ["x-pgmq-group" .= ("groupA" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupA" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupB" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupB" :: String)])
                ],
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.batchSendMessageWithHeaders msgs)
    -- Read grouped - should prefer filling from one group
    let readQuery =
          ReadGrouped
            { queueName = queueName,
              visibilityTimeout = 30,
              qty = 2
            }
    messages <- assertSession pool (Sessions.readGrouped readQuery)
    assertEqual "Should read 2 messages" 2 (V.length messages)
    -- Delete the messages
    let msgIds = V.toList $ V.map PgmqTypes.messageId messages
    _ <-
      assertSession pool $
        Sessions.batchDeleteMessages
          BatchMessageQuery {queueName = queueName, messageIds = msgIds}
    cleanupQueue pool queueName

-- | Test readGroupedRoundRobin - interleaves across groups
testReadGroupedRoundRobin :: Pool.Pool -> TestTree
testReadGroupedRoundRobin p = testCase "readGroupedRoundRobin interleaves across groups" $ do
  withTestFixture p $ \TestFixture {pool, queueName} -> do
    assertSession pool (Sessions.createQueue queueName)
    -- Create FIFO index
    assertSession pool (Sessions.createFifoIndex queueName)
    -- Send messages from 3 groups
    let msgs =
          BatchSendMessageWithHeaders
            { queueName = queueName,
              messageBodies =
                [ MessageBody (object ["msg" .= (1 :: Int)]),
                  MessageBody (object ["msg" .= (2 :: Int)]),
                  MessageBody (object ["msg" .= (3 :: Int)]),
                  MessageBody (object ["msg" .= (4 :: Int)]),
                  MessageBody (object ["msg" .= (5 :: Int)]),
                  MessageBody (object ["msg" .= (6 :: Int)])
                ],
              messageHeaders =
                [ MessageHeaders (object ["x-pgmq-group" .= ("groupA" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupB" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupC" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupA" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupB" :: String)]),
                  MessageHeaders (object ["x-pgmq-group" .= ("groupC" :: String)])
                ],
              delay = Nothing
            }
    _ <- assertSession pool (Sessions.batchSendMessageWithHeaders msgs)
    -- Read round-robin - should interleave groups
    let readQuery =
          ReadGrouped
            { queueName = queueName,
              visibilityTimeout = 30,
              qty = 3
            }
    messages <- assertSession pool (Sessions.readGroupedRoundRobin readQuery)
    assertEqual "Should read 3 messages" 3 (V.length messages)
    -- Clean up
    let msgIds = V.toList $ V.map PgmqTypes.messageId messages
    _ <-
      assertSession pool $
        Sessions.batchDeleteMessages
          BatchMessageQuery {queueName = queueName, messageIds = msgIds}
    cleanupQueue pool queueName