{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE InstanceSigs       #-}

module Data.Api.Types where

import           Control.Exception.Safe
import           Control.Monad.IO.Class   (MonadIO, liftIO)
import           Control.Monad.Reader     (MonadReader (..), ReaderT (..))
import           Data.Aeson
import           Data.Aeson.TH
import           Data.Api.TestByteStrings
import           Data.ByteString.Lazy     (ByteString)
import           Data.Fixed
import           Data.Functor.Identity
import qualified Data.Map                 as M
import           Data.Maybe               (isJust, isNothing)
import           Data.Monoid              ((<>))
import           Data.Text                (Text)
import qualified Data.Text                as T
import           Data.Time                (Day)
import           Lens.Micro
import           Lens.Micro.TH
import           Network.HTTP.Conduit
import           Text.Casing              (camel, quietSnake)

data Environment
  = Sandbox
  | Development
  | Production deriving (Ord, Eq)

$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Environment)

instance Show Environment where
  show Sandbox     = "sandbox"
  show Development = "development"
  show Production  = "production"

class Monad m => PlaidHttp m where
  executePost :: ToJSON a => Text -> a -> m ByteString
  executeGet :: Text -> m ByteString

newtype PlaidError =
  PlaidError Text
  deriving Show
  deriving anyclass Exception

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''PlaidError)

newtype AccessToken =
  AccessToken
    { unAccessToken :: Text
    } deriving (Eq, Show)

instance ToJSON AccessToken where
  toJSON AccessToken {..} = String unAccessToken

instance FromJSON AccessToken where
  parseJSON = withText "AccessToken" $ \t ->
    return $ AccessToken t

newtype PublicToken =
  PublicToken
    { unPublicToken :: Text
    } deriving (Eq, Show)

instance ToJSON PublicToken where
  toJSON PublicToken {..} = String unPublicToken

instance FromJSON PublicToken where
  parseJSON = withText "PublicToken" $ \t ->
    return $ PublicToken t

newtype ClientId =
  ClientId
    { unClientId :: Text
    } deriving (Eq, Show)

instance ToJSON ClientId where
  toJSON ClientId {..} = String unClientId

instance FromJSON ClientId where
  parseJSON = withText "ClientId" $ \t ->
    return $ ClientId t

newtype Secret =
  Secret
    { unSecret :: Text
    } deriving (Eq, Show)

instance ToJSON Secret where
  toJSON Secret {..} = String unSecret

instance FromJSON Secret where
  parseJSON = withText "Secret" $ \t ->
    return $ Secret t

newtype InstitutionId =
  InstitutionId
    { unInstitutionId :: Text
    } deriving (Eq, Show)

instance ToJSON InstitutionId where
  toJSON InstitutionId {..} = String unInstitutionId

instance FromJSON InstitutionId where
  parseJSON = withText "InstitutionId" $ \t ->
    return $ InstitutionId t

newtype Url a = Url { unUrl :: Text }

instance ToJSON (Url a) where
  toJSON Url {..} = String unUrl

instance FromJSON (Url a) where
  parseJSON = withText "Url" $ \t ->
    return $ Url t

type PublicKey     = Text
type AccessKey     = Text
type PlaidProduct  = Text
type Institution = Text
type AccountNumber = Text
type SortCode = Text
type AccountId = Text
type Iban = Text
type Bic = Text
type RequestId = Text

data AuthGet
data GetBalance
data PublicTokenCreate
data PlaidTokenExchange
data PlaidTransactionsGet
data PlaidIdentityGet
data PlaidIncomeGet

data PlaidOptions =
  PlaidOptions
    { _plaidOptionsWebHook          :: Text
    , _plaidOptionsOverrideUsername :: Text
    , _plaidOptionsOverridePassword :: Text
    } deriving (Eq, Show)

makeLenses ''PlaidOptions
$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''PlaidOptions)

