{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}

module Test.Method.ProtocolSpec where

import RIO (ByteString, Text, void)
import Test.Hspec
  ( Spec,
    anyErrorCall,
    before,
    context,
    describe,
    it,
    shouldReturn,
    shouldThrow,
  )
import Test.Method.Protocol
  ( ProtocolM,
    decl,
    dependsOn,
    lookupMock,
    protocol,
    thenReturn,
    verify,
    whenArgs,
  )

data Methods m where
  FindUser :: Methods (UserName -> IO (Maybe User))
  FindPassword :: Methods (UserName -> IO (Maybe HashedPassword))
  CreateUser :: Methods (UserName -> IO User)
  HashPassword :: Methods (PlainPassword -> IO HashedPassword)
  UpsertAuth :: Methods (UserId -> HashedPassword -> IO ())

data Service = Service
  { findUser :: UserName -> IO (Maybe User),
    findPassword :: UserName -> IO (Maybe HashedPassword),
    createUser :: UserName -> IO User,
    hashPassword :: PlainPassword -> IO HashedPassword,
    upsertAuth :: UserId -> HashedPassword -> IO ()
  }

doService :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doService Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      hpasswd <- hashPassword passwd
      user <- createUser usernm
      upsertAuth (userId user) hpasswd
      pure $ Just user

doServiceHashFirst :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceHashFirst Service {..} usernm passwd = do
  hpasswd <- hashPassword passwd
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      user <- createUser usernm
      upsertAuth (userId user) hpasswd
      pure $ Just user

doServiceWrongArgument :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongArgument Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      _ <- hashPassword passwd
      user <- createUser usernm
      upsertAuth (userId user) passwd
      pure $ Just user

doServiceWrongExtraCalls :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongExtraCalls Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      hpasswd <- hashPassword passwd
      hpasswd' <- hashPassword hpasswd
      user <- createUser usernm
      upsertAuth (userId user) hpasswd'
      pure $ Just user

doServiceWrongMissingCalls :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongMissingCalls Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      _ <- hashPassword passwd
      user <- createUser usernm
      pure $ Just user

doServiceWrongOrder :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongOrder Service {..} usernm passwd = do
  user <- createUser usernm
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      hpasswd <- hashPassword passwd
      upsertAuth (userId user) hpasswd
      pure $ Just user

doServiceWrongUnspecifiedMethod :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongUnspecifiedMethod Service {..} usernm _passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      Just hpasswd <- findPassword usernm
      user <- createUser usernm
      upsertAuth (userId user) hpasswd
      pure $ Just user

serviceProtocol :: UserName -> PlainPassword -> HashedPassword -> UserId -> ProtocolM Methods ()
serviceProtocol usernm passwd hpasswd userid = do
  i1 <- decl $ whenArgs FindUser (== usernm) `thenReturn` Nothing
  i2 <- decl $ whenArgs CreateUser (== usernm) `thenReturn` User usernm userid `dependsOn` [i1]
  i3 <- decl $ whenArgs HashPassword (== passwd) `thenReturn` hpasswd
  void $ decl $ whenArgs UpsertAuth ((== userid), (== hpasswd)) `thenReturn` () `dependsOn` [i2, i3]

deriving instance (Show (Methods m))

deriving instance (Eq (Methods m))

deriving instance (Ord (Methods m))

type UserName = Text

type PlainPassword = ByteString

type HashedPassword = ByteString

type UserId = Int

data Env = Env

data User = User {userName :: UserName, userId :: UserId}
  deriving (Eq, Ord, Show)

spec :: Spec
spec = describe "protocol" $ do
  let usernm = "user1"
      passwd = "password1"
      hpasswd = "hashed_password1"
      userid = 0
  let setup = do
        penv <- protocol $ serviceProtocol usernm passwd hpasswd userid
        pure
          ( penv,
            Service
              { findUser = lookupMock FindUser penv,
                findPassword = lookupMock FindPassword penv,
                createUser = lookupMock CreateUser penv,
                hashPassword = lookupMock HashPassword penv,
                upsertAuth = lookupMock UpsertAuth penv
              }
          )
  before setup $ do
    context "accept valid impl" $ do
      it "accept valid impl findUser first" $ \(penv, service) -> do
        doService service usernm passwd `shouldReturn` Just (User usernm userid)
        verify penv
      it "accept valid impl hash first" $ \(penv, service) -> do
        doServiceHashFirst service usernm passwd `shouldReturn` Just (User usernm userid)
        verify penv
    context "reject invalid impl" $ do
      it "invalid argument" $ \(_, service) ->
        doServiceWrongArgument service usernm passwd `shouldThrow` anyErrorCall
      it "extra calls" $ \(_, service) ->
        doServiceWrongExtraCalls service usernm passwd `shouldThrow` anyErrorCall
      it "missing calls" $ \(penv, service) -> do
        doServiceWrongMissingCalls service usernm passwd `shouldReturn` Just (User usernm userid)
        verify penv `shouldThrow` anyErrorCall
      it "wrong order" $ \(_, service) -> do
        doServiceWrongOrder service usernm passwd `shouldThrow` anyErrorCall
      it "unspecified method call" $ \(_, service) -> do
        doServiceWrongUnspecifiedMethod service usernm passwd `shouldThrow` anyErrorCall