{-# OPTIONS_GHC -fno-warn-orphans #-}
module Test.Framework.Tests.Runners.XMLTests (
    test, property
  ) where

import Test.Framework.Runners.Core ( RunTest(..), FinishedTest )
import Test.Framework.Runners.XML.JUnitWriter ( RunDescription(..), morphFlatTestCase, serialize )

import Test.HUnit.Base               ( Test(..), (@?=) )
import Test.QuickCheck               ( Arbitrary, sized, Gen, oneof, listOf, arbitrary )
import qualified Test.QuickCheck.Property as QC ( Property, property )

import Control.Monad ( ap, liftM2, liftM3 )

import Data.Maybe ( isJust )

import qualified Text.XML.Light as XML         ( Element, findAttr, unqual )
import qualified Text.XML.Light.Input as XML   ( parseXMLDoc )

-- | `Arbitrary` instance for `TestResult` generation.
instance Arbitrary FinishedTest where
  arbitrary = sized testResult

-- | Size-constrained generator for `TestResult`
testResult :: Int -> Gen FinishedTest
testResult n | n <= 0    = arbitraryTR
             | otherwise = oneof [ liftM2 RunTestGroup arbitraryXmlStr (listOf subResult),
                                  subResult]
               where arbitraryTR = liftM3 RunTest arbitraryXmlStr arbitraryXmlStr
                                   (liftM2 (,) arbitraryXmlStr arbitrary)
                     -- | drastically cut the size at each level.
                     -- round .. -1 is a hack.  It works a bit better
                     -- (is more extreme) than floor and we're really
                     -- just trying to bound the size so that the
                     -- tests finish quickly.  To see how @floor /=
                     -- (-1) + round@ consider the inputs: 0.5, 1.5,
                     -- and 2.5.
                     subResult :: Gen FinishedTest
                     subResult = let reduce x = (round (logBase 32 (fromIntegral x) :: Double)) - 1
                                 in testResult $ reduce n

-- | `RunDescription` generator.  All string records are restricted to valid xml characters.
instance Arbitrary RunDescription where
  arbitrary = do
              return RunDescription
              `ap` arbitrary            -- errors
              `ap` arbitrary            -- failed count
              `ap` arbitrary            -- skipped
              `ap` arbitraryMaybeXmlStr -- hostname
              `ap` arbitraryXmlStr      -- suiteName
              `ap` arbitrary            -- testCount
              `ap` arbitrary            -- time
              `ap` arbitraryMaybeXmlStr -- timeStamp
              `ap` arbitraryMaybeXmlStr -- runId
              `ap` arbitraryMaybeXmlStr -- package
              `ap` arbitrary            -- tests

-- | Generator for strings that only contain valid XML codepoints, and
-- are wrapped in Maybe.  If/when empty strings are generated, they
-- have a 50% chance of being `Nothing`, so this generator should be biased
-- to create `Just` `String`s over `Nothing`
arbitraryMaybeXmlStr :: Gen (Maybe String)
arbitraryMaybeXmlStr = do
  str <- arbitraryXmlStr
  if null str  -- if we have an empty string, we have a chance of generating @Nothing@
    then oneof [return (Just str), return Nothing]
    else return (Just str)

-- | String generator that does not include invalid XML characters.  The
-- set of invalid characters is specified here:
-- http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
arbitraryXmlStr :: Gen String
arbitraryXmlStr = listOf arbitraryXmlChar
  where
    arbitraryXmlChar :: Gen Char
    arbitraryXmlChar = do c <- arbitrary
                          if validXmlChar (fromEnum c)
                            then return c
                            else arbitraryXmlChar
    validXmlChar c = c == 0x9 || c == 0xA || c == 0xD
                         || (c >= 0x20 && c <= 0xD7FF)
                         || (c >= 0xE000 && c <= 0xFFFD)
                         || (c >= 0x10000 && c <= 0x10FFFF)

-- | Generate random `RunDescriptions`, serialize to (flat) XML strings, then check that they are XML
-- TODO: check them against the JUnit schema
property :: RunDescription -> QC.Property
property = QC.property . isJust . parseSerialize

parseSerialize :: RunDescription -> Maybe XML.Element
parseSerialize = XML.parseXMLDoc . serialize False

-- | Verify that the group names are properly prepended to sub-tests.
test :: Test
test = TestLabel "Check the composition of group names" $ TestCase $
       XML.findAttr (XML.unqual "classname") x @?= Just "top.g1"
  where x = head $ morphFlatTestCase [] $ RunTestGroup "top" [RunTestGroup "g1" [RunTest "t1" "" ("", True)]]