instance ToJSON PlaidOptions where
  toJSON b =
    object [ "webhook"           .= (b ^. plaidOptionsWebHook)
           , "override_username" .= (b ^. plaidOptionsOverrideUsername)
           , "override_password" .= (b ^. plaidOptionsOverridePassword)
           ]

data PlaidPaginationOptions =
  PlaidPaginationOptions
    { _plaidPaginationOptionsCount  :: Int
    , _plaidPaginationOptionsOffset :: Int
    } deriving (Eq, Show)

makeLenses ''PlaidPaginationOptions
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 23 }) ''PlaidPaginationOptions)

defPaginationOptions :: PlaidPaginationOptions
defPaginationOptions =
  PlaidPaginationOptions
    { _plaidPaginationOptionsCount  = 100
    , _plaidPaginationOptionsOffset = 0
    }

data PlaidEnv =
  PlaidEnv
    { _plaidEnvPublicKey   :: PublicKey
    , _plaidEnvClientId    :: ClientId
    , _plaidEnvSecret      :: Secret
    , _plaidEnvEnvironment :: Environment
    } deriving (Eq, Show)

makeLenses ''PlaidEnv
$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''PlaidEnv)

data PlaidBody a
  = PlaidBody
      { _plaidBodyEnv               :: PlaidEnv
      , _plaidBodyPublicToken       :: Maybe PublicToken
      , _plaidBodyAccessToken       :: Maybe AccessToken
      , _plaidBodyOptions           :: Maybe PlaidOptions
      , _plaidBodyPaginationOptions :: Maybe PlaidPaginationOptions
      , _plaidBodyInstitutionId     :: Maybe InstitutionId
      , _plaidBodyInitialProducts   :: Maybe [PlaidProduct]
      , _plaidBodyStartDate         :: Maybe Day
      , _plaidBodyEndDate           :: Maybe Day
      } deriving (Eq, Show)

makeLenses ''PlaidBody

instance ToJSON (PlaidBody GetBalance) where
  toJSON b =
    if isNothing (b ^. plaidBodyOptions)
      then
        object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
               , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
               , "access_token" .= toJSON (b ^. plaidBodyAccessToken)
               ]
      else
        object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
               , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
               , "access_token" .= (b ^. plaidBodyAccessToken)
               , "options"      .= (b ^. plaidBodyOptions)
               ]

instance ToJSON (PlaidBody PublicTokenCreate) where
  toJSON b =
    if isNothing (b ^. plaidBodyOptions)
      then
        object [ "institution_id"   .= (b ^. plaidBodyInstitutionId)
               , "public_key"       .= (b ^. plaidBodyEnv . plaidEnvPublicKey)
               , "initial_products" .= (b ^. plaidBodyInitialProducts)
               ]
      else
        object [ "institution_id"   .= (b ^. plaidBodyInstitutionId)
               , "public_key"       .= (b ^. plaidBodyEnv . plaidEnvPublicKey)
               , "initial_products" .= (b ^. plaidBodyInitialProducts)
               , "options"          .= toJSON (b ^. plaidBodyOptions)
               ]

instance ToJSON (PlaidBody AuthGet) where
  toJSON b =
    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
           , "access_token" .= (b ^. plaidBodyAccessToken)
           ]

instance ToJSON (PlaidBody PlaidTokenExchange) where
  toJSON b =
    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
           , "public_token" .= (b ^. plaidBodyPublicToken)
           ]

instance ToJSON (PlaidBody PlaidTransactionsGet) where
  toJSON b =
    object $ [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
             , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
             , "access_token" .= (b ^. plaidBodyAccessToken)
             , "start_date"   .= (b ^. plaidBodyStartDate)
             , "end_date"     .= (b ^. plaidBodyEndDate)
             ] <> perhapsSendObject
    where
      perhapsSendObject =
        ["options" .= toJSON (b ^. plaidBodyPaginationOptions) | isJust (b ^. plaidBodyPaginationOptions)]

