{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import qualified Conduit as C
import Control.Exception
import qualified Data.ByteString.Char8 as B
import Data.CSV.Conduit
import Data.CSV.Conduit.Conversion
import Data.CSV.Conduit.Types
import qualified Data.Map as Map
import qualified Data.Map.Ordered as OMap
import Data.Monoid as M
import Data.Text (Text)
import qualified Data.Vector as V
import System.Directory
import Test.Framework (Test, defaultMain, testGroup)
import Test.Framework.Providers.HUnit
import Test.HUnit (assertFailure, (@=?), (@?=))

main :: IO ()
main = defaultMain tests

tests :: [Test]
tests =
  [ testGroup "Basic Ops" baseTests,
    testGroup "decodeCSV" decodeCSVTests
  ]

baseTests :: [Test]
baseTests =
  [ testCase "mapping with id works" test_identityMap,
    testCase "simple parsing works" (test_simpleParse testFile1),
    testCase "simple parsing works for Mac-Excel" (test_simpleParse testFile3),
    testCase "simple parsing works for Windows-Excel" (test_simpleParse testFile4),
    testCase "fails parsing gracefully" test_parseFail,
    testCase "OrderedMap" test_orderedMap
  ]

decodeCSVTests :: [Test]
decodeCSVTests =
  [ testCase "parses a CSV" $ do
      let efoos = decodeCSV defCSVSettings ("Foo\nfoo" :: B.ByteString)
      case efoos :: Either SomeException (V.Vector (Named Foo)) of
        Left e -> assertFailure (show e)
        Right foos -> V.fromList [Named Foo] @=? foos,
    testCase "eats parse errors, evidently" $ do
      let efoos = decodeCSV defCSVSettings ("Foo\nbad" :: B.ByteString)
      case efoos :: Either SomeException (V.Vector (Named Foo)) of
        Left e -> assertFailure (show e)
        Right foos -> M.mempty @=? foos
  ]

data Foo = Foo deriving (Show, Eq)

instance FromNamedRecord Foo where
  parseNamedRecord nr = do
    s <- nr .: "Foo"
    case s of
      "foo" -> pure Foo
      _ -> fail ("Expected \"foo\" but got " <> B.unpack s)

instance ToNamedRecord Foo where
  toNamedRecord Foo = namedRecord ["Foo" .= ("foo" :: B.ByteString)]

test_identityMap :: IO ()
test_identityMap = do
  _ <- runResourceT $ mapCSVFile csvSettings f testFile2 outFile
  f1 <- readFile testFile2
  f2 <- readFile outFile
  f1 @=? f2
  removeFile outFile
  where
    outFile = "test/testOut.csv"
    f :: Row Text -> [Row Text]
    f = return

test_simpleParse :: FilePath -> IO ()
test_simpleParse fp = do
  (d :: V.Vector (MapRow B.ByteString)) <- readCSVFile csvSettings fp
  V.mapM_ assertRow d
  where
    assertRow r = v3 @=? (v1 + v2)
      where
        v1 = readBS $ r Map.! "Col2"
        v2 = readBS $ r Map.! "Col3"
        v3 = readBS $ r Map.! "Sum"

test_parseFail :: IO ()
test_parseFail = do
  (d :: V.Vector (MapRow B.ByteString)) <- readCSVFile csvSettings testXLS
  errored <- catch (V.mapM_ assertRow d >> pure False) handler
  if errored then pure () else assertFailure "readCSVFile shouldn't read XLS"
  where
    handler :: ErrorCall -> IO Bool
    handler _ = pure True
    assertRow r = v3 @=? (v1 + v2)
      where
        v1 = readBS $ r Map.! "Col2"
        v2 = readBS $ r Map.! "Col3"
        v3 = readBS $ r Map.! "Sum"

test_orderedMap :: IO ()
test_orderedMap = do
  unorderedRes <-
    C.runConduit $
      C.yieldMany [unorderedRow]
        C..| writeHeaders defCSVSettings
        C..| C.foldC
  unorderedRes @?= ("\"a\",\"b\"\n\"aval\",\"bval\"\n" :: B.ByteString)
  orderedRes <-
    C.runConduit $
      C.yieldMany [orderedRow]
        C..| writeHeadersOrdered defCSVSettings
        C..| C.foldC
  orderedRes @?= ("\"b\",\"a\"\n\"bval\",\"aval\"\n" :: B.ByteString)
  where
    orderedRow :: OrderedMapRow Text
    orderedRow = OMap.fromList pairs
    unorderedRow :: MapRow Text
    unorderedRow = Map.fromList pairs
    pairs = [("b", "bval"), ("a", "aval")]

csvSettings :: CSVSettings
csvSettings = defCSVSettings {csvQuoteCharAndStyle = Just ('`', DontQuoteEmpty)}

testFile1, testFile2, testFile3, testFile4 :: FilePath
testFile1 = "test/test.csv"
testFile2 = "test/test.csv"
testFile3 = "test/test-mac-excel.csv"
testFile4 = "test/test-windows-excel.csv"

testXLS :: FilePath
testXLS = "test/test.xls"

readBS :: B.ByteString -> Int
readBS = read . B.unpack
