{-# LANGUAGE OverloadedStrings #-}
module Data.CQRS.Test.Internal.RepositoryTest
    ( mkRepositorySpec
    ) where

import           Control.DeepSeq (NFData)
import           Control.Monad (forM_, liftM)
import           Control.Monad.Trans.Reader (ask)
import           Control.Monad.IO.Class (liftIO)
import           Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString as B
import           Data.CQRS.Command (CommandT)
import qualified Data.CQRS.Command as C
import           Data.CQRS.Query
import           Data.CQRS.Repository
import           Data.CQRS.Types.EventStore (EventStore)
import           Data.CQRS.Types.SnapshotStore (nullSnapshotStore, SnapshotStore)
import           Data.CQRS.Test.Internal.AggregateAction (byteStringAggregateAction)
import           Data.CQRS.Test.Internal.Scope (ScopeM, verify, randomUUID, mkRunScope)
import           Data.CQRS.Test.Internal.TestKitSettings
import           Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef')
import           Data.Maybe (fromJust)
import           Data.UUID.Types (UUID)
import qualified System.Random as R
import qualified Test.Hspec as Hspec
import           Test.Hspec (Spec, shouldBe)
import           Test.HUnit (assertBool)

-- Ambient data for test scope for each spec.
data Scope a e = Scope { scopeRepository :: Repository a e
                       , scopePublishedEvents :: IORef [(UUID, e, Int)]
                       }

-- Assert that the given list of events was published.
assertDidPublish :: (Show e, Eq e) => [(UUID, e, Int)] -> ScopeM (Scope a e) ()
assertDidPublish expectedEvents = do
  publishedEventsRef <- fmap scopePublishedEvents $ ask
  verify $ do
    publishedEvents <- readIORef publishedEventsRef
    length publishedEvents `shouldBe` length expectedEvents
    forM_ (zip publishedEvents expectedEvents) $ uncurry shouldBe

-- Get the repository which is in scope.
getRepository :: ScopeM (Scope a e) (Repository a e)
getRepository = fmap scopeRepository ask

-- Run a command in scope.
runCommandT :: CommandT a e IO r -> ScopeM (Scope a e) r
runCommandT command = do
  repository <- getRepository
  liftIO $ C.runCommandT repository command

-- Create a new test scope runner from the test kit settings.
mkRunScope' :: Int -> TestKitSettings s (EventStore ByteString, SnapshotStore ByteString) -> (ScopeM (Scope ByteString ByteString) r -> IO r)
mkRunScope' snapshotFrequency testKitSettings = mkRunScope testKitSettings $ \a -> do
  -- We collect all events published by the repository for verification
  publishedEventsRef <- newIORef []
  let publish (aggregateId, events') = atomicModifyIORef' publishedEventsRef $ \events ->
        (events ++ map (\(PersistedEvent e s _) -> (aggregateId, e, s)) events', ())
  -- Repository setup
  (eventStore, snapshotStore) <- (tksMakeContext testKitSettings) a
  let settings = setSnapshotFrequency snapshotFrequency $ defaultSettings
  let repository = newRepository settings byteStringAggregateAction eventStore snapshotStore publish R.randomIO
  -- Build the ambient state.
  return $ Scope repository publishedEventsRef

-- Given test kit settings, create the full spec for testing the
-- repository implementation against those settings.
mkRepositorySpec :: TestKitSettings a (EventStore ByteString, SnapshotStore ByteString) -> Spec
mkRepositorySpec testKitSettings = do
  -- We do each set of tests both *with* and *without* a snapshot
  -- store and with varying snapshot frequency. This should hopefully
  -- give us enough coverage against the handling of snapshots in the
  -- Repository portion of the code.
  forM_ [ 3, 5 ] $ \f -> do
    let fs = "frequency " ++ show f
    let s1 = "(snapshots; " ++ fs ++ ")"
    let s2 = "(null snapshots; " ++ fs ++ ")"
    mkSpec s1 (mkRunScope' f $ testKitSettings)
    mkSpec s2 (mkRunScope' f $ disableSnapshots testKitSettings)
  where
    disableSnapshots settings =
        settings { tksMakeContext = \a -> do
                        (eventStore, _) <- tksMakeContext settings a
                        return (eventStore, nullSnapshotStore)
                 }

mkSpec :: String -> (ScopeM (Scope ByteString ByteString) () -> IO ()) -> Spec
mkSpec suffix runScope = do

  describe "Repository" $ do

    it "should support creating an aggregate and returning its value" $ do
      -- Exercise
      (aggregateId, a) <- newAggregate ["3"]
      -- Should have updated aggregate value
      verify $ a `shouldBe` "3"
      -- Should have published appropriate event
      assertDidPublish [ (aggregateId, "3", 0) ]

    it "should support creating an aggregate and loading it" $ do
      -- Exercise
      (aggregateId, _) <- newAggregate ["4"]
      -- Should have an updated aggregate value
      a <- loadAggregate aggregateId
      verify $ a `shouldBe` "4"
      -- Should have published appropriate event
      assertDidPublish [ (aggregateId, "4", 0) ]

    it "should support publishing >1 events to an aggregate (1 txn)" $ do
      -- Exercise
      (aggregateId, _) <- newAggregate ["7", "1"]
      -- Should have an updated aggregate value
      a <- loadAggregate aggregateId
      verify $ a `shouldBe` "71"
      -- Should have published two events
      assertDidPublish [ (aggregateId, "7", 0)
                       , (aggregateId, "1", 1)
                       ]

    it "should support publishing >1 events to an aggregate (2 txns)" $ do
      -- Exercise: 1st transaction
      (aggregateId, _) <- newAggregate ["9"]
      -- Exercise: 2nd transaction
      _ <- runCommandT $ do
        C.updateAggregate aggregateId $ \_ -> do
          C.publishEvent $ "7"
      -- Should have an updated aggregate value.
      a <- loadAggregate aggregateId
      verify $ a `shouldBe` "97"
      -- Should have published two events.
      assertDidPublish [ (aggregateId, "9", 0)
                       , (aggregateId, "7", 1)
                       ]

    it "should support publishing a large number of events to an aggregate" $ do
      -- Setup
      let events = map B8.pack $ map show ([1.. 100] :: [Int])
      -- Exercise
      (aggregateId, _) <- newAggregate events
      -- Verify
      a <- loadAggregate aggregateId
      verify $ a `shouldBe` B.concat events

    it "should be possible to find an existing aggregate" $ do
      -- Setup
      (aggregateId, _) <- newAggregate ["xyzzy"]
      -- Exercise
      a <- findAggregate aggregateId
      -- Should have found it
      verify $ a `shouldBe` Just "xyzzy"

    it "should not be possible to find a non-existent aggregate" $ do
      -- Exercise
      aggregateId <- randomUUID
      a <- findAggregate aggregateId
      -- Should NOT have found anything
      verify $ a `shouldBe` Nothing

    it "should be possible to work with two different aggregates (serially) in a command" $ do
      -- Setup
      aggregateId0 <- randomUUID
      aggregateId1 <- randomUUID
      -- Exercise
      _ <- runCommandT $ do
        C.createAggregate aggregateId0 $ \_ -> do
          C.publishEvent "34"
        C.createAggregate aggregateId1 $ \_ -> do
          C.publishEvent "1"
        C.updateAggregate aggregateId0 $ \_ -> do
          C.publishEvent "5"
      -- Should have updated values for both aggregates
      a0 <- loadAggregate aggregateId0
      a1 <- loadAggregate aggregateId1
      verify $ a0 `shouldBe` "345"
      verify $ a1 `shouldBe` "1"
      -- Should have published events in order of publishing
      assertDidPublish [ (aggregateId0, "34", 0)
                       , (aggregateId1,  "1", 0)
                       , (aggregateId0,  "5", 1)
                       ]

    it "'getter' function returns up-to-date values when updating an aggregate" $ do
      -- Setup
      (aggregateId, _) <- newAggregate ["x"]
      -- Exercise:
      Just (a, a') <- runCommandT $ do -- We'll assume pattern match will work, otherwise test fails
        C.updateAggregate aggregateId $ \get -> do
          a <- get
          C.publishEvent "y"
          a' <- get
          return (a, a')
      -- Should have received original value in a'
      verify $ a `shouldBe` "x"
      -- Should have received an updated value in a''
      verify $ a' `shouldBe` "xy"
      -- Should have published events
      assertDidPublish [ (aggregateId, "x", 0)
                       , (aggregateId, "y", 1)
                       ]

    it "'getter' function returns up-to-date values when creating an aggregate" $ do
      -- Setup
      aggregateId <- randomUUID
      -- Exercise
      (a, a') <- runCommandT $ do
        C.createAggregate aggregateId $ \get -> do
          a <- get  -- Should return Nothing
          C.publishEvent "x"
          a' <- get  -- Should reutrn (Just "x")
          return (a, a')
      -- Should have received Nothing in a' since aggregate didn't actually exist
      verify $ a `shouldBe` Nothing
      -- Should have received (Just "x") in a'' since we'd just published an "x" event
      verify $ a' `shouldBe` (Just "x")
      -- Should have published event
      assertDidPublish [ (aggregateId, "x", 0) ]

  where
    -- Boilerplate avoidance
    it msg scope = Hspec.it msg $ runScope scope
    describe msg = Hspec.describe (msg ++ " " ++ suffix)

-- Create an aggregate with an initial series of events.
newAggregate :: (NFData a, NFData e) => [e] -> ScopeM (Scope a e) (UUID, a)
newAggregate es = do
  -- Create new aggregate ID.
  aggregateId <- randomUUID
  -- Sanity check
  liftIO $ assertBool "List of initial events must be non-emtpy" (length es > 0)
  -- Create the aggregate with its initial list of events
  runCommandT $ do
    a <- C.createAggregate aggregateId $ \getAggregate -> do
      forM_ es $ C.publishEvent
      liftM fromJust $ getAggregate
    return (aggregateId, a)

-- Load an aggregate value.
loadAggregate :: UUID -> ScopeM (Scope a e) a
loadAggregate aggregateId = liftM get $ runCommandT $ C.readAggregate aggregateId
    where
      get Nothing  = error $ "loadAggregate: Missing expected aggregate"
      get (Just a) = a

-- Get aggregate's value, if the aggregate exists
findAggregate :: UUID -> ScopeM (Scope a e) (Maybe a)
findAggregate = do
  runCommandT . (flip C.updateAggregate) id