{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-incomplete-patterns #-}
module Test.Distribution.Version (properties) where

import Distribution.Version
import Distribution.Text

import Test.QuickCheck
import Test.QuickCheck.Utils
import qualified Test.Laws as Laws

import Control.Monad (liftM, liftM2)
import Data.Maybe (isJust, fromJust)
import Data.List (sort, sortBy, nub)
import Data.Ord  (comparing)

properties :: [Property]
properties =
    -- properties to validate the test framework
  [ property prop_nonNull
  , property prop_gen_intervals1
  , property prop_gen_intervals2
  , property prop_equivalentVersionRange
  , property prop_intermediateVersion

    -- the basic syntactic version range functions
  , property prop_anyVersion
  , property prop_noVersion
  , property prop_thisVersion
  , property prop_notThisVersion
  , property prop_laterVersion
  , property prop_orLaterVersion
  , property prop_earlierVersion
  , property prop_orEarlierVersion
  , property prop_unionVersionRanges
  , property prop_intersectVersionRanges
  , property prop_withinVersion
  , property prop_foldVersionRange
  , property prop_foldVersionRange'

    -- the semantic query functions
  , property prop_isAnyVersion1
  , property prop_isAnyVersion2
  , property prop_isNoVersion
  , property prop_isSpecificVersion1
  , property prop_isSpecificVersion2
  , property prop_simplifyVersionRange1
  , property prop_simplifyVersionRange1'
  , property prop_simplifyVersionRange2
  , property prop_simplifyVersionRange2'
  , property prop_simplifyVersionRange2'' --FIXME

    -- converting between version ranges and version intervals
  , property prop_to_intervals
  , property prop_to_intervals_canonical
  , property prop_to_intervals_canonical'
  , property prop_from_intervals
  , property prop_to_from_intervals
  , property prop_from_to_intervals
  , property prop_from_to_intervals'

    -- union and intersection of version intervals
  , property prop_unionVersionIntervals
  , property prop_unionVersionIntervals_idempotent
  , property prop_unionVersionIntervals_commutative
  , property prop_unionVersionIntervals_associative
  , property prop_intersectVersionIntervals
  , property prop_intersectVersionIntervals_idempotent
  , property prop_intersectVersionIntervals_commutative
  , property prop_intersectVersionIntervals_associative
  , property prop_union_intersect_distributive
  , property prop_intersect_union_distributive

   -- parsing an pretty printing
  , property prop_parse_disp
  ]

instance Arbitrary Version where
  arbitrary = do
    branch <- smallListOf1 $
                frequency [(3, return 0)
                          ,(3, return 1)
                          ,(2, return 2)
                          ,(1, return 3)]
    return (Version branch []) -- deliberate []
    where
      smallListOf1 = adjustSize (\n -> min 5 (n `div` 3)) . listOf1

  shrink (Version branch []) =
    [ Version branch' [] | branch' <- shrink branch, not (null branch') ]
  shrink (Version branch _tags) =
    [ Version branch [] ]

instance Arbitrary VersionRange where
  arbitrary = sized verRangeExp
    where
      verRangeExp n = frequency $
        [ (2, return anyVersion)
        , (1, liftM thisVersion arbitrary)
        , (1, liftM laterVersion arbitrary)
        , (1, liftM orLaterVersion arbitrary)
        , (1, liftM orLaterVersion' arbitrary)
        , (1, liftM earlierVersion arbitrary)
        , (1, liftM orEarlierVersion arbitrary)
        , (1, liftM orEarlierVersion' arbitrary)
        , (1, liftM withinVersion arbitrary)
        ] ++ if n == 0 then [] else
        [ (2, liftM2 unionVersionRanges     verRangeExp2 verRangeExp2)
        , (2, liftM2 intersectVersionRanges verRangeExp2 verRangeExp2)
        ]
        where
          verRangeExp2 = verRangeExp (n `div` 2)

      orLaterVersion'   v =
        UnionVersionRanges (LaterVersion v)   (ThisVersion v)
      orEarlierVersion' v =
        UnionVersionRanges (EarlierVersion v) (ThisVersion v)

---------------------------
-- VersionRange properties
--

prop_nonNull :: Version -> Bool
prop_nonNull = not . null . versionBranch

prop_anyVersion :: Version -> Bool
prop_anyVersion v' =
  withinRange v' anyVersion == True

prop_noVersion :: Version -> Bool
prop_noVersion v' =
  withinRange v' noVersion == False

prop_thisVersion :: Version -> Version -> Bool
prop_thisVersion v v' =
     withinRange v' (thisVersion v)
  == (v' == v)

prop_notThisVersion :: Version -> Version -> Bool
prop_notThisVersion v v' =
     withinRange v' (notThisVersion v)
  == (v' /= v)

prop_laterVersion :: Version -> Version -> Bool
prop_laterVersion v v' =
     withinRange v' (laterVersion v)
  == (v' > v)

prop_orLaterVersion :: Version -> Version -> Bool
prop_orLaterVersion v v' =
     withinRange v' (orLaterVersion v)
  == (v' >= v)

prop_earlierVersion :: Version -> Version -> Bool
prop_earlierVersion v v' =
     withinRange v' (earlierVersion v)
  == (v' < v)

prop_orEarlierVersion :: Version -> Version -> Bool
prop_orEarlierVersion v v' =
     withinRange v' (orEarlierVersion v)
  == (v' <= v)

prop_unionVersionRanges :: VersionRange -> VersionRange -> Version -> Bool
prop_unionVersionRanges vr1 vr2 v' =
     withinRange v' (unionVersionRanges vr1 vr2)
  == (withinRange v' vr1 || withinRange v' vr2)

prop_intersectVersionRanges :: VersionRange -> VersionRange -> Version -> Bool
prop_intersectVersionRanges vr1 vr2 v' =
     withinRange v' (intersectVersionRanges vr1 vr2)
  == (withinRange v' vr1 && withinRange v' vr2)

prop_withinVersion :: Version -> Version -> Bool
prop_withinVersion v v' =
     withinRange v' (withinVersion v)
  == (v' >= v && v' < upper v)
  where
    upper (Version lower t) = Version (init lower ++ [last lower + 1]) t

prop_foldVersionRange :: VersionRange -> Bool
prop_foldVersionRange range =
     expandWildcard range
  == foldVersionRange anyVersion thisVersion
                      laterVersion earlierVersion
                      unionVersionRanges intersectVersionRanges
                      range
  where
    expandWildcard (WildcardVersion v) =
        intersectVersionRanges (orLaterVersion v) (earlierVersion (upper v))
      where
        upper (Version lower t) = Version (init lower ++ [last lower + 1]) t

    expandWildcard (UnionVersionRanges     v1 v2) =
      UnionVersionRanges (expandWildcard v1) (expandWildcard v2)
    expandWildcard (IntersectVersionRanges v1 v2) =
      IntersectVersionRanges (expandWildcard v1) (expandWildcard v2)
    expandWildcard v = v


prop_foldVersionRange' :: VersionRange -> Bool
prop_foldVersionRange' range =
     canonicalise range
  == foldVersionRange' anyVersion thisVersion
                       laterVersion earlierVersion
                       orLaterVersion orEarlierVersion
                       (\v _ -> withinVersion v)
                       unionVersionRanges intersectVersionRanges
                       range
  where
    canonicalise (UnionVersionRanges (LaterVersion v)
                                     (ThisVersion  v')) | v == v'
                = UnionVersionRanges (ThisVersion   v')
                                     (LaterVersion  v)
    canonicalise (UnionVersionRanges (EarlierVersion v)
                                     (ThisVersion    v')) | v == v'
                = UnionVersionRanges (ThisVersion    v')
                                     (EarlierVersion v)
    canonicalise (UnionVersionRanges v1 v2) =
      UnionVersionRanges (canonicalise v1) (canonicalise v2)
    canonicalise (IntersectVersionRanges v1 v2) =
      IntersectVersionRanges (canonicalise v1) (canonicalise v2)
    canonicalise v = v


prop_isAnyVersion1 :: VersionRange -> Version -> Property
prop_isAnyVersion1 range version =
  isAnyVersion range ==> withinRange version range

prop_isAnyVersion2 :: VersionRange -> Property
prop_isAnyVersion2 range =
  isAnyVersion range ==>
    foldVersionRange True (\_ -> False) (\_ -> False) (\_ -> False)
                          (\_ _ -> False) (\_ _ -> False)
      (simplifyVersionRange range)

prop_isNoVersion :: VersionRange -> Version -> Property
prop_isNoVersion range version =
  isNoVersion range ==> not (withinRange version range)

prop_isSpecificVersion1 :: VersionRange -> NonEmptyList Version -> Property
prop_isSpecificVersion1 range (NonEmpty versions) =
  isJust version && not (null versions') ==>
    allEqual (fromJust version : versions')
  where
    version     = isSpecificVersion range
    versions'   = filter (`withinRange` range) versions
    allEqual xs = and (zipWith (==) xs (tail xs))

prop_isSpecificVersion2 :: VersionRange -> Property
prop_isSpecificVersion2 range =
  isJust version ==>
    foldVersionRange Nothing Just (\_ -> Nothing) (\_ -> Nothing)
                     (\_ _ -> Nothing) (\_ _ -> Nothing)
      (simplifyVersionRange range)
    == version

  where
    version = isSpecificVersion range

-- | 'simplifyVersionRange' is a semantic identity on 'VersionRange'.
--
prop_simplifyVersionRange1 :: VersionRange -> Version -> Bool
prop_simplifyVersionRange1 range version =
  withinRange version range == withinRange version (simplifyVersionRange range)

prop_simplifyVersionRange1' :: VersionRange -> Bool
prop_simplifyVersionRange1' range =
  range `equivalentVersionRange` (simplifyVersionRange range)

-- | 'simplifyVersionRange' produces a canonical form for ranges with
-- equivalent semantics.
--
prop_simplifyVersionRange2 :: VersionRange -> VersionRange -> Version -> Property
prop_simplifyVersionRange2 r r' v =
  r /= r' && simplifyVersionRange r == simplifyVersionRange r' ==>
    withinRange v r == withinRange v r'

prop_simplifyVersionRange2' :: VersionRange -> VersionRange -> Property
prop_simplifyVersionRange2' r r' =
  r /= r' && simplifyVersionRange r == simplifyVersionRange r' ==>
    r `equivalentVersionRange` r'

prop_simplifyVersionRange2'' :: VersionRange -> VersionRange -> Property
prop_simplifyVersionRange2'' r r' =
  r /= r' && r `equivalentVersionRange` r' ==>
       simplifyVersionRange r == simplifyVersionRange r'
    || isNoVersion r
    || isNoVersion r'

--------------------
-- VersionIntervals
--

-- | Generating VersionIntervals
--
-- This is a tad tricky as VersionIntervals is an abstract type, so we first
-- make a local type for generating the internal representation. Then we check
-- that this lets us construct valid 'VersionIntervals'.
--
newtype VersionIntervals' = VersionIntervals' [VersionInterval]
  deriving (Eq, Show)

instance Arbitrary VersionIntervals' where
  arbitrary = do
    ubound <- arbitrary
    bounds <- arbitrary
    let intervals = mergeTouching
                  . map fixEmpty
                  . replaceUpper ubound
                  . pairs
                  . sortBy (comparing fst)
                  $ bounds
    return (VersionIntervals' intervals)

    where
      pairs ((l, lb):(u, ub):bs) = (LowerBound l lb, UpperBound u ub)
                                 : pairs bs
      pairs _                    = []

      replaceUpper NoUpperBound [(l,_)] = [(l, NoUpperBound)]
      replaceUpper NoUpperBound (i:is)  = i : replaceUpper NoUpperBound is
      replaceUpper _               is   = is

      -- merge adjacent intervals that touch
      mergeTouching (i1@(l,u):i2@(l',u'):is)
        | doesNotTouch u l' = i1 : mergeTouching (i2:is)
        | otherwise         =      mergeTouching ((l,u'):is)
      mergeTouching is      = is

      doesNotTouch :: UpperBound -> LowerBound -> Bool
      doesNotTouch NoUpperBound _ = False
      doesNotTouch (UpperBound u ub) (LowerBound l lb) =
            u <  l
        || (u == l && ub == ExclusiveBound && lb == ExclusiveBound)
      
      fixEmpty (LowerBound l _, UpperBound u _)
        | l == u = (LowerBound l InclusiveBound, UpperBound u InclusiveBound)
      fixEmpty i = i

  shrink (VersionIntervals' intervals) =
    [ VersionIntervals' intervals' | intervals' <- shrink intervals ]

instance Arbitrary Bound where
  arbitrary = elements [ExclusiveBound, InclusiveBound]

instance Arbitrary LowerBound where
  arbitrary = liftM2 LowerBound arbitrary arbitrary

instance Arbitrary UpperBound where
  arbitrary = oneof [return NoUpperBound
                    ,liftM2 UpperBound arbitrary arbitrary]

-- | Check that our VersionIntervals' arbitrary instance generates intervals
-- that satisfies the invariant.
--
prop_gen_intervals1 :: VersionIntervals' -> Bool
prop_gen_intervals1 (VersionIntervals' intervals) =
  isJust (mkVersionIntervals intervals)

instance Arbitrary VersionIntervals where
  arbitrary = do
    VersionIntervals' intervals <- arbitrary
    case mkVersionIntervals intervals of
      Just xs -> return xs

-- | Check that constructing our intervals type and converting it to a
-- 'VersionRange' and then into the true intervals type gives us back
-- the exact same sequence of intervals. This tells us that our arbitrary
-- instance for 'VersionIntervals'' is ok.
--
prop_gen_intervals2 :: VersionIntervals' -> Bool
prop_gen_intervals2 (VersionIntervals' intervals') =
    asVersionIntervals (fromVersionIntervals intervals) == intervals'
  where
    Just intervals = mkVersionIntervals intervals'

-- | Check that 'VersionIntervals' models 'VersionRange' via
-- 'toVersionIntervals'.
--
prop_to_intervals :: VersionRange -> Version -> Bool
prop_to_intervals range version =
  withinRange version range == withinIntervals version intervals
  where
    intervals = toVersionIntervals range

-- | Check that semantic equality on 'VersionRange's is the same as converting
-- to 'VersionIntervals' and doing syntactic equality.
--
prop_to_intervals_canonical :: VersionRange -> VersionRange -> Property
prop_to_intervals_canonical r r' =
  r /= r' && r `equivalentVersionRange` r' ==>
    toVersionIntervals r == toVersionIntervals r'

prop_to_intervals_canonical' :: VersionRange -> VersionRange -> Property
prop_to_intervals_canonical' r r' =
  r /= r' && toVersionIntervals r == toVersionIntervals r' ==>
    r `equivalentVersionRange` r'

-- | Check that 'VersionIntervals' models 'VersionRange' via
-- 'fromVersionIntervals'.
--
prop_from_intervals :: VersionIntervals -> Version -> Bool
prop_from_intervals intervals version =
  withinRange version range == withinIntervals version intervals
  where
    range = fromVersionIntervals intervals

-- | @'toVersionIntervals' . 'fromVersionIntervals'@ is an exact identity on
-- 'VersionIntervals'.
--
prop_to_from_intervals :: VersionIntervals -> Bool
prop_to_from_intervals intervals =
  toVersionIntervals (fromVersionIntervals intervals) == intervals

-- | @'fromVersionIntervals' . 'toVersionIntervals'@ is a semantic identity on
-- 'VersionRange', though not necessarily a syntactic identity.
--
prop_from_to_intervals :: VersionRange -> Bool
prop_from_to_intervals range =
  range' `equivalentVersionRange` range
  where
    range' = fromVersionIntervals (toVersionIntervals range)

-- | Equivalent of 'prop_from_to_intervals'
--
prop_from_to_intervals' :: VersionRange -> Version -> Bool
prop_from_to_intervals' range version =
  withinRange version range' == withinRange version range
  where
    range' = fromVersionIntervals (toVersionIntervals range)

-- | The semantics of 'unionVersionIntervals' is (||).
--
prop_unionVersionIntervals :: VersionIntervals -> VersionIntervals
                           -> Version -> Bool
prop_unionVersionIntervals is1 is2 v =
     withinIntervals v (unionVersionIntervals is1 is2)
  == (withinIntervals v is1 || withinIntervals v is2)

-- | 'unionVersionIntervals' is idempotent
--
prop_unionVersionIntervals_idempotent :: VersionIntervals -> Bool
prop_unionVersionIntervals_idempotent =
  Laws.idempotent_binary unionVersionIntervals

-- | 'unionVersionIntervals' is commutative
--
prop_unionVersionIntervals_commutative :: VersionIntervals
                                       -> VersionIntervals -> Bool
prop_unionVersionIntervals_commutative =
  Laws.commutative unionVersionIntervals

-- | 'unionVersionIntervals' is associative
--
prop_unionVersionIntervals_associative :: VersionIntervals
                                       -> VersionIntervals
                                       -> VersionIntervals -> Bool
prop_unionVersionIntervals_associative =
  Laws.associative unionVersionIntervals

-- | The semantics of 'intersectVersionIntervals' is (&&).
--
prop_intersectVersionIntervals :: VersionIntervals -> VersionIntervals
                               -> Version -> Bool
prop_intersectVersionIntervals is1 is2 v =
     withinIntervals v (intersectVersionIntervals is1 is2)
  == (withinIntervals v is1 && withinIntervals v is2)

-- | 'intersectVersionIntervals' is idempotent
--
prop_intersectVersionIntervals_idempotent :: VersionIntervals -> Bool
prop_intersectVersionIntervals_idempotent =
  Laws.idempotent_binary intersectVersionIntervals

-- | 'intersectVersionIntervals' is commutative
--
prop_intersectVersionIntervals_commutative :: VersionIntervals
                                           -> VersionIntervals -> Bool
prop_intersectVersionIntervals_commutative =
  Laws.commutative intersectVersionIntervals

-- | 'intersectVersionIntervals' is associative
--
prop_intersectVersionIntervals_associative :: VersionIntervals
                                           -> VersionIntervals
                                           -> VersionIntervals -> Bool
prop_intersectVersionIntervals_associative =
  Laws.associative intersectVersionIntervals

-- | 'unionVersionIntervals' distributes over 'intersectVersionIntervals'
--
prop_union_intersect_distributive :: Property
prop_union_intersect_distributive =
      Laws.distributive_left  unionVersionIntervals intersectVersionIntervals
  .&. Laws.distributive_right unionVersionIntervals intersectVersionIntervals

-- | 'intersectVersionIntervals' distributes over 'unionVersionIntervals'
--
prop_intersect_union_distributive :: Property
prop_intersect_union_distributive =
      Laws.distributive_left  intersectVersionIntervals unionVersionIntervals
  .&. Laws.distributive_right intersectVersionIntervals unionVersionIntervals

--------------------------------
-- equivalentVersionRange helper

prop_equivalentVersionRange :: VersionRange  -> VersionRange
                            -> Version -> Property
prop_equivalentVersionRange range range' version =
  equivalentVersionRange range range' && range /= range' ==>
    withinRange version range == withinRange version range'

equivalentVersionRange :: VersionRange -> VersionRange -> Bool
equivalentVersionRange vr1 vr2 =
  let allVersionsUsed = nub (sort (versionsUsed vr1 ++ versionsUsed vr2))
      minPoint = Version [0] []
      maxPoint | null allVersionsUsed = minPoint
               | otherwise = case maximum allVersionsUsed of
                   Version vs _ -> Version (vs ++ [1]) []
      probeVersions = minPoint : maxPoint
                    : intermediateVersions allVersionsUsed

  in all (\v -> withinRange v vr1 == withinRange v vr2) probeVersions

  where
    versionsUsed = foldVersionRange [] (\x->[x]) (\x->[x]) (\x->[x]) (++) (++)
    intermediateVersions (v1:v2:vs) = v1 : intermediateVersion v1 v2
                                         : intermediateVersions (v2:vs)
    intermediateVersions vs = vs

intermediateVersion :: Version -> Version -> Version
intermediateVersion v1 v2 | v1 >= v2 = error "intermediateVersion: v1 >= v2"
intermediateVersion (Version v1 _) (Version v2 _) =
  Version (intermediateList v1 v2) []
  where
    intermediateList :: [Int] -> [Int] -> [Int]
    intermediateList []     (_:_) = [0]
    intermediateList (x:xs) (y:ys)
        | x <  y    = x : xs ++ [0]
        | otherwise = x : intermediateList xs ys

prop_intermediateVersion :: Version -> Version -> Property
prop_intermediateVersion v1 v2 =
  (v1 /= v2) && not (adjacentVersions v1 v2) ==>
  if v1 < v2
    then let v = intermediateVersion v1 v2
          in (v1 < v && v < v2)
    else let v = intermediateVersion v2 v1
          in v1 > v && v > v2

adjacentVersions :: Version -> Version -> Bool
adjacentVersions (Version v1 _) (Version v2 _) = v1 ++ [0] == v2
                                              || v2 ++ [0] == v1

--------------------------------
-- Parsing and pretty printing
--

prop_parse_disp :: VersionRange -> Bool
prop_parse_disp vr =
  simpleParse (display vr) == Just (canonicalise vr)

  where
    canonicalise = swizzle . swap

    swizzle     (UnionVersionRanges (UnionVersionRanges v1 v2) v3)
      | not (isOrLaterVersion v1 v2) && not (isOrEarlierVersion v1 v2)
      = swizzle (UnionVersionRanges v1 (UnionVersionRanges v2  v3))

    swizzle     (IntersectVersionRanges (IntersectVersionRanges v1 v2) v3)
      = swizzle (IntersectVersionRanges v1 (IntersectVersionRanges v2  v3))

    swizzle (UnionVersionRanges v1 v2) =
      UnionVersionRanges (swizzle v1) (swizzle v2)
    swizzle (IntersectVersionRanges v1 v2) =
      IntersectVersionRanges (swizzle v1) (swizzle v2)
    swizzle v = v

    isOrLaterVersion (ThisVersion  v) (LaterVersion v') = v == v'
    isOrLaterVersion _                _                 = False

    isOrEarlierVersion (ThisVersion v)    (EarlierVersion v') = v == v'
    isOrEarlierVersion _                  _                   = False

    swap =
      foldVersionRange' anyVersion thisVersion
                        laterVersion earlierVersion
                        orLaterVersion orEarlierVersion
                        (\v _ -> withinVersion v)
                        unionVersionRanges intersectVersionRanges
