-- allows automatic derivation from Entity type class
{-# LANGUAGE DeriveAnyClass #-}

module OneToManySpec
  ( test,
    spec,
  )
where

import           Data.Maybe                     (fromJust)
import           Database.GP.GenericPersistence
import           Database.HDBC
import           Database.HDBC.Sqlite3
import           GHC.Generics
import           Test.Hspec

-- `test` is here so that this module can be run from GHCi on its own.  It is
-- not needed for automatic spec discovery. (start up stack repl --test to bring up ghci and have access to all the test functions)
test :: IO ()
test = hspec spec

prepareDB :: IO Conn
prepareDB = do
  conn <- connect SQLite <$> connectSqlite3 ":memory:"
  setupTableFor @Article conn
  setupTableFor @Author conn
  return conn

data Article = Article
  { articleID :: Int,
    title     :: String,
    authorId  :: Int,
    year      :: Int
  }
  deriving (Generic, Entity, Show, Eq) -- automatically derives Entity

data Author = Author
  { authorID :: Int,
    name     :: String,
    address  :: String,
    articles :: [Article]
  }
  deriving (Generic, Show, Eq)

instance Entity Author where
  fieldsToColumns :: [(String, String)]                  -- ommitting the articles field, 
  fieldsToColumns =                                      -- as this can not be mapped to a single column
    [ ("authorID", "authorID"),
      ("name", "name"),
      ("address", "address")
    ]

  fromRow :: Conn -> [SqlValue] -> IO Author
  fromRow conn row = do
    let authID = head row                                 -- authorID is the first column
    articlesBy <- retrieveAllWhere conn "authorId" authID -- retrieve all articles by this author
    return rawAuthor {articles = articlesBy}              -- add the articles to the author
    where
      rawAuthor = Author (col 0) (col 1) (col 2) []       -- create the author from row (w/o articles)
      col i = fromSql (row !! i)                          -- helper function to convert SqlValue to Haskell type

  toRow :: Conn -> Author -> IO [SqlValue]
  toRow conn a = do
    mapM_ (persist conn) (articles a)                     -- persist all articles of this author (update or insert)
    return [toSql (authorID a),                           -- return the author as a list of SqlValues
            toSql (name a), toSql (address a)]


article1 :: Article
article1 =
  Article
    { articleID = 1,
      title = "Persistence without Boilerplate",
      authorId = 1,
      year = 2018
    }

article2 :: Article
article2 =
  Article
    { articleID = 2,
      title = "Boilerplate for Dummies",
      authorId = 2,
      year = 2020
    }

article3 :: Article
article3 =
  Article
    { articleID = 3,
      title = "The return of the boilerplate",
      authorId = 2,
      year = 2022
    }

arthur :: Author
arthur =
  Author
    { authorID = 2,
      name = "Arthur Miller",
      address = "Denver",
      articles = [article2, article3]
    }

spec :: Spec
spec = do
  describe "Handling of 1:N References" $ do
    it "works like a charm" $ do
      conn <- prepareDB

      insert conn arthur
      insert conn article1

      authors <- retrieveAll conn :: IO [Author]
      length authors `shouldBe` 1

      articles' <- retrieveAll conn :: IO [Article]
      length articles' `shouldBe` 3

      author2 <- retrieveById conn "2" :: IO (Maybe Author)
      fromJust author2 `shouldBe` arthur
      length (articles $ fromJust author2) `shouldBe` 2
