module Test.Framework.Runners.XML.JUnitWriter (
        RunDescription(..),
        serialize, toXml,
#ifdef TEST
        morphTestCase
#endif
    ) where

import Test.Framework.Runners.Core (RunTest(..), FinishedTest)

import Data.Maybe ( fromMaybe )
import Text.XML.Light ( ppTopElement, unqual, unode
                      , Attr(..), Element(..), QName(..), Content(..))


-- | An overall description of the test suite run.  This is currently
-- styled after the JUnit xml.  It contains records that are not yet
-- used, however, it provides a sensible structure to populate as we
-- are able, and the serialiazation code behaves as though these are
-- filled.
data RunDescription = RunDescription {
    errors :: Int -- ^ The number of tests that triggered error
                  -- conditions (unanticipated failures)
  , failedCount :: Int        -- ^ Count of tests that invalidated stated assertions.
  , skipped :: Maybe Int      -- ^ Count of tests that were provided but not run.
  , hostname :: Maybe String  -- ^ The hostname that ran the test suite.
  , suiteName :: String       -- ^ The name of the test suite.
  , testCount :: Int          -- ^ The total number of tests provided.
  , time :: Double            -- ^ The total execution time for the test suite.
  , timeStamp :: Maybe String -- ^ The time stamp that identifies when this run happened.
  , runId :: Maybe String     -- ^ Included for completness w/ junit.
  , package :: Maybe String   -- ^ holdover from Junit spec. Could be
                              -- used to specify the module under test.
  , tests :: [FinishedTest]   -- ^ detailed description and results for each test run.
  } deriving (Show)


-- | Serializes a `RunDescription` value to a `String`.
serialize :: RunDescription -> String
serialize = ppTopElement . fixClassNames . toXml
  where fixClassNames = setAttributeValue (unqual "classname") (setUnsetClassName "<none>")

-- | Maps a `RunDescription` value to an XML Element
toXml :: RunDescription -> Element
toXml runDesc = unode "testsuite" (attrs, concatMap morphTestCase $ tests runDesc)
  where
    -- | Top-level attributes for the first @testsuite@ tag.
    attrs :: [Attr]
    attrs = map (\(x,f)->Attr (unqual x) (f runDesc)) fields
    fields = [ ("errors",    show . errors)
             , ("failures",  show . failedCount)
             , ("skipped",   fromMaybe "" . fmap show . skipped)
             , ("hostname",  fromMaybe "" . hostname)
             , ("name",      id . suiteName)
             , ("tests",     show . testCount)
             , ("time",      show . time)
             , ("timeStamp", fromMaybe "" . timeStamp)
             , ("id",        fromMaybe "" . runId)
             , ("package",   fromMaybe "" . package)
             ]

-- | Generates XML elements for an individual test case or test group.
morphTestCase :: FinishedTest -> [Element]
morphTestCase (RunTestGroup gname testList) = map (setClassName gname) $
                                            concatMap morphTestCase testList
  where
    setClassName :: String -> Element -> Element
    setClassName group e@(Element _ attribs _ _) =
      e { elAttribs=setClassAttr group attribs }

    -- | Find the classname attribute and prepend gname to it.
    setClassAttr :: String -> [Attr] -> [Attr]
    setClassAttr _ []          = []
    setClassAttr group (a@(Attr k v):as)
      | qName k == "classname" = (Attr k (updateName gname v)):as
      | otherwise              = a:setClassAttr group as
        where
          updateName prefix suffix | suffix == ""                 = prefix
                                   | otherwise                    = prefix++"."++suffix

morphTestCase (RunTest tName _ (tout, pass)) = case pass of
  True  ->  [unode "testcase" caseAttrs]
  False ->  [unode "testcase" (caseAttrs, unode "failure" (failAttrs, tout))]
  where caseAttrs = [ Attr (unqual "name") tName
                    , Attr (unqual "classname") ""
                    , Attr (unqual "time") ""
                    ]
        failAttrs = [ Attr (unqual "message") ""
                    , Attr (unqual "type") ""
                    ]

-- | Sets the specified attributes to the specified value in the given
-- @Element@, returning a new Element with the change.  This recurses
-- deeply through the @Element@, changing all attributes.
setAttributeValue :: QName -> (Attr -> Attr) -> Element -> Element
setAttributeValue aName fn e@(Element _ attribs contents _) = e {
  elAttribs = map fn attribs
  , elContent = map recurse contents }
  where
    recurse :: Content -> Content
    recurse (Elem el) = Elem $ setAttributeValue aName fn el
    -- If content isn't an element, then just return the content as-is:
    recurse x         = x

-- | Sets the attribute value to @newV@ iff the attribute represents a classname.
setUnsetClassName :: String -> Attr -> Attr
setUnsetClassName newV a@(Attr qn v) | qn == (unqual "classname") && v == ""   = a { attrVal = newV }
                                     | otherwise = a