{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

module Common where

import Control.Monad (unless, when)
import Data.List (isInfixOf, isSuffixOf, (!!))
import Data.Monoid ((<>))
import qualified Data.Set as S
import Data.String (fromString)
import Data.Text (Text, pack, unpack)
import qualified Data.Text as T
import System.Directory
  ( createDirectory,
    createDirectoryIfMissing,
    doesDirectoryExist,
    doesFileExist,
    getTemporaryDirectory,
    listDirectory,
    removeDirectoryRecursive,
    removePathForcibly,
  )
import System.FilePath (takeExtensions, (</>))
import Test.Tasty
import Test.Tasty.HUnit
import qualified Text.Pandoc.Builder as B
import qualified Text.Pandoc.Definition as B
import Text.Pandoc.Filter.Plot
import Text.Pandoc.Filter.Plot.Internal
import Text.Pandoc.JSON

defaultTestConfig :: Configuration
defaultTestConfig =
  defaultConfiguration
    { logVerbosity = Silent,
      logSink = StdErr
    }

-------------------------------------------------------------------------------
-- Test that plot files and source files are created when the filter is run
testFileCreation :: Toolkit -> TestTree
testFileCreation tk =
  testCase "writes output files in appropriate directory" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-file-creation-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    let cb = (addDirectory tempDir $ codeBlock tk (trivialContent tk))
    _ <- runPlotM Nothing defaultTestConfig $ make cb
    filesCreated <- length <$> listDirectory tempDir
    assertEqual "" 2 filesCreated

-------------------------------------------------------------------------------
-- Test that plot files and source files are created when the filter is run
-- and the path of the files involves spaces. See Issue #2
testFileCreationPathWithSpaces :: Toolkit -> TestTree
testFileCreationPathWithSpaces tk =
  testCase "writes output files in appropriate directory (with spaces)" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-file-creation-with-spaces-   -" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    let cb = (addDirectory tempDir $ codeBlock tk (trivialContent tk))
    _ <- runPlotM Nothing defaultTestConfig $ make cb
    filesCreated <- length <$> listDirectory tempDir
    assertEqual "" 2 filesCreated

-------------------------------------------------------------------------------
-- Test that pandoc-plot appropriately transforms code blocks that are
-- nested in other blocks (e.g. Divs)
testNestedCodeBlocks :: Toolkit -> TestTree
testNestedCodeBlocks tk =
  testCase "transforms code blocks nested in other blocks" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-nester-blocks-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    let block =
          Div mempty $
            singleton $
              addDirectory tempDir $
                codeBlock tk (trivialContent tk)
    _ <- runPlotM Nothing defaultTestConfig $ make block
    filesCreated <- length <$> listDirectory tempDir
    assertEqual "" 2 filesCreated
  where
    singleton :: a -> [a]
    singleton = return

-------------------------------------------------------------------------------
-- Test that included files are found within the source
testFileInclusion :: Toolkit -> TestTree
testFileInclusion tk =
  testCase "includes plot inclusions" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-file-inclusion-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    let cb =
          ( addPreamble (include tk) $
              addDirectory tempDir $ codeBlock tk (trivialContent tk)
          )
    _ <- runPlotM Nothing defaultTestConfig $ make cb
    inclusion <- readFile (include tk)
    sourcePath <- head . filter (isExtensionOf ".src.html") <$> listDirectory tempDir
    src <- readFile (tempDir </> sourcePath)
    assertIsInfix inclusion src
  where
    include Matplotlib = "tests/includes/matplotlib.py"
    include PlotlyPython = "tests/includes/plotly-python.py"
    include PlotlyR = "tests/includes/plotly-r.r"
    include Matlab = "tests/includes/matlabplot.m"
    include Mathematica = "tests/includes/mathplot.m"
    include Octave = "tests/includes/octave.m"
    include GGPlot2 = "tests/includes/ggplot2.r"
    include GNUPlot = "tests/includes/gnuplot.gp"
    include Graphviz = "tests/includes/graphviz.dot"
    include Bokeh = "tests/includes/bokeh.py"
    include Plotsjl = "tests/includes/plotsjl.jl"
    include PlantUML = "tests/includes/plantuml.txt"
    include SageMath = "tests/includes/sagemath.sage"

-------------------------------------------------------------------------------
-- Test that the files are saved in the appropriate format
testSaveFormat :: Toolkit -> TestTree
testSaveFormat tk =
  testCase "saves in the appropriate format" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-safe-format-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir
    let fmt = head (supportedSaveFormats tk)
        cb =
          ( addSaveFormat fmt $
              addDirectory tempDir $ codeBlock tk (trivialContent tk)
          )
    _ <- runPlotM Nothing defaultTestConfig $ make cb
    numberjpgFiles <-
      length <$> filter (isExtensionOf (extension fmt))
        <$> listDirectory tempDir
    assertEqual "" numberjpgFiles 1

-------------------------------------------------------------------------------
-- Test that the appropriate error is raised when trying to save figures
-- in an incompatible format
testSaveFormatIncompatibility :: Toolkit -> TestTree
testSaveFormatIncompatibility tk = 
  testCase "raises the appropriate error on save format incompatibility" $ do
    let allSaveFormats = enumFromTo minBound maxBound :: [SaveFormat]
        incompatibleFormats = S.toList $ S.difference (S.fromList allSaveFormats) (S.fromList $ supportedSaveFormats tk)
    if null incompatibleFormats
      then return ()
      else do
        let fmt = head incompatibleFormats
            cb = addSaveFormat fmt $ codeBlock tk (trivialContent tk)

        result <- runPlotM Nothing defaultTestConfig $ makeEither cb
        let expectedCheck :: Either PandocPlotError a -> Bool
            expectedCheck (Left (IncompatibleSaveFormatError fmt' tk')) = (fmt' == fmt) && (tk' == tk)
            expectedCheck _ = False
        assertBool "" (expectedCheck result)

-------------------------------------------------------------------------------
-- Test that it is possible to not render source links in captions
testWithSource :: Toolkit -> TestTree
testWithSource tk =
  testCase "appropriately omits links to source code" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-caption-links-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    let expected = "caption content"
        noSource =
          addWithSource False $
            addDirectory tempDir $
              addCaption expected $
                codeBlock tk (trivialContent tk)
        withSource =
          addWithSource True $
            addDirectory tempDir $
              addCaption expected $
                codeBlock tk (trivialContent tk)
    blockNoSource <- runPlotM Nothing defaultTestConfig $ make noSource
    blockWithSource <- runPlotM Nothing defaultTestConfig $ make withSource

    -- In the case where source=false, the caption is used verbatim.
    -- Otherwise, links will be appended to the caption; hence, the caption
    -- is no longer equal to the initial value
    assertEqual "" (B.toList $ fromString expected) (extractCaption blockNoSource)
    assertNotEqual "" (B.toList $ fromString expected) (extractCaption blockWithSource)
  where
    extractCaption (B.Para blocks) = extractImageCaption . head $ blocks
    extractCaption _ = mempty

    extractImageCaption (Image _ c _) = c
    extractImageCaption _ = mempty

-------------------------------------------------------------------------------
-- Test that it is possible to change the source code label in captions
testSourceLabel :: Toolkit -> TestTree
testSourceLabel tk =
  testCase "appropriately changes the source code label" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-source-label-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    -- Note that this test requires that the actual caption be empty
    -- so that the caption is only the source code label
    let withSource =
          addWithSource True $
            addDirectory tempDir $
              addCaption mempty $ -- This test requires that the actual caption be empty
                codeBlock tk (trivialContent tk)
    blockWithSource <- runPlotM Nothing defaultTestConfig {sourceCodeLabel = "Test label"} $ make withSource

    -- The caption will look like [Space, Str "(", Link ... ]. Hence, we skip the first elements with (!!)
    let resultCaption = linkLabel $ (!! 2) . B.toList $ extractCaption blockWithSource
    assertEqual "" (B.str "Test label") resultCaption
  where
    extractCaption (B.Para blocks) = extractImageCaption . head $ blocks
    extractCaption _ = mempty

    extractImageCaption (Image _ c _) = B.fromList c
    extractImageCaption _ = mempty

    linkLabel (B.Link _ ils _) = B.fromList ils
    linkLabel _ = mempty

-------------------------------------------------------------------------------
-- Test that parameters in code blocks will override the defaults in configuration
testOverrideConfiguration :: Toolkit -> TestTree
testOverrideConfiguration tk =
  -- We set the default save format to JPG via the configuration,
  -- but set the code block parameter to PNG.
  -- Therefore, after the filter has been used, there should be one PNG file and
  -- no JPG files.
  testCase "code block attributes override configuration defaults" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-caption-links-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    let config =
          defaultTestConfig
            { defaultDirectory = tempDir,
              defaultSaveFormat = JPG
            }

    -- Not all toolkits support both save formats
    when
      ( JPG `elem` supportedSaveFormats tk
          && PNG `elem` supportedSaveFormats tk
      )
      $ do
        let cb =
              addDirectory tempDir $
                addSaveFormat PNG $
                  codeBlock tk (trivialContent tk)
        _ <- runPlotM Nothing config $ make cb

        numberPngFiles <-
          length <$> filter (isExtensionOf (extension PNG))
            <$> listDirectory (defaultDirectory config)
        numberJpgFiles <-
          length <$> filter (isExtensionOf (extension JPG))
            <$> listDirectory (defaultDirectory config)
        assertEqual "" numberPngFiles 1
        assertEqual "" numberJpgFiles 0

-------------------------------------------------------------------------------
-- Test that Markdown bold formatting in captions is correctly rendered
testMarkdownFormattingCaption1 :: Toolkit -> TestTree
testMarkdownFormattingCaption1 tk =
  testCase "appropriately parses captions 1" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-caption-parsing1-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    -- Note that this test is fragile, in the sense that the expected result must be carefully
    -- constructed
    let expected = [B.Strong [B.Str "caption"]]
        cb =
          addDirectory tempDir $
            addCaption "**caption**" $
              codeBlock tk (trivialContent tk)
        fmt = B.Format "markdown"
    result <- runPlotM Nothing (defaultTestConfig {captionFormat = fmt}) $ make cb
    assertIsInfix expected (extractCaption result)
  where
    extractCaption (B.Para blocks) = extractImageCaption . head $ blocks
    extractCaption _ = mempty

    extractImageCaption (Image _ c _) = c
    extractImageCaption _ = mempty

-------------------------------------------------------------------------------
-- Test that Markdown bold formatting in captions is correctly rendered
testMarkdownFormattingCaption2 :: Toolkit -> TestTree
testMarkdownFormattingCaption2 tk =
  testCase "appropriately parses captions 2" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-caption-parsing2-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    -- Note that this test is fragile, in the sense that the expected result must be carefully
    -- constructed
    let expected = [Link ("", [], []) [Str "title"] ("https://google.com", "")]
        cb =
          addDirectory tempDir $
            addCaption "[title](https://google.com)" $
              codeBlock tk (trivialContent tk)
        fmt = B.Format "markdown"
    result <- runPlotM Nothing (defaultTestConfig {captionFormat = fmt}) $ make cb
    assertIsInfix expected (extractCaption result)
  where
    extractCaption (B.Para blocks) = extractImageCaption . head $ blocks
    extractCaption _ = mempty

    extractImageCaption (Image _ c _) = c
    extractImageCaption _ = mempty

-------------------------------------------------------------------------------
-- Test that Markdown bold formatting in captions is correctly rendered
testFigureWithoutCaption :: Toolkit -> TestTree
testFigureWithoutCaption tk =
  testCase "appropriately build an image if no caption" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-image-if-no-caption-" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    -- Note that this test is fragile, in the sense that the expected result must be carefully
    -- constructed
    let cb =
          addDirectory tempDir $ codeBlock tk (trivialContent tk)
        fmt = B.Format "markdown"
    result <- runPlotM Nothing (defaultTestConfig {captionFormat = fmt}) $ make cb
    assertEqual "" (Just mempty) (extractTitle result)
  where
    extractTitle (B.Para blocks) = extractImageCaption . head $ blocks
    extractTitle _ = Nothing

    extractImageCaption (Image _ _ (_, title)) = Just title
    extractImageCaption _ = Nothing


-------------------------------------------------------------------------------
-- Test that cleanOutpuDirs correctly cleans the output directory specified in a block.
testCleanOutputDirs :: Toolkit -> TestTree
testCleanOutputDirs tk =
  testCase "correctly cleans output directories" $ do
    let postfix = unpack . cls $ tk
    tempDir <- (</> "test-clean-output-dir" <> postfix) <$> getTemporaryDirectory
    ensureDirectoryExistsAndEmpty tempDir

    let cb =
          addDirectory tempDir $
            codeBlock tk (trivialContent tk)

    result <- runPlotM Nothing defaultTestConfig $ make cb
    cleanedDirs <- cleanOutputDirs defaultTestConfig cb

    assertEqual "" [tempDir] cleanedDirs

    outputDirExists <- doesDirectoryExist tempDir
    assertEqual "" outputDirExists False

-------------------------------------------------------------------------------
-- Test that toolkit checks failed when appropriate.
testChecksFail :: Toolkit -> TestTree
testChecksFail tk =
  testCase "script checks fail when appropriate" $ do
    assertChecksFail tk
  where
    assertChecksFail Matplotlib = do
      let postfix = unpack . cls $ tk
      tempDir <- (</> "test-checks" <> postfix) <$> getTemporaryDirectory
      ensureDirectoryExistsAndEmpty tempDir

      let cb = addDirectory tempDir $ codeBlock Matplotlib "plt.show()"
      result <- runPlotM Nothing defaultTestConfig $ makeEither cb
      let expectedCheck :: Either PandocPlotError a -> Bool
          expectedCheck (Left (ScriptChecksFailedError _)) = True
          expectedCheck _ = False
      assertBool "" (expectedCheck result)
    assertChecksFail _ = assertEqual "Test skipped" True True

codeBlock :: Toolkit -> Script -> Block
codeBlock tk script = CodeBlock (mempty, [cls tk], mempty) script

trivialContent :: Toolkit -> Script
trivialContent Matplotlib = "import matplotlib.pyplot as plt\n"
trivialContent PlotlyPython = "import plotly.graph_objects as go; fit = go.Figure()\n"
trivialContent PlotlyR = "library(plotly)\nfig <- plot_ly(midwest, x = ~percollege, color = ~state, type = \"box\")"
trivialContent Matlab = "figure('visible', 'off')\n"
trivialContent Mathematica = "\n"
trivialContent Octave = "figure('visible', 'off')\nplot (-10:0.1:10);"
trivialContent GGPlot2 = "library(ggplot2)\nggplot()\n"
trivialContent GNUPlot = "plot sin(x)"
trivialContent Graphviz = "digraph {A -> B [label=\"test\"];}"
trivialContent Bokeh =
  T.unlines
    [ "from bokeh.plotting import figure",
      "p = figure(title='simple line example')",
      "p.line([1,2,3,4], [5,6,7,8])"
    ]
trivialContent Plotsjl = "using Plots; x = 1:10; y = rand(10); plot(x, y);"
trivialContent PlantUML = "@startuml\nAlice -> Bob: test\n@enduml"
trivialContent SageMath = "G = plot(sin, 1, 10)"

addCaption :: String -> Block -> Block
addCaption caption (CodeBlock (id', cls, attrs) script) =
  CodeBlock (id', cls, attrs ++ [(tshow CaptionK, pack caption)]) script

addDirectory :: FilePath -> Block -> Block
addDirectory dir (CodeBlock (id', cls, attrs) script) =
  CodeBlock (id', cls, attrs ++ [(tshow DirectoryK, pack dir)]) script

addPreamble :: FilePath -> Block -> Block
addPreamble inclusionPath (CodeBlock (id', cls, attrs) script) =
  CodeBlock (id', cls, attrs ++ [(tshow PreambleK, pack inclusionPath)]) script

addSaveFormat :: SaveFormat -> Block -> Block
addSaveFormat saveFormat (CodeBlock (id', cls, attrs) script) =
  CodeBlock (id', cls, attrs ++ [(tshow SaveFormatK, pack . extension $ saveFormat)]) script

addDPI :: Int -> Block -> Block
addDPI dpi (CodeBlock (id', cls, attrs) script) =
  CodeBlock (id', cls, attrs ++ [(tshow DpiK, pack . show $ dpi)]) script

addWithSource :: Bool -> Block -> Block
addWithSource yn (CodeBlock (id', cls, attrs) script) =
  CodeBlock (id', cls, attrs ++ [(tshow WithSourceK, pack . show $ yn)]) script

-- | Assert that a file exists
assertFileExists :: HasCallStack => FilePath -> Assertion
assertFileExists filepath = do
  fileExists <- doesFileExist filepath
  unless fileExists (assertFailure msg)
  where
    msg = mconcat ["File ", filepath, " does not exist."]

-- | Assert not equal
assertNotEqual :: (HasCallStack, Eq a, Show a) => String -> a -> a -> Assertion
assertNotEqual msg expected actual =
  unless
    (expected /= actual)
    (assertFailure $ mconcat [msg, ": expected ", show expected, " but got ", show actual])

-- | Not available with GHC < 8.4
-- since this function was added in filepath-1.4.2
-- but GHC 8.2.2 comes with filepath-1.4.1.2
isExtensionOf :: String -> FilePath -> Bool
isExtensionOf ext@('.' : _) = isSuffixOf ext . takeExtensions
isExtensionOf ext = isSuffixOf ('.' : ext) . takeExtensions

-- | Assert that the first list is contained,
-- wholly and intact, anywhere within the second.
assertIsInfix :: (Eq a, Show a, HasCallStack) => [a] -> [a] -> Assertion
assertIsInfix xs ys = unless (xs `isInfixOf` ys) (assertFailure msg)
  where
    msg = mconcat ["Expected ", show xs, " to be an infix of ", show ys]

-- Ensure a directory is empty but exists.
ensureDirectoryExistsAndEmpty :: FilePath -> IO ()
ensureDirectoryExistsAndEmpty dir = do
  exists <- doesDirectoryExist dir
  if exists
    then removePathForcibly dir
    else return ()
  createDirectory dir

tshow :: Show a => a -> Text
tshow = pack . show