{-# LANGUAGE PatternGuards, ScopedTypeVariables #-}

module Text.EditDistance.Tests.Properties (
        tests
    ) where

import Text.EditDistance.EditCosts
import qualified Text.EditDistance.SquareSTUArray as SquareSTUArray
import qualified Text.EditDistance.STUArray as STUArray
import qualified Text.EditDistance.Bits as Bits
import Text.EditDistance.Tests.EditOperationOntology

import Test.Framework
import Test.Framework.Providers.QuickCheck2
import Test.QuickCheck


tests :: [Test]
tests = [ testGroup "Levenshtein Distance (SquareSTUArray)" sqstu_levenshteinDistanceTests
        , testGroup "Restricted Damerau-Levenshtein Distance (SquareSTUArray)" sqstu_restrictedDamerauLevenshteinDistanceTests
        , testGroup "Levenshtein Distance (STUArray)" stu_levenshteinDistanceTests
        , testGroup "Restricted Damerau-Levenshtein Distance (STUArray)" stu_restrictedDamerauLevenshteinDistanceTests
        , testGroup "Levenshtein Distance (Bits)" bits_levenshteinDistanceTests
        , testGroup "Restricted Damerau-Levenshtein Distance (Bits)" bits_restrictedDamerauLevenshteinDistanceTests
        , testGroup "Levenshtein Distance Crosschecks" levenshteinDistanceCrosscheckTests
        , testGroup "Restricted Damerau-Levenshtein Distance Crosschecks" restrictedDamerauLevenshteinDistanceCrosscheckTests
        --, testGroup "Levenshtein Distance Cutoff (Bits)" bits_levenshteinDistanceCutoffTests
        ]
  where
    sqstu_levenshteinDistanceTests                  = standardDistanceTests SquareSTUArray.levenshteinDistance                  interestingCosts (undefined :: BasicEditOperation)
    sqstu_restrictedDamerauLevenshteinDistanceTests = standardDistanceTests SquareSTUArray.restrictedDamerauLevenshteinDistance interestingCosts (undefined :: ExtendedEditOperation)
    stu_levenshteinDistanceTests                    = standardDistanceTests STUArray.levenshteinDistance                        interestingCosts (undefined :: BasicEditOperation)
    stu_restrictedDamerauLevenshteinDistanceTests   = standardDistanceTests STUArray.restrictedDamerauLevenshteinDistance       interestingCosts (undefined :: ExtendedEditOperation)
    bits_levenshteinDistanceTests                   = standardDistanceTests (const Bits.levenshteinDistance)                    defaultEditCosts (undefined :: BasicEditOperation)
    bits_restrictedDamerauLevenshteinDistanceTests  = standardDistanceTests (const Bits.restrictedDamerauLevenshteinDistance)   defaultEditCosts (undefined :: ExtendedEditOperation)

    --bits_levenshteinDistanceCutoffTests = [ testProperty "Cutoff vs. Non-Cutoff" (forAll arbitrary (\cutoff -> distanceEqIfBelowProperty cutoff (Bits.levenshteinDistanceCutoff cutoff) Bits.levenshteinDistance defaultEditCosts (undefined :: BasicEditOperation))) ]

    levenshteinDistanceCrosscheckTests
      = crossCheckTests [ ("SquareSTUArray", SquareSTUArray.levenshteinDistance defaultEditCosts)
                        , ("STUArray",       STUArray.levenshteinDistance defaultEditCosts)
                        , ("Bits",           Bits.levenshteinDistance) ]
                        (undefined :: BasicEditOperation)

    restrictedDamerauLevenshteinDistanceCrosscheckTests
      = crossCheckTests [ ("SquareSTUArray", SquareSTUArray.restrictedDamerauLevenshteinDistance defaultEditCosts)
                        , ("STUArray",       STUArray.restrictedDamerauLevenshteinDistance defaultEditCosts)
                        , ("Bits",           Bits.restrictedDamerauLevenshteinDistance) ]
                        (undefined :: ExtendedEditOperation)


interestingCosts :: EditCosts
interestingCosts = EditCosts {
    deletionCosts = ConstantCost 1,
    insertionCosts = ConstantCost 2,
    substitutionCosts = ConstantCost 3, -- Can't be higher than deletion + insertion
    transpositionCosts = ConstantCost 3 -- Can't be higher than deletion + insertion
}


crossCheckTests :: forall op. (EditOperation op, Show op) => [(String, String -> String -> Int)] -> op -> [Test]
crossCheckTests named_distances _op_dummy
  = [ testProperty (name1 ++ " vs. " ++ name2) (distanceEqProperty distance1 distance2 _op_dummy)
    | (ix1, (name1, distance1)) <- enumerated_named_distances, (ix2, (name2, distance2)) <- enumerated_named_distances, ix2 > ix1 ]
  where
    enumerated_named_distances = [(1 :: Int)..] `zip` named_distances

distanceEqProperty :: (String -> String -> Int) -> (String -> String -> Int) -> op -> EditedString op -> Bool
distanceEqProperty distance1 distance2 _op_dummy (MkEditedString old new _ _) = distance1 old new == distance2 old new

--distanceEqIfBelowProperty :: (EditOperation op) => Int -> (String -> String -> Int) -> (String -> String -> Int) -> EditCosts -> op -> EditedString op -> Property
--distanceEqIfBelowProperty cutoff distance1 distance2 costs _op_dummy (MkEditedString old new ops) = (editCost costs ops <= cutoff) ==> distance1 old new == distance2 old new

standardDistanceTests :: forall op. (EditOperation op, Show op) => (EditCosts -> String -> String -> Int) -> EditCosts -> op -> [Test]
standardDistanceTests distance costs _op_dummy
  = [ testProperty "Self distance is zero" prop_self_distance_zero
    , testProperty "Pure deletion has the right cost" prop_pure_deletion_cost_correct
    , testProperty "Pure insertion has the right cost" prop_pure_insertion_cost_correct
    , testProperty "Single operations have the right cost" prop_single_op_cost_is_distance
    , testProperty "Cost bound is respected" prop_combined_op_cost_at_least_distance
    ]
  where
    testableDistance = distance costs

    prop_self_distance_zero str
      = testableDistance str str == 0
    prop_pure_deletion_cost_correct str
      = testableDistance str "" == sum [deletionCost costs c | c <- str]
    prop_pure_insertion_cost_correct str
      = testableDistance "" str == sum [insertionCost costs c | c <- str]
    prop_single_op_cost_is_distance (MkEditedString old new _ops cost :: EditedString op)
      = (length old > 2) ==> testableDistance old new == cost costs || old == new
    prop_combined_op_cost_at_least_distance (MkEditedString old new ops cost :: EditedString [op])
      = not (containsTransposition ops) ==> testableDistance old new <= cost costs