instance ToJSON (PlaidBody PlaidIdentityGet) where
  toJSON b =
    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
           , "access_token" .= (b ^. plaidBodyAccessToken)
           ]

instance ToJSON (PlaidBody PlaidIncomeGet) where
  toJSON b =
    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)
           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)
           , "access_token" .= (b ^. plaidBodyAccessToken)
           ]

-- | Main type used as the carrier for 'PlaidHttp' instance.
-- We use mtl style constraints in library functions and at
-- the end call 'runPlaid' to run all operations inside of IO.
-- You never need to construct value of this type it is just used
-- as a monad stack to carry Plaid operations.
newtype Plaid a =
  Plaid
    { unPlaid :: ReaderT PlaidEnv IO a
    } deriving newtype ( Functor
                       , Applicative
                       , Monad
                       , MonadReader PlaidEnv
                       , MonadIO
                       , MonadThrow
                       )

-- | Carrier type used for testing
newtype PlaidTest a =
  PlaidTest
    { unPlaidTest :: Identity a
    } deriving newtype ( Functor
                       , Applicative
                       , Monad
                       )

runPlaid :: PlaidEnv -> Plaid a -> IO a
runPlaid env = flip runReaderT env . unPlaid

runTestPlaid :: PlaidTest a -> a
runTestPlaid = runIdentity . unPlaidTest

data CurrencyCode
  = USD
  | EUR
  deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''CurrencyCode)

data Balance =
  Balance
    { balanceAvailable              :: Double
    , balanceCurrent                :: Double
    , balanceLimit                  :: Maybe Double
    , balanceIsoCurrencyCode        :: CurrencyCode
    , balanceUnofficialCurrencyCode :: Maybe CurrencyCode
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Balance)

data Account =
  Account
    { accountAccountId    :: Text
    , accountBalances     :: Balance
    , accountMask         :: Text
    , accountName         :: Text
    , accountOfficialName :: Text
    , accountSubtype      :: Text
    , accountType         :: Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Account)

data Ach =
  Ach
    { achAccount     :: AccountNumber
    , achAccountId   :: AccountId
    , achRouting     :: Text
    , achWireRouting :: Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Ach)

data Error =
  Error
    { errorType      :: Text
    , errorCode      :: Text
    , errorMessage   :: Text
    , displayMessage :: Maybe Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 5 }) ''Error)

data Item =
  Item
    { itemAvailableProducts :: [Text]
    , itemBilledProducts    :: [Text]
    , itemError             :: Maybe Error
    , itemInstitutionId     :: InstitutionId
    , itemItemId            :: Text
    , itemWebhook           :: Maybe Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Item)

data Eft =
  Eft
    { eftAccount     :: AccountNumber
    , eftAccountId   :: AccountId
    , eftInstitution :: Institution
    , eftBranch      :: Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Eft)

data International =
  International
    { internationalAccountId :: AccountId
    , internationalBic       :: Bic
    , internationalIban      :: Iban
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''International)

data Bacs =
  Bacs
    { bacsAccount   :: AccountNumber
    , bacsAccountId :: AccountId
    , bacsSortCode  :: SortCode
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Bacs)

data Numbers =
  Numbers
    { numbersAch           :: [Ach]
    , numbersEft           :: [Eft]
    , numbersInternational :: [International]
    , numbersBacs          :: [Bacs]
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Numbers)

data TransactionType
  = Digital
  | Place
  | Special
  | Unresolved
  deriving (Eq, Show)

instance ToJSON TransactionType where
  toJSON Digital    = String "digital"
  toJSON Place      = String "place"
  toJSON Special    = String "special"
  toJSON Unresolved = String "unresolved"

instance FromJSON TransactionType where
  parseJSON "digital"    = pure Digital
  parseJSON "place"      = pure Place
  parseJSON "special"    = pure Special
  parseJSON "unresolved" = pure Unresolved
  parseJSON _            = mempty

