{-# LANGUAGE OverloadedStrings #-}
{- |
   Module      : Tests.Writers.AnnotatedTable
   Copyright   : 2020 Christian Despres
   License     : GNU GPL, version 2 or above

   Maintainer  : Christian Despres <christian.j.j.despres@gmail.com>
   Stability   : alpha
   Portability : portable

Tests for the table helper functions.
-}
module Tests.Writers.AnnotatedTable
  ( tests
  )
where

import           Prelude
import qualified Data.Foldable                 as F
import qualified Data.List.NonEmpty            as NonEmpty
import           Test.Tasty
import           Test.Tasty.HUnit               ( testCase
                                                , (@?=)
                                                )
import           Test.Tasty.QuickCheck          ( QuickCheckTests(..)
                                                , Property
                                                , Testable
                                                , conjoin
                                                , forAll
                                                , testProperty
                                                , (===)
                                                , vectorOf
                                                , choose
                                                , arbitrary
                                                , elements
                                                )
import           Text.Pandoc.Arbitrary          ( )
import           Text.Pandoc.Builder
import qualified Text.Pandoc.Writers.AnnotatedTable
                                               as Ann

tests :: [TestTree]
tests = [testGroup "toTable" $ testAnnTable <> annTableProps]

getSpec :: Ann.Cell -> [ColSpec]
getSpec (Ann.Cell colspec _ _) = F.toList colspec

catHeaderSpec :: Ann.HeaderRow -> [ColSpec]
catHeaderSpec (Ann.HeaderRow _ _ x) = concatMap getSpec x

catBodySpec :: Ann.BodyRow -> [ColSpec]
catBodySpec (Ann.BodyRow _ _ x y) = concatMap getSpec x <> concatMap getSpec y

-- Test if the first list can be obtained from the second by deleting
-- elements from it.
isSubsetOf :: Eq a => [a] -> [a] -> Bool
isSubsetOf (x : xs) (y : ys) | x == y    = isSubsetOf xs ys
                             | otherwise = isSubsetOf (x : xs) ys
isSubsetOf [] _  = True
isSubsetOf _  [] = False

testAnnTable :: [TestTree]
testAnnTable =
  [testCase "annotates a sample table properly" $ generated @?= expected]
 where
  spec1 = (AlignRight, ColWidthDefault)
  spec2 = (AlignLeft, ColWidthDefault)
  spec3 = (AlignCenter, ColWidthDefault)
  spec  = [spec1, spec2, spec3]

  cl a h w = Cell (a, [], []) AlignDefault h w []
  rws = map $ Row nullAttr
  th  = TableHead nullAttr . rws
  tb n x y = TableBody nullAttr n (rws x) (rws y)
  tf           = TableFoot nullAttr . rws
  initialHeads = [[cl "a" 1 1, cl "b" 3 2], [cl "c" 2 2, cl "d" 1 1]]
  initialTB1   = tb 1
                    [[], [cl "e" 5 1, cl "f" (-7) 0]]
                    [[cl "g" 4 3, cl "h" 4 3], [], [emptyCell]]
  initialTB2 = tb 2 [] [[cl "i" 4 3, cl "j" 4 3]]
  generated  = Ann.toTable nullAttr
                           emptyCaption
                           spec
                           (th initialHeads)
                           [initialTB1, initialTB2]
                           (tf initialHeads)

  acl al n a h w =
    Ann.Cell (NonEmpty.fromList al) n $ Cell (a, [], []) AlignDefault h w []
  emptyAnnCell al n = acl al n "" 1 1
  ahrw    = Ann.HeaderRow nullAttr
  abrw    = Ann.BodyRow nullAttr
  ath     = Ann.TableHead nullAttr
  atb     = Ann.TableBody nullAttr
  atf     = Ann.TableFoot nullAttr

  finalTH = ath
    [ ahrw 0 [acl [spec1] 0 "a" 1 1, acl [spec2, spec3] 1 "b" 2 2]
    , ahrw 1 [acl [spec1] 0 "c" 1 1]
    ]
  finalTB1 = atb
    1
    [ ahrw
      2
      [emptyAnnCell [spec1] 0, emptyAnnCell [spec2] 1, emptyAnnCell [spec3] 2]
    , ahrw
      3
      [acl [spec1] 0 "e" 1 1, acl [spec2] 1 "f" 1 1, emptyAnnCell [spec3] 2]
    ]
    [ abrw 4 [acl [spec1] 0 "g" 3 1] [acl [spec2, spec3] 1 "h" 3 2]
    , abrw 5 []                      []
    , abrw 6 []                      []
    ]
  finalTB2 =
    atb 2 [] [abrw 7 [acl [spec1, spec2] 0 "i" 1 2] [acl [spec3] 2 "j" 1 1]]
  finalTF = atf
    [ ahrw 8 [acl [spec1] 0 "a" 1 1, acl [spec2, spec3] 1 "b" 2 2]
    , ahrw 9 [acl [spec1] 0 "c" 1 1]
    ]
  expected =
    Ann.Table nullAttr emptyCaption spec finalTH [finalTB1, finalTB2] finalTF

withColSpec :: Testable prop => ([ColSpec] -> prop) -> Property
withColSpec = forAll arbColSpec
 where
  arbColSpec = do
    cs <- choose (1 :: Int, 6)
    vectorOf
      cs
      ((,) <$> arbitrary <*> elements
        [ColWidthDefault, ColWidth (1 / 3), ColWidth 0.25]
      )

annTableProps :: [TestTree]
annTableProps =
  localOption (QuickCheckTests 50)
    <$> [ testProperty "normalizes like the table builder"  propBuilderAnnTable
        , testProperty "has valid final cell columns"       propColNumber
        , testProperty "has valid first row column data"    propFirstRowCols
        , testProperty "has valid all row column data"      propColSubsets
        , testProperty "has valid cell column data lengths" propCellColLengths
        ]

-- The property that Ann.toTable will normalize a table identically to
-- the table builder. This should mean that Ann.toTable is at least as
-- rigorous as Builder.table in that respect without repeating those
-- tests here (see the pandoc-types Table tests for examples).
propBuilderAnnTable :: TableHead -> [TableBody] -> TableFoot -> Property
propBuilderAnnTable th tbs tf = withColSpec $ \cs ->
  convertTable (table emptyCaption cs th tbs tf)
    === convertAnnTable (Ann.toTable nullAttr emptyCaption cs th tbs tf)
 where
  convertTable blks = case toList blks of
    [Table _ _ colspec a b c] -> Right (colspec, a, b, c)
    x                         -> Left x
  convertAnnTable x = case Ann.fromTable x of
    (_, _, colspec, a, b, c) -> Right (colspec, a, b, c)

-- The property of Ann.toTable that if the last cell in the first row
-- of a table section has ColSpan w and ColNumber n, then w + n is the
-- width of the table.
propColNumber :: TableHead -> [TableBody] -> TableFoot -> Property
propColNumber th tbs tf = withColSpec $ \cs ->
  let twidth = length cs
      Ann.Table _ _ _ ath atbs atf =
          Ann.toTable nullAttr emptyCaption cs th tbs tf
  in  conjoin
        $  [colNumTH twidth ath]
        <> (colNumTB twidth <$> atbs)
        <> [colNumTF twidth atf]
 where
  colNumTH n (Ann.TableHead _ rs) = firstly (isHeaderValid n) rs
  colNumTB n (Ann.TableBody _ _ rs ts) =
    firstly (isHeaderValid n) rs && firstly (isBodyValid n) ts
  colNumTF n (Ann.TableFoot _ rs) = firstly (isHeaderValid n) rs

  isHeaderValid n (Ann.HeaderRow _ _ x) = isSegmentValid n x
  isBodyValid n (Ann.BodyRow _ _ _ x) = isSegmentValid n x

  firstly f (x : _) = f x
  firstly _ []      = True
  lastly f [x     ] = f x
  lastly f (_ : xs) = lastly f xs
  lastly _ []       = True

  isSegmentValid twidth cs =
    flip lastly cs
      $ \(Ann.Cell _ (Ann.ColNumber n) (Cell _ _ _ (ColSpan w) _)) ->
          n + w == twidth

-- The property of an Ann.Table from Ann.toTable that if the NonEmpty
-- ColSpec data of the cells in the first row of a table section are
-- concatenated, the result should equal the [ColSpec] of the entire
-- table.
propFirstRowCols :: TableHead -> [TableBody] -> TableFoot -> Property
propFirstRowCols th tbs tf = withColSpec $ \cs ->
  let Ann.Table _ _ _ ath atbs atf =
          Ann.toTable nullAttr emptyCaption cs th tbs tf
  in  conjoin
        $  [firstRowTH cs ath]
        <> (firstRowTB cs <$> atbs)
        <> [firstRowTF cs atf]
 where
  firstly f (x : _) = f x
  firstly _ []      = True

  firstHeaderValid cs = firstly $ \r -> cs == catHeaderSpec r
  firstBodyValid cs = firstly $ \r -> cs == catBodySpec r

  firstRowTH cs (Ann.TableHead _ rs) = firstHeaderValid cs rs
  firstRowTB cs (Ann.TableBody _ _ rs ts) =
    firstHeaderValid cs rs && firstBodyValid cs ts
  firstRowTF cs (Ann.TableFoot _ rs) = firstHeaderValid cs rs

-- The property that in any row in an Ann.Table from Ann.toTable, the
-- NonEmpty ColSpec annotations on cells, when concatenated, form a
-- subset (really sublist) of the [ColSpec] of the entire table.
propColSubsets :: TableHead -> [TableBody] -> TableFoot -> Property
propColSubsets th tbs tf = withColSpec $ \cs ->
  let Ann.Table _ _ _ ath atbs atf =
          Ann.toTable nullAttr emptyCaption cs th tbs tf
  in  conjoin
        $  subsetTH cs ath
        <> concatMap (subsetTB cs) atbs
        <> subsetTF cs atf
 where
  subsetTH cs (Ann.TableHead _ rs) = map (subsetHeader cs) rs
  subsetTB cs (Ann.TableBody _ _ rs ts) =
    map (subsetHeader cs) rs <> map (subsetBody cs) ts
  subsetTF cs (Ann.TableFoot _ rs) = map (subsetHeader cs) rs

  subsetHeader cs r = catHeaderSpec r `isSubsetOf` cs
  subsetBody cs r = catBodySpec r `isSubsetOf` cs

-- The property that in any cell in an Ann.Table from Ann.toTable, the
-- NonEmpty ColSpec annotation on a cell is equal in length to its
-- ColSpan.
propCellColLengths :: TableHead -> [TableBody] -> TableFoot -> Property
propCellColLengths th tbs tf = withColSpec $ \cs ->
  let Ann.Table _ _ _ ath atbs atf =
          Ann.toTable nullAttr emptyCaption cs th tbs tf
  in  conjoin $ cellColTH ath <> concatMap cellColTB atbs <> cellColTF atf
 where
  cellColTH (Ann.TableHead _ rs) = concatMap cellColHeader rs
  cellColTB (Ann.TableBody _ _ rs ts) =
    concatMap cellColHeader rs <> concatMap cellColBody ts
  cellColTF (Ann.TableFoot _ rs) = concatMap cellColHeader rs

  cellColHeader (Ann.HeaderRow _ _ x) = fmap validLength x
  cellColBody (Ann.BodyRow _ _ x y) = fmap validLength x <> fmap validLength y

  validLength (Ann.Cell colspec _ (Cell _ _ _ (ColSpan w) _)) =
    length colspec == w