{-# LANGUAGE OverloadedStrings #-}

module Text.XmlHtml.OASISTest (testsOASIS) where

import           Control.Applicative ((<*>))
import           Data.ByteString.Builder
import qualified Data.ByteString.Lazy as BSL
import           Data.Foldable (forM_)
import           Data.Functor ((<$>))
import           Data.Traversable (forM)
import qualified Data.ByteString as B
import           Data.Maybe
import           Data.Monoid (mconcat)
import qualified Data.Text as T
import           System.Directory
import           Test.Hspec
import           Test.HUnit hiding (Test, Node)
import           Text.XmlHtml

------------------------------------------------------------------------------
-- | Runs the OASIS XML conformance test suite.  The test cases sit in
-- directories inside of @test/resources@.  This test reads them, parses them,
-- and ensures that they behave as expected:
--
-- * If there is a file in the same directory named /filename/@.correct@
--   or /filename/@.incorrect@, the parsing is expected to either succeed
--   or fail, as indicated.  Otherwise, the remaining cases apply.
--
-- * For those tests marked @not-wf@, the test expects an error message.
--
-- * For tests marked @valid@ or @invalid@ (there is no distinction since
--   @xmlhtml@ is not a validating parser), the test expects a successful
--   parse, but does not verify the parse result.
--
-- For tests that should succeed as XML but not HTML, or vice versa, files
-- can be named /filename/@.xml.correct@, and so on (all 4 combinations).


-- We need this as long as we support bytestring versions
-- that don't export BSL.toStrict (ghc-7.4 and older)
toStrict' :: BSL.ByteString -> B.ByteString
toStrict' = mconcat . BSL.toChunks

testsOASIS :: Spec
testsOASIS = do
    it "xmlhtml/ibm-not-wf     " $ oasisP  "ibm/ibm_oasis_not-wf.xml"
    it "xmlhtml/ibm-invalid    " $ oasisP  "ibm/ibm_oasis_invalid.xml"
    it "xmlhtml/ibm-valid      " $ oasisP  "ibm/ibm_oasis_valid.xml"
    it "xmlhtml/oasis          " $ oasisP  "oasis/oasis.xml"
    it "xmlhtml/r-ibm-not-wf   " $ oasisR  "ibm/ibm_oasis_not-wf.xml"
    it "xmlhtml/r-ibm-invalid  " $ oasisR  "ibm/ibm_oasis_invalid.xml"
    it "xmlhtml/r-ibm-valid    " $ oasisR  "ibm/ibm_oasis_valid.xml"
    it "xmlhtml/r-oasis        " $ oasisR  "oasis/oasis.xml"
    it "xmlhtml/h-ibm-not-wf   " $ oasisHP "ibm/ibm_oasis_not-wf.xml"
    it "xmlhtml/h-ibm-invalid  " $ oasisHP "ibm/ibm_oasis_invalid.xml"
    it "xmlhtml/h-ibm-valid    " $ oasisHP "ibm/ibm_oasis_valid.xml"
    it "xmlhtml/h-oasis        " $ oasisHP "oasis/oasis.xml"
    it "xmlhtml/hr-ibm-not-wf  " $ oasisHR "ibm/ibm_oasis_not-wf.xml"
    it "xmlhtml/hr-ibm-invalid " $ oasisHR "ibm/ibm_oasis_invalid.xml"
    it "xmlhtml/hr-ibm-valid   " $ oasisHR "ibm/ibm_oasis_valid.xml"
    it "xmlhtml/hr-oasis       " $ oasisHR "oasis/oasis.xml"


oasisP :: String -> Assertion
oasisP name = do
    tests <- getOASIS ".xml" name
    forM_ tests $ \(fn, ty) -> case ty of
        True  -> oasisWF fn
        False -> oasisNotWF fn


oasisR :: String -> Assertion
oasisR name = do
    tests <- getOASIS ".xml" name
    forM_ tests $ \(fn, ty) -> case ty of
        True  -> oasisRerender fn
        False -> return ()


oasisHP :: String -> Assertion
oasisHP name = do
    tests <- getOASIS ".html" name
    forM_ tests $ \(fn, ty) -> case ty of
        True  -> hOasisWF fn
        False -> hOasisNotWF fn


oasisHR :: String -> Assertion
oasisHR name = do
    tests <- getOASIS ".html" name
    forM_ tests $ \(fn, ty) -> case ty of
        True  -> hOasisRerender fn
        False -> return ()


getOASIS :: String -> String -> IO [(String, Bool)]
getOASIS sub name = do
    testListSrc <- B.readFile ("test/resources/" ++ name)
    let Right (XmlDocument _ _ ns) = parseXML name testListSrc
    let Just c = listToMaybe (filter isElement ns)
    oasisTestCases sub name c


oasisTestCases :: String -> String -> Node -> IO [(String, Bool)]
oasisTestCases sub name n = do
    fmap concat $ forM (childElements n) $ \t -> case tagName t of
        Just "TESTCASES" -> oasisTestCases sub name t
        Just "TEST"      -> oasisTest sub name t
        _                -> error (show t)


oasisTest :: String -> String -> Node -> IO [(String, Bool)]
oasisTest sub name t = do
    let fn = file $ fromJust $ getAttribute "URI" t
    fe <- doesFileExist fn
    ce <- (||) <$> doesFileExist (fn ++ ".correct")
               <*> doesFileExist (fn ++ sub ++ ".correct")
    ie <- (||) <$> doesFileExist (fn ++ ".incorrect")
               <*> doesFileExist (fn ++ sub ++ ".incorrect")
    let ty = getAttribute "TYPE" t
    if fe then case () of
        () | ce                   -> return [(fn, True)]
           | ie                   -> return [(fn, False)]
           | ty == Just "not-wf"  -> return [(fn, False)]
           | ty == Just "invalid" -> return [(fn, True)]
           | ty == Just "valid"   -> return [(fn, True)]
           | otherwise            -> return [(fn, True)]
      else return []
  where
    file f        = "test/resources/" ++ toLastSlash name ++ T.unpack f
    toLastSlash s = snd (go s)
        where go []     = (False, [])
              go (c:cs) = let (found, ccs) = go cs
                          in  if found then (True, c:ccs)
                              else if c == '/' then (True, "/")
                              else (False, c:cs)


oasisNotWF :: String -> Assertion
oasisNotWF name = do
    src <- B.readFile name
    assertBool ("not-wf parse " ++ name) $ isLeft (parseXML name src)
  where
    isLeft (Left _) = True
    isLeft _        = False


oasisWF :: String -> Assertion
oasisWF name = do
    src <- B.readFile name
    assertBool ("wf parse " ++ name) $ isRight (parseXML name src)
  where
    isRight (Right _) = True
    isRight _         = False


oasisRerender :: String -> Assertion
oasisRerender name = do
    src         <- B.readFile name
    let Right d  = parseXML "" src
    let src2     = toStrict' . toLazyByteString $ render d
    let Right d2 = parseXML "" src2
    assertEqual ("rerender " ++ name) d d2


hOasisNotWF :: String -> Assertion
hOasisNotWF name = do
    src <- B.readFile name
    assertBool ("not-wf parse " ++ name) $ isLeft (parseHTML name src)
  where
    isLeft (Left _) = True
    isLeft _        = False


hOasisWF :: String -> Assertion
hOasisWF name = do
    src <- B.readFile name
    assertBool ("wf parse " ++ name) $ isRight (parseHTML name src)
  where
    isRight (Right _) = True
    isRight _         = False


hOasisRerender :: String -> Assertion
hOasisRerender name = do
    src         <- B.readFile name
    let Right d  = parseHTML "" src
    let src2     = toStrict' . toLazyByteString $ render d
    let Right d2 = parseHTML "" src2
    assertEqual ("rerender " ++ name) d d2