data TransactionLocation =
  TransactionLocation
    { transactionLocationAddress       :: Maybe Text
    , transactionLocationCity          :: Maybe Text
    , transactionLocationRegion        :: Maybe Text
    , transactionLocationPostalCode    :: Maybe Text
    , transactionLocationPostalCountry :: Maybe Text
    , transactionLocationPostalLat     :: Maybe Double
    , transactionLocationPostalLon     :: Maybe Double
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 19 }) ''TransactionLocation)

data PaymentMeta =
  PaymentMeta
    { paymentMetaReferenceNumber :: Maybe Text
    , paymentMetaPpdId           :: Maybe Text
    , paymentMetaPayee           :: Maybe Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''PaymentMeta)

data Transaction =
  Transaction
    { transactionAccountId              :: AccountId
    , transactionAmount                 :: Double
    , transactionIsoCurrencyCode        :: CurrencyCode
    , transactionUnofficialCurrencyCode :: Maybe CurrencyCode
    , transactionCategory               :: Maybe [Text]
    , transactionCategoryId             :: Maybe Text
    , transactionTransactionType        :: Maybe TransactionType
    , transactionName                   :: Text
    , transactionDate                   :: Day
    , transactionLocation               :: TransactionLocation
    , transactionPending                :: Bool
    , transactionPaymentMeta            :: PaymentMeta
    , transactionPendingTransactionId   :: Maybe Text
    , transactionAccountOwner           :: Maybe Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Transaction)

data PlaidTransactionsGetResponse =
  PlaidTransactionsGetResponse
    { _plaidTransactionsGetResponseAccounts     :: [Account]
    , _plaidTransactionsGetResponseTransactions :: [Transaction]
    , _plaidTransactionsItem                    :: Item
    , _plaidTransactionsTotalTransactions       :: Int
    , _plaidTransactionsRequestId               :: Text
    } deriving (Eq, Show)

makeLenses ''PlaidTransactionsGetResponse

$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 29 }) ''PlaidTransactionsGetResponse)

instance FromJSON PlaidTransactionsGetResponse where
  parseJSON = withObject "PlaidTransactionsGetResponse" $ \o ->
    PlaidTransactionsGetResponse
      <$> o .: "accounts"
      <*> o .: "transactions"
      <*> o .: "item"
      <*> o .: "total_transactions"
      <*> o .: "request_id"

data PlaidAuthGetResponse =
  PlaidAuthGetResponse
    { _plaidAuthGetResponseAccounts  :: [Account]
    , _plaidAuthGetResponseNumbers   :: Numbers
    , _plaidAuthGetResponseItem      :: Item
    , _plaidAuthGetResponseRequestId :: RequestId
    } deriving (Eq, Show)

makeLenses ''PlaidAuthGetResponse

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 21 }) ''PlaidAuthGetResponse)

data PlaidPublicTokenResponse =
  PlaidPublicTokenResponse
    { _plaidPublicTokenResponsePublicToken :: PublicToken
    , _plaidPublicTokenResponseRequestId   :: Text
    } deriving (Eq, Show)

makeLenses ''PlaidPublicTokenResponse

instance ToJSON PlaidPublicTokenResponse where
  toJSON PlaidPublicTokenResponse {..} =
    object [ "public_token" .= _plaidPublicTokenResponsePublicToken
           , "request_id"   .= _plaidPublicTokenResponseRequestId
           ]

instance FromJSON PlaidPublicTokenResponse where
  parseJSON = withObject "PlaidPublicTokenResponse" $ \o ->
    PlaidPublicTokenResponse
      <$> o .: "public_token"
      <*> o .: "request_id"

data PlaidAccessTokenResponse =
  PlaidAccessTokenResponse
    { _plaidAccessTokenResponseAccessToken :: AccessToken
    , _plaidAccessTokenResponseRequestId   :: Text
    , _plaidAccessTokenResponseItemId      :: Text
    } deriving (Eq, Show)

