{-|
Module      : Test.Hspec.Hashable
Description : Hashable testing functions
Copyright   : (c) Plow Technologies, 2016
License     : BSD3
Maintainer  : mchaver@gmail.com
Stability   : Beta
-}

{-# LANGUAGE ScopedTypeVariables #-}

module Test.Hspec.Hashable (
  -- * Introduction
  -- $introduction

  -- * Main functions
  -- $main
    testHashableUniqueness
  , testHashableUniquenessWithoutTypeable
  , testSelfEquality
  , testHashableCollision

  -- * Internal help functions
  -- $helperfunctions
  , dupsByMatchingSnd
  ) where

import Control.Arrow ((&&&))

import Data.Hashable
import Data.List
import Data.Proxy
import Data.Typeable

import Test.Hspec
import Test.QuickCheck

-- $main

-- | the main testing function, give it a sampleSize larger than zero (or it will fail) and it
-- will produce arbitrary elements to test the uniqueness of the created hash
-- for a particular type. Should use a large sample size to help find hash collisions.
testHashableUniqueness :: forall a. (Arbitrary a, Eq a, Hashable a, Show a, Typeable a)
  => Int -> Proxy a -> Spec
testHashableUniqueness sampleSize proxy = do
  case sampleSize <= 0 of
    True -> fail ("The sample size must be greater than zero. The sample size you provided is: " ++ show sampleSize ++ ".")
    False -> do
      testSelfEquality sampleSize typeName proxy
      testHashableCollision sampleSize typeName proxy
  where
    typeName = show . typeRep $ proxy

-- | same as 'testHashableUniqueness' but it does not require an instance
-- of typeable and you should pass the type name as a string so it appears
-- in the error message.
testHashableUniquenessWithoutTypeable :: forall a. (Arbitrary a, Eq a, Hashable a, Show a)
  => Int -> String -> Proxy a -> Spec
testHashableUniquenessWithoutTypeable sampleSize typeName proxy = do
  case sampleSize <= 0 of
    True -> fail ("The sample size must be greater than zero. The sample size you provided is: " ++ show sampleSize ++ ".")
    False -> do
      testSelfEquality sampleSize typeName proxy
      testHashableCollision sampleSize typeName proxy

-- | test whether or not the Eq instances is defined such that any value
-- equals itself. If it does not, then the testHashableCollision
-- testing function might not work as expected.
testSelfEquality :: forall a. (Arbitrary a, Eq a, Hashable a, Show a)
  => Int -> String -> Proxy a -> Spec
testSelfEquality sampleSize typeName Proxy =
  describe ("Values of " ++ typeName ++ " derive Eq.") $
    it "all values should be equal to themself. " $ do
      xs <- generate (vectorOf sampleSize (arbitrary :: Gen a))
      (and $ (\x -> x == x) <$> xs) `shouldBe` True

-- | test whether or not there is are hash collisions between unique values.
-- if there are you need to fix your definition of Hashable.
testHashableCollision :: forall a. (Arbitrary a, Eq a, Hashable a, Show a)
  => Int -> String -> Proxy a -> Spec
testHashableCollision sampleSize typeName Proxy =
  describe ("Hashed values of " ++ typeName) $
    it "all non-equivalent values should have unique hashes" $ do
      xs <- generate (vectorOf sampleSize (arbitrary :: Gen a))
      -- nub : remove duplicates in xs
      -- (id &&& hash): put x and hash of x in a tuple
      -- dupsByMatchingSnd: get any tuples that have the same hash value but
      -- have unique (non-equivalent) x values.
      let matchingHashesForUniqueXs = dupsByMatchingSnd [] $ (id &&& hash) <$> nub xs
      -- if the Eq and Hashable instances are well defined, the list should be empty
      matchingHashesForUniqueXs `shouldBe` []


-- $helperfunctions

-- | filter a list by collecting all duplications of the second item of
-- the tuple and return both elements of the tuple.
dupsByMatchingSnd :: (Eq b) => [(a,b)] -> [(a,b)] -> [(a,b)]
dupsByMatchingSnd ys (x:xs) = newX ++ dupsByMatchingSnd (ys ++ [x]) xs
  where
    xDups = filter (\y -> (snd x) == (snd y)) (xs ++ ys)
    newX  = if length xDups > 0
              then [x]
              else []
dupsByMatchingSnd _  []     = []


-- $introduction
--
-- For every 'Hashable' instance of a type, each unique value of that type
-- should have a unique hash. Generally, a 'Generic' 'Hashable' instance of a type should
-- create a unique hash, and ideally these match the rules of type's 'Eq'
-- instance. Any values for that type that are equal should have the same hash
-- and any values that are not equal should have unique hashes.
-- There might still be cases where a 'Generic' 'Hashable' instance
-- breaks those expectations. There are also cases where you might implement
-- 'Hashable' by hand. This testing library assumes that you expect the
-- uniqueness of a type matches in `Eq` and `Hashable`.