{-# OPTIONS_GHC -Wno-x-partial #-}

module Hasura.FeatureMatrix (render, parseLogs, extractFeatures, renderFeatureMatrix) where

import Control.Applicative
import Control.Monad (unless, void)
import Control.Monad.Except
import Control.Monad.State
import Data.Aeson
import Data.Aeson.Parser
import Data.Aeson.Types
import Data.Attoparsec.ByteString as Atto
import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as LBS
import Data.Foldable
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Maybe
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Lucid
import Text.RawString.QQ (r)
import Prelude hiding (interact)

type FeatureName = Text

type Features = Map FeatureName [TestRun]

data TestRun = TestRun
  { -- Extra data we capture but do not currently use.
    _testRunRequirement :: Text,
    testRunGroups :: [Text],
    testRunPassed :: Bool,
    -- Extra data we capture but do not currently use.
    _testRunHspecItem :: HspecItem
  }

data HspecEventItemDone = HspecEventItemDone
  { heidGroups :: [Text],
    heidItem :: HspecItem,
    heidRequirement :: Text
  }

{- Decode things from events such as:

    {
        "event_tag": "ItemDone",
        "groups": [
            "Test.HelloWorld",
            "Postgres",
            "Example test"
        ],
        "item": {
            "duration": 1.1321041419996618,
            "info": "",
            "location": {
                "column": 5,
                "file": "test/Test/HelloWorldSpec.hs",
                "line": 60
            },
            "result": {
                "result": "Success"
            }
        },
        "requirement": "Works as expected",
        "type": "Hspec Event"
    }
-}

instance FromJSON HspecEventItemDone where
  parseJSON = withObject "Hspec Event ItemDone" \o -> do
    let tags = parseMaybe (\o' -> (,) <$> o' .: "type" <*> o' .: "event_tag") o
    unless
      (tags == Just ("Hspec Event" :: Text, "ItemDone" :: Text))
      (fail "Not a Hspec Event ItemDone")
    HspecEventItemDone <$> o .: "groups" <*> o .: "item" <*> o .: "requirement"

data HspecItem = HspecItem
  { hiResult :: HspecItemResult
  }

instance FromJSON HspecItem where
  parseJSON =
    withObject
      "Hspec Item"
      ( \o -> HspecItem <$> o .: "result"
      )

data HspecItemResult = HIRSuccess | HIRPending Text | HIRFailure Value -- Failures have details that we don't care about just yet.

instance FromJSON HspecItemResult where
  parseJSON =
    withObject
      "Hspec Item Result"
      ( \o -> do
          result :: String <- o .: "result"
          case result of
            "Success" -> pure HIRSuccess
            "Failure" -> HIRFailure <$> (o .: "reason")
            "Pending" -> HIRPending <$> (o .: "message")
            _ -> fail $ "Unknown result type: " ++ result
      )

render :: ByteString -> ByteString
render input =
  let parsedLogs = parseLogs input
      features = runExcept . flip execStateT mempty . traverse extractFeatures <$> parsedLogs
   in renderFeatureMatrix features

parseLogs :: ByteString -> Atto.Result [Value]
parseLogs = noPartial . Atto.parse (json `sepBy` (string "\n") <* (option () (void (string "\n")) *> endOfInput))

noPartial :: Atto.Result a -> Atto.Result a
noPartial (Partial c) = c "" -- You signal Attoparsec that you're done by
-- giving an empty string to partial results. A bit weird...
noPartial result = result

extractFeatures :: Value -> (StateT Features (Except Text)) ()
extractFeatures v = do
  case parseEither parseJSON v of
    -- Skip this entry, as it's uninteresting.
    Left "Error in $: Not a Hspec Event ItemDone" -> return ()
    -- We do want to report other errors though, because those are indicative
    -- of format changes that we need to catch in order to ensure we're
    -- reporting correctly.
    Left err -> throwError (T.pack err)
    Right (itemDone :: HspecEventItemDone) -> do
      case testRun itemDone of
        Nothing -> return ()
        Just run -> modify (M.unionWith (<>) (M.singleton (featureName itemDone) [run]))
  where
    featureName :: HspecEventItemDone -> FeatureName
    featureName HspecEventItemDone {..} = case heidGroups of
      [] -> "empty??"
      name : _ -> T.takeWhile (/= '.') $ fromMaybe name (T.stripPrefix "Test." name)

    testRun :: HspecEventItemDone -> Maybe TestRun
    testRun HspecEventItemDone {..} =
      TestRun heidRequirement
        <$> pure heidGroups
        <*> case hiResult heidItem of
          HIRSuccess -> Just True
          HIRPending {} -> Nothing -- We don't care about pending items
          HIRFailure {} -> Just False
        <*> pure heidItem

-- | Output the rendered feature matrix to stdout.
renderFeatureMatrix :: Atto.Result (Either Text Features) -> ByteString
renderFeatureMatrix =
  LBS.toStrict . renderBS . \case
    Fail unconsumed contexts errorMsg ->
      reportHead do
        h1_ "Failed to decode json"
        pre_ (toHtml errorMsg)
        pre_ (toHtml unconsumed)
        mapM_ (pre_ . toHtml) contexts
    Partial {} ->
      reportHead do
        h1_ "Failed to decode json (`Partial` result, impossible?)"
    Done unconsumed (Left err) ->
      reportHead do
        h1_ "Failed to gather feature information"
        pre_ (toHtml err)
        h1_ "Maybe unconsumed input remains"
        pre_ (toHtml unconsumed)
    Done "" (Right features) ->
      reportHead do
        h1_ reportTitle
        table_ $ traverse_ featureRow (M.toList features)
    Done unconsumed (Right _) ->
      reportHead do
        h1_ "Parsing succeded, but unconsumed input remains (impossible?)"
        pre_ (toHtml unconsumed)
  where
    reportHead :: Html () -> Html ()
    reportHead content =
      doctypehtml_ do
        head_ do
          meta_ [charset_ "utf-8"]
          title_ reportTitle
          style_
            [r|
              body {
                max-width: 900px;
                margin: auto;
                font-family: sans-serif;
              }
              h1 { text-align: center; }
              table {
                width: 70%;
                margin: auto;
                font-size: 20px;
              }
              td { padding: 5px; padding-left: 15px; padding-right: 15px; }
              tr { border-bottom: 1px solid #dadada; }
              tr:hover { background-color: #eee; }
            |]
        body_ content

    reportTitle = "Feature Matrix Compatibility Report"

    featureRow :: (FeatureName, [TestRun]) -> Html ()
    featureRow (name, runs) = tr_ $ do
      td_ (toHtml name)
      td_ [style_ "text-align: right", title_ (runNames runs)] (toHtml $ runsStatus runs)

    -- Construct a sensible list of indications of which tests have been run.
    runNames :: [TestRun] -> Text
    runNames =
      T.unlines
        . Set.toList
        . Set.fromList
        . filter (not . T.null)
        . map (\testRun -> runsStatus [testRun] <> " " <> inlineGroups (testRunGroups $ testRun))

    inlineGroups :: [Text] -> Text
    inlineGroups groups =
      T.intercalate
        "."
        (filter (/= "Postgres") $ tail groups)

    runsStatus :: [TestRun] -> Text
    runsStatus runs
      | all testRunPassed runs = "✅"
      | otherwise = "❌"
