{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-----------------------------------------------------------------------------
    reactive-banana
------------------------------------------------------------------------------}
-- | Property tests for 'GraphGC'.
module Reactive.Banana.Test.Low.GraphGC
    ( tests
    ) where

import Control.Monad
    ( when )
import Control.Monad.IO.Class
    ( liftIO )
import Data.Map.Strict
    ( Map )
import Data.Unique.Really
    ( Unique )
import Reactive.Banana.Prim.Low.Graph 
    ( Graph )
import Reactive.Banana.Prim.Low.GraphGC
    ( GraphGC )
import Reactive.Banana.Test.Low.Gen
    ( DeltaGraph (..), TestGraph (..), Vertex )
import Test.QuickCheck
    ( Gen, Property, (===), (=/=) )
import Test.Tasty
    ( testGroup, TestTree )
import Test.Tasty.QuickCheck
    ( testProperty )

import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Set as Set

import qualified Control.DeepSeq as Memory
import qualified Control.Exception as Memory
import qualified System.Mem as System
import qualified Control.Concurrent as System

import qualified Test.QuickCheck as Q
import qualified Test.QuickCheck.Monadic as Q
import qualified Reactive.Banana.Test.Low.Graph as Q
import qualified Reactive.Banana.Test.Low.Gen as Q

import qualified Reactive.Banana.Prim.Low.Graph as Graph
import qualified Reactive.Banana.Prim.Low.GraphGC as GraphGC
import qualified Reactive.Banana.Prim.Low.Ref as Ref


tests :: TestTree
tests = testGroup "GraphGC"
    [ testGroup "Garbage collection (GC)"
        [ testProperty "retains the reachable vertices" prop_performGC
        , testProperty "not doing GC retains all vertices" prop_notPerformGC
        ]
    ]

{-----------------------------------------------------------------------------
    Properties
------------------------------------------------------------------------------}
prop_performGC :: Property
prop_performGC =
    Q.forAll Q.genTestGraph
    $ \g0 -> Q.forAll (genGarbageCollectionRoots g0)
    $ \roots ->
    let g = Q.mkGraph g0
        expected = Graph.collectGarbage roots g
    in  Q.cover 10 (Graph.size g == Graph.size expected)
            "no   vertices unreachable"
        $ Q.cover 75 (Graph.size g > Graph.size expected)
            "some vertices unreachable"
        $ Q.cover 15 (Graph.size g > 2*Graph.size expected)
            "many vertices unreachable"
        $ Q.monadicIO $ liftIO $ do
            (actual, vertices) <- mkGraphGC g0
            let rootRefs = map (vertices Map.!) roots
            Memory.evaluate $ Memory.rnf rootRefs

            System.performMajorGC
            GraphGC.removeGarbage actual
            reachables <- traverse Ref.read =<<
                GraphGC.listReachableVertices actual

            -- keep rootsRef reachable until this point
            rootsFromRef <- traverse Ref.read rootRefs

            pure $
                ( roots === rootsFromRef )
                Q..&&.
                ( Set.fromList (Graph.listConnectedVertices expected)
                    === Set.fromList reachables
                )

prop_notPerformGC :: Property
prop_notPerformGC =
    Q.forAll Q.genSquareSide
    $ \n -> Q.monadicIO $ liftIO $ do
        -- Trigger a garbage collection now so that it is
        -- highly unlikely to happen in the subsequent lines
        System.performMinorGC

        let g = Q.mkLinearChain n

        (actual, _) <- mkGraphGC g
        GraphGC.removeGarbage actual
        reachables <- traverse Ref.read =<<
            GraphGC.listReachableVertices actual

        pure $
            Set.fromList reachables === Set.fromList [1..n]

{-----------------------------------------------------------------------------
    Test graphs
------------------------------------------------------------------------------}
-- | Generate a 'GraphGC' from a 'TestGraph'.
mkGraphGC :: TestGraph -> IO (GraphGC Vertex, Map Vertex (Ref.Ref Vertex))
mkGraphGC TestGraph{vertices,edges} = do
    g <- GraphGC.new
    refMap <- Map.fromList . zip vertices <$> traverse Ref.new vertices
    let insertEdge (InsertEdge x y) = do
            GraphGC.insertEdge (refMap Map.! x, refMap Map.! y) g
    traverse insertEdge edges
    pure (g, refMap)

-- | Randomly generate a set of garbage collection roots.
genGarbageCollectionRoots :: TestGraph -> Gen [Vertex]
genGarbageCollectionRoots TestGraph{vertices} = Q.sized $ \n ->
    sequence . replicate (n `mod` 10) $ Q.elements vertices