makeLenses ''PlaidAccessTokenResponse

instance ToJSON PlaidAccessTokenResponse where
  toJSON PlaidAccessTokenResponse {..} =
    object [ "access_token" .= _plaidAccessTokenResponseAccessToken
           , "request_id"   .= _plaidAccessTokenResponseRequestId
           , "item_id"      .= _plaidAccessTokenResponseItemId
           ]

instance FromJSON PlaidAccessTokenResponse where
  parseJSON = withObject "PlaidAccessTokenResponse" $ \o ->
    PlaidAccessTokenResponse
      <$> o .: "access_token"
      <*> o .: "request_id"
      <*> o .: "item_id"

data PhoneNumber =
  PhoneNumber
    { _phoneNumberData    :: Text
    , _phoneNumberPrimary :: Bool
    , _phoneNumberType    :: Text
    } deriving (Eq, Show)

makeLenses ''PhoneNumber

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''PhoneNumber)

data Email =
  Email
    { _emailData    :: Text
    , _emailPrimary :: Bool
    , _emailType    :: Text
    } deriving (Eq, Show)

makeLenses ''Email

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 6 }) ''Email)

data Address =
  Address
    { _addressCity       :: Text
    , _addressRegion     :: Text
    , _addressStreet     :: Text
    , _addressPostalCode :: Text
    , _addressCountry    :: Maybe Text
    } deriving (Eq, Show)

makeLenses ''Address

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''Address)

data Addresses =
  Addresses
    { _addressesData    :: Address
    , _addressesPrimary :: Bool
    } deriving (Eq, Show)

makeLenses ''Addresses

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''Addresses)

data Owners =
  Owners
    { _ownersAddresses    :: [Addresses]
    , _ownersEmails       :: [Email]
    , _ownersNames        :: [Text]
    , _ownersPhoneNumbers :: [PhoneNumber]
    } deriving (Eq, Show)

makeLenses ''Owners

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Owners)

data Accounts =
  Accounts
    { _accountsAccountId    :: Text
    , _accountsBalances     :: Balance
    , _accountsMask         :: Text
    , _accountsName         :: Text
    , _accountsOfficialName :: Maybe Text
    , _accountsOwners       :: [Owners]
    , _accountsSubtype      :: Text
    , _accountsType         :: Text
    } deriving (Eq, Show)

$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 9 }) ''Accounts)

data PlaidIdentityGetResponse =
  PlaidIdentityGetResponse
    { _plaidIdentityGetResponseAccounts :: [Accounts]
    , _plaidIdentityGetItem             :: Item
    , _plaidIdentityGetRequestId        :: Text
    } deriving (Eq, Show)

makeLenses ''PlaidIdentityGetResponse

$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 25 }) ''PlaidIdentityGetResponse)

instance FromJSON PlaidIdentityGetResponse where
  parseJSON = withObject "PlaidIdentityGetResponse" $ \o ->
    PlaidIdentityGetResponse
      <$> o .: "accounts"
      <*> o .: "item"
      <*> o .: "request_id"

data IncomeStream =
  IncomeStream
    { _incomeStreamMonthlyIncome :: Fixed E2
    , _incomeStreamConfidence    :: Fixed E2
    , _incomeStreamDays          :: Fixed E2
    , _incomeStreamName          :: Text
    } deriving (Eq, Show)

makeLenses ''IncomeStream

$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 13 }) ''IncomeStream)

data Income =
  Income
    { _incomeLastYearIncome                      :: Fixed E2
    , _incomeLastYearIncomeBeforeTax             :: Fixed E2
    , _incomeProjectedYearlyIncome               :: Fixed E2
    , _incomeProjectedYearlyIncomeBeforeTax      :: Fixed E2
    , _incomeIncomeStreams                       :: [IncomeStream]
    , _incomeMaxNumberOfOverlappingIncomeStreams :: Fixed E2
    , _incomeNumberOfIncomeStreams               :: Fixed E2
    } deriving (Eq, Show)

