{-# LANGUAGE FlexibleInstances #-}
module OpTest where

import qualified Prelude
import Prelude hiding ( lookup, last )

import Control.Applicative
import Control.Monad
import Control.Monad.Exception.Synchronous

import Data.Cache.LRU.Internal

import Test.QuickCheck
    ( Arbitrary(..)
    , Args(..)
    , Gen
    , choose
    , oneof
    , shrinkNothing
    , quickCheckWith
    , stdArgs
    )
import Test.QuickCheck.Property ( Result(..), result, succeeded )

data Action key val = Insert key val
                    | Lookup key
                    | Delete key
                    | Pop
                      deriving (Show, Eq)

instance Arbitrary (Action Int Int) where
    arbitrary = oneof [ins, look, del, pop]
        where ins = liftM2 Insert key $ choose (100, 104)
              look = liftM Lookup key
              del = liftM Delete key
              pop = return Pop
              key = choose (1, 10)

    shrink = shrinkNothing

newtype History key val = H ( Maybe Integer
                            , [Action key val] -> [Action key val]
                            )

instance Arbitrary (History Int Int) where
    arbitrary = liftM2 (curry H) s h
        where s = liftM2 (<$) (choose (1, 5)) (arbitrary :: Gen (Maybe ()))
              h = liftM (++) arbitrary

    shrink (H (k, h)) = map (H . (,) k . (++)) . drops . h $ []
        where drops [] = []
              drops (x:xs) = xs:[x:ys | ys <- drops xs]

instance (Show key, Show val) => Show (History key val) where
    show (H (k, h)) = show (k, h [])

execute :: (Ord key, Eq val, Show key, Show val) => History key val
        -> Exceptional String (LRU key val)
execute (H (k, h)) = execute' (h []) (newLRU k)
    where
      execute' [] lru = return lru
      execute' (x:xs) lru = executeA x lru >>= execute' xs

      execA' key val lru lru' = do
        when (not . valid $ lru') $ throw "not valid"

        let pre = toList lru
            post = toList lru'

            naive = (key, val) : filter ((key /=) . fst) pre
            sizeOk = maybe True (fromIntegral (length naive) <=) k
            projected = if sizeOk then naive else init naive
        when (projected /= post) $ throw "unexpected result"

        return lru'

      executeA (Delete key) lru = do
        let (lru', removed) = delete key lru
        when (not . valid $ lru') $ throw "not valid"

        let pre = toList lru
            post = toList lru'
            projected = filter ((key /=) . fst) pre
            expectedRemoval = Prelude.lookup key pre

        when (removed /= expectedRemoval) $ throw "unexpected value removed"
        when (projected /= post) $ throw "unexpected resulting lru"
        return lru'

      executeA (Insert key val) lru = execA' key val lru $ insert key val lru

      executeA (Lookup key) lru = case mVal of
                                    Nothing -> checkSame
                                    Just val -> execA' key val lru lru'
          where (lru', mVal) = lookup key lru
                checkSame = do when (toList lru /= toList lru') $
                                    throw "unexpected result"
                               return lru'

      executeA Pop lru = do
        let (lru', popped) = pop lru
        when (not . valid $ lru') $ throw "not valid"

        let pre = toList lru
            post = toList lru'

            (ePost, ePopped) = case pre of
                                 [] -> ([], Nothing)
                                 _  -> (init pre, Just $ Prelude.last pre)

        when (post /= ePost) $ throw "unexpected result lru"
        when (popped /= ePopped) $ throw "unexpected result key-value"
        return lru'


executesProperly :: History Int Int -> Result
executesProperly h = case execute h of
                       Success _ -> succeeded
                       Exception e -> result { ok = Just False
                                             , reason = e
                                             }

main :: IO ()
main = quickCheckWith stdArgs { maxSuccess = 1000 } executesProperly