makeLenses ''Income

$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 7 }) ''Income)

baseUrl :: Environment -> Url Environment
baseUrl e = Url $ T.pack $ "https://" <> show e <> ".plaid.com"

envUrl :: Environment -> Text
envUrl = unUrl . baseUrl

validInstitutionIds :: [InstitutionId]
validInstitutionIds =
  InstitutionId <$>
    [ "ins_1"
    , "ins_2"
    , "ins_3"
    , "ins_4"
    , "ins_5"
    , "ins_6"
    , "ins_7"
    , "ins_9"
    , "ins_10"
    , "ins_11"
    , "ins_13"
    , "ins_14"
    , "ins_15"
    , "ins_16"
    , "ins_19"
    , "ins_20"
    , "ins_21"
    , "ins_23"
    , "ins_24"
    , "ins_27"
    , "ins_29"
    ]

-- | Map used for testing different plaid endpoints
requestMap :: M.Map Text ByteString
requestMap =
  M.fromList
    [ ("/auth/get", responseAuthGet)
    , ("/transactions/get", responseTransactionsGet)
    , ("/public_token/create", responsePublicTokenCreate)
    , ("/item/public_token/exchange", responsePublicTokenExchange)
    , ("/identity/get", identityJson)
    ]

-- | Instances
instance PlaidHttp Plaid where
  executeGet :: Text -> Plaid ByteString
  executeGet url = do
    request <- parseUrlThrow (T.unpack url)
    m <- liftIO $ newManager tlsManagerSettings
    response <- httpLbs request m
    return $ responseBody response

  executePost :: ToJSON a => Text -> a -> Plaid ByteString
  executePost url postBody = do
    initialRequest <- parseUrlThrow (T.unpack url)
    m <- liftIO (newManager tlsManagerSettings)
    let request =
          initialRequest
          { method = "POST"
          , requestBody = RequestBodyLBS (encode postBody)
          , requestHeaders = [ ("Content-Type", "application/json") ]
          }
    response <- httpLbs request m
    return $ responseBody response

instance PlaidHttp PlaidTest where
  executeGet :: Text -> PlaidTest ByteString
  executeGet url =
    case M.lookup url requestMap of
      Nothing -> return ""
      Just bs -> return bs

  executePost :: ToJSON a => Text -> a -> PlaidTest ByteString
  executePost url _postBody =
    case M.lookup url requestMap of
      Nothing -> return ""
      Just bs -> return bs

instance Semigroup PlaidOptions where
  (<>) _ b = b

instance Monoid PlaidOptions where
  mempty =
    PlaidOptions
      { _plaidOptionsWebHook          = ""
      , _plaidOptionsOverrideUsername = ""
      , _plaidOptionsOverridePassword = ""
      }
  mappend _ b = b

instance Semigroup (PlaidBody a) where
  (<>) _ b = b

instance Monoid (PlaidBody a) where
  mempty =
    let env =
          PlaidEnv
            { _plaidEnvPublicKey   = ""
            , _plaidEnvClientId    = ClientId ""
            , _plaidEnvSecret      = Secret ""
            , _plaidEnvEnvironment = Sandbox
            }
    in
      PlaidBody
        { _plaidBodyEnv               = env
        , _plaidBodyPublicToken       = Nothing
        , _plaidBodyAccessToken       = Nothing
        , _plaidBodyOptions           = Nothing
        , _plaidBodyPaginationOptions = Nothing
        , _plaidBodyInstitutionId     = Nothing
        , _plaidBodyInitialProducts   = Nothing
        , _plaidBodyStartDate         = Nothing
        , _plaidBodyEndDate           = Nothing
        }
  mappend _ b = b