{-|
Module      : Web.Framework.Plzwrk.MockJSVal
Description : Mock browser for testing
Copyright   : (c) Mike Solomon 2020
License     : GPL-3
Maintainer  : mike@meeshkan.com
Stability   : experimental
Portability : POSIX, Windows

This module exports a mock browser called
@defaultInternalBrowser@ used in plzwrk's tests
and that can be used in your unit tests as well.
-}
module Web.Framework.Plzwrk.MockJSVal
  ( MockJSVal(..)
  , makeMockBrowser
  , defaultInternalBrowser
  , makeMockBrowserWithContext
  )
where

import           Data.Aeson                     ( FromJSON )
import           Data.ByteString.Internal       ( ByteString )
import           Data.HashMap.Strict     hiding ( foldr
                                                , null
                                                )
import           Data.IORef
import           Data.List                      ( elemIndex )
import           Prelude                 hiding ( lookup )
import           Web.Framework.Plzwrk.Base
import           Web.Framework.Plzwrk.Browserful

data LogEvent = ListenerReceived String Int
    | AddedAsListenerTo Int
    | AttributeReceived String String
    | ChildReceived Int
    | AddedAsChildTo Int
    | RemovedNode Int
    | RemovedAsNodeFrom Int
    | RemovedListener String Int
    | RemovedAsListenerFrom Int
    | CreatedElement Int
    | CreatedTextNode Int
    | InsertedChildBefore Int Int
    | InsertedAsChildBefore Int Int
    | ElementAddedBefore Int
    | GotElementById
    | MadeCallback Int
    | FreeCallback Int
    deriving (Show, Eq)


data MockAttributes = MockAttributes
  { _d_attrs  :: HashMap String String
  , _d_events :: HashMap String MockJSVal
  }
  deriving Show

data MockJSVal = MockJSElement Int String MockAttributes [MockJSVal] [LogEvent]
    | MockJSTextNode Int String [LogEvent]
    | MockJSFunction Int (MockJSVal -> IO ()) [LogEvent]
    | MockJSObject Int (HashMap String Int) [LogEvent]
    | MockJSString Int String [LogEvent]
    | MockJSDouble Int Double [LogEvent]
    | MockJSInt Int Int [LogEvent]
    | MockJSBool Int Bool [LogEvent]
    | MockJSByteString Int ByteString [LogEvent]
    | MockJSArray Int [Int] [LogEvent]
    | MockMouseEvent Int

instance Show MockJSVal where
  show (MockJSElement a b c d e) =
    show a
      <> " "
      <> " "
      <> show b
      <> " "
      <> show c
      <> " "
      <> show d
      <> " "
      <> show e
  show (MockJSTextNode   a b c) = show a <> " " <> show b <> " " <> show c
  show (MockJSFunction   a _ c) = show a <> " " <> show c
  show (MockJSObject     a b c) = show a <> " " <> show b <> " " <> show c
  show (MockJSString     a b c) = show a <> " " <> show b <> " " <> show c
  show (MockJSDouble     a b c) = show a <> " " <> show b <> " " <> show c
  show (MockJSInt        a b c) = show a <> " " <> show b <> " " <> show c
  show (MockJSBool       a b c) = show a <> " " <> show b <> " " <> show c
  show (MockJSByteString a b c) = show a <> " " <> show b <> " " <> show c
  show (MockJSArray      a b c) = show a <> " " <> show b <> " " <> show c
  show (MockMouseEvent a      ) = show a

_withNewLog :: MockJSVal -> [LogEvent] -> MockJSVal
_withNewLog (MockJSElement a b c d _) log = MockJSElement a b c d log
_withNewLog (MockJSTextNode   a b _ ) log = MockJSTextNode a b log
_withNewLog (MockJSFunction   a b _ ) log = MockJSFunction a b log
_withNewLog (MockJSObject     a b _ ) log = MockJSObject a b log
_withNewLog (MockJSString     a b _ ) log = MockJSString a b log
_withNewLog (MockJSDouble     a b _ ) log = MockJSDouble a b log
_withNewLog (MockJSBool       a b _ ) log = MockJSBool a b log
_withNewLog (MockJSInt        a b _ ) log = MockJSInt a b log
_withNewLog (MockJSByteString a b _ ) log = MockJSByteString a b log
_withNewLog (MockJSArray      a b _ ) log = MockJSArray a b log

_withNewAttrs :: MockJSVal -> MockAttributes -> MockJSVal
_withNewAttrs (MockJSElement n tg _ chlds log) newat =
  MockJSElement n tg newat chlds log
_withNewAttrs a _ = a

_withNewKids :: MockJSVal -> [MockJSVal] -> MockJSVal
_withNewKids (MockJSElement n tg attrs _ log) newKids =
  MockJSElement n tg attrs newKids log
_withNewKids a _ = a

_ptr :: MockJSVal -> Int
_ptr (MockJSElement a _ _ _ _) = a
_ptr (MockJSTextNode   a _ _ ) = a
_ptr (MockJSFunction   a _ _ ) = a
_ptr (MockJSObject     a _ _ ) = a
_ptr (MockJSString     a _ _ ) = a
_ptr (MockJSDouble     a _ _ ) = a
_ptr (MockJSBool       a _ _ ) = a
_ptr (MockJSInt        a _ _ ) = a
_ptr (MockJSByteString a _ _ ) = a
_ptr (MockJSArray      a _ _ ) = a

_eventTargetAddEventListener
  :: MockJSVal
  -> String
  -> MockJSVal
  -> IO (MockAttributes, [LogEvent], [LogEvent])
_eventTargetAddEventListener (MockJSElement n _ (MockAttributes atts lstns) _ logn) evt fn@(MockJSFunction m _ logm)
  = pure
    $ ( MockAttributes atts $ insert evt fn lstns
      , logn <> [ListenerReceived evt m]
      , logm <> [AddedAsListenerTo n]
      )
_eventTargetAddEventListener _ _ _ =
  error "Can only add event listener to element"

_elementSetAttribute
  :: MockJSVal -> String -> String -> IO (MockAttributes, [LogEvent])
_elementSetAttribute (MockJSElement n _ (MockAttributes atts lstns) _ logn) nm attr
  = pure
    $ ( MockAttributes (insert nm attr atts) lstns
      , logn <> [AttributeReceived nm attr]
      )
_elementSetAttribute _ _ _ = error "Can only add event listener to element"

_nodeAppendChild
  :: MockJSVal -> MockJSVal -> IO ([MockJSVal], [LogEvent], [LogEvent])
_nodeAppendChild (MockJSElement n _ _ kids logn) kid@(MockJSElement m _ _ _ logm)
  = pure
    $ (kids <> [kid], logn <> [ChildReceived m], logm <> [AddedAsChildTo n])
_nodeAppendChild (MockJSElement n _ _ kids logn) kid@(MockJSTextNode m _ logm)
  = pure
    $ (kids <> [kid], logn <> [ChildReceived m], logm <> [AddedAsChildTo n])
_nodeAppendChild _ _ = error "Can only append element to element"

__nodeRemoveChild
  :: Int
  -> [MockJSVal]
  -> [LogEvent]
  -> MockJSVal
  -> Int
  -> [LogEvent]
  -> IO ([MockJSVal], [LogEvent], [LogEvent])
__nodeRemoveChild n kids logn kid m logm = maybe
  (error ("Existing item " <> show m <> " not child of " <> show n))
  (\x ->
    pure
      $ ( take x kids <> drop (x + 1) kids
        , logn <> [RemovedNode m]
        , logm <> [RemovedAsNodeFrom n]
        )
  )
  (elemIndex (_ptr kid) (fmap _ptr kids))

_nodeRemoveChild
  :: MockJSVal -> MockJSVal -> IO ([MockJSVal], [LogEvent], [LogEvent])
_nodeRemoveChild (MockJSElement n _ _ kids logn) kid@(MockJSElement m _ _ _ logm)
  = __nodeRemoveChild n kids logn kid m logm
_nodeRemoveChild (MockJSElement n _ _ kids logn) kid@(MockJSTextNode m _ logm)
  = __nodeRemoveChild n kids logn kid m logm
_nodeRemoveChild _ _ = error "Can only remove element from element"

_eventTargetRemoveEventListener
  :: MockJSVal
  -> String
  -> MockJSVal
  -> IO (MockAttributes, [LogEvent], [LogEvent])
_eventTargetRemoveEventListener (MockJSElement n _ (MockAttributes atts lstns) _ logn) evt fn@(MockJSFunction m _ logm)
  = maybe
    (error ("Listener " <> show m <> " not child of " <> show n))
    (\x ->
      pure
        $ ( MockAttributes atts $ delete evt lstns
          , logn <> [RemovedListener evt m]
          , logm <> [RemovedAsListenerFrom n]
          )
    )
    (lookup evt lstns)
_eventTargetRemoveEventListener _ _ _ =
  error "Can only add event listener to element"

_nodeInsertBeforeInternal
  :: Int
  -> [MockJSVal]
  -> [LogEvent]
  -> MockJSVal
  -> Int
  -> [LogEvent]
  -> MockJSVal
  -> Int
  -> [LogEvent]
  -> IO
       ( [MockJSVal]
       , [LogEvent]
       , [LogEvent]
       , [LogEvent]
       )
_nodeInsertBeforeInternal n kids logn newI m logm existingI l logl = maybe
  (error ("Existing item " <> show l <> " not child of " <> show n))
  (\x ->
    pure
      $ ( take x kids <> [newI] <> drop x kids
        , logn <> [InsertedChildBefore m l]
        , logm <> [InsertedAsChildBefore n l]
        , logl <> [ElementAddedBefore m]
        )
  )
  (elemIndex (_ptr existingI) (fmap _ptr kids))


_nodeInsertBefore
  :: MockJSVal
  -> MockJSVal
  -> MockJSVal
  -> IO ([MockJSVal], [LogEvent], [LogEvent], [LogEvent])
_nodeInsertBefore (MockJSElement n _ _ kids logn) newI@(MockJSElement m _ _ _ logm) existingI@(MockJSElement l _ _ _ logl)
  = _nodeInsertBeforeInternal n kids logn newI m logm existingI l logl
_nodeInsertBefore (MockJSElement n _ _ kids logn) newI@(MockJSTextNode m _ logm) existingI@(MockJSElement l _ _ _ logl)
  = _nodeInsertBeforeInternal n kids logn newI m logm existingI l logl
_nodeInsertBefore (MockJSElement n _ _ kids logn) newI@(MockJSElement m _ _ _ logm) existingI@(MockJSTextNode l _ logl)
  = _nodeInsertBeforeInternal n kids logn newI m logm existingI l logl
_nodeInsertBefore (MockJSElement n _ _ kids logn) newI@(MockJSTextNode m _ logm) existingI@(MockJSTextNode l _ logl)
  = _nodeInsertBeforeInternal n kids logn newI m logm existingI l logl
_nodeInsertBefore _ _ _ = error "Can only append element to element"

_elementTagName :: MockJSVal -> IO String
_elementTagName (MockJSElement _ tag _ _ _) = return tag
_elementTagName _ = error "Can only get tag of element"

_nodeTextContent :: MockJSVal -> IO String
_nodeTextContent (MockJSTextNode _ txt _) = return txt
_nodeTextContent _ = error "Can only get text content of text node"


_nodeChildNodes :: MockJSVal -> IO [Int]
_nodeChildNodes (MockJSElement _ _ _ kids _) = return $ fmap _ptr kids
_nodeChildNodes _ = error "Can only get children of element"

__freeCallback :: MockJSVal -> IO [LogEvent]
__freeCallback (MockJSFunction n _ log) = pure (log <> [FreeCallback n])
__freeCallback _                        = error "Can only free function"

dummyClick :: MockJSVal -> IO ()
-- todo give real number


dummyClick (MockJSFunction _ f _) = f $ MockMouseEvent (-1)


_htmlElemenetClick :: MockJSVal -> IO ()
_htmlElemenetClick (MockJSElement _ _ (MockAttributes _ evts) _ _) = do
  let oc = lookup "click" evts
  maybe (pure ()) (\x -> dummyClick x) oc

_htmlElemenetClick _ = error "Can only free function"


--------------



data MockBrowserInternal = MockBrowserInternal
  { unBrowser :: HashMap Int MockJSVal
  , unCtr     :: Int
  }
  deriving Show

look :: IORef MockBrowserInternal -> Int -> IO MockJSVal
look env elt = do
  r <- readIORef env
  let bz = unBrowser r
  maybe (error $ "Cannot find object pointer in env: " <> (show elt))
        (\x -> return x)
        (lookup elt bz)

incr :: IORef MockBrowserInternal -> IO Int
incr env = do
  r <- readIORef env
  let ctr = unCtr r
  writeIORef env $ r { unCtr = ctr + 1 }
  return ctr


wrt :: IORef MockBrowserInternal -> Int -> MockJSVal -> IO ()
wrt env elt v = do
  r <- readIORef env
  let bz = unBrowser r
  writeIORef env $ r { unBrowser = insert elt v bz }

_'eventTargetAddEventListener
  :: IORef MockBrowserInternal -> Int -> String -> Int -> IO ()
_'eventTargetAddEventListener env elt evt fn = do
  _elt                            <- look env elt
  _fn                             <- look env fn
  (newAttrs, newLogElt, newLogFn) <- _eventTargetAddEventListener _elt evt _fn
  wrt env elt $ _withNewLog (_withNewAttrs _elt newAttrs) newLogElt
  wrt env fn $ _withNewLog _fn newLogFn

_'nodeAppendChild :: IORef MockBrowserInternal -> Int -> Int -> IO ()
_'nodeAppendChild env parent kid = do
  _parent                            <- look env parent
  _kid                               <- look env kid
  (newKids, newLogParent, newLogKid) <- _nodeAppendChild _parent _kid
  wrt env parent $ _withNewLog (_withNewKids _parent newKids) newLogParent
  wrt env kid $ _withNewLog _kid newLogKid

_'documentCreateElement :: IORef MockBrowserInternal -> String -> IO Int
_'documentCreateElement env tg = do
  i <- incr env
  let elt =
        MockJSElement i tg (MockAttributes empty empty) [] [CreatedElement i]
  wrt env i elt
  return i

----------------------
_jsValFrom
  :: (Int -> s -> [LogEvent] -> MockJSVal)
  -> IORef MockBrowserInternal
  -> s
  -> IO Int
_jsValFrom trans env toConv = do
  i <- incr env
  let elt = trans i toConv []
  wrt env i elt
  return i

_'jsValFromArray = _jsValFrom MockJSArray
_'jsValFromBool = _jsValFrom MockJSBool
_'jsValFromByteString = _jsValFrom MockJSByteString
_'jsValFromDouble = _jsValFrom MockJSDouble
_'jsValFromInt = _jsValFrom MockJSInt
_'jsValFromString = _jsValFrom MockJSString

----------------------

_'mathRandom :: IORef MockBrowserInternal -> IO Double
_'mathRandom _ = pure 0.5

_'consoleLog :: IORef MockBrowserInternal -> Int -> IO ()
_'consoleLog env v = do
  _v <- look env v
  print v


_'documentCreateTextNode :: IORef MockBrowserInternal -> String -> IO Int
_'documentCreateTextNode env txt = do
  i <- incr env
  let elt = MockJSTextNode i txt [CreatedTextNode i]
  wrt env i elt
  return i

_'getPropertyAsOpaque
  :: IORef MockBrowserInternal -> Int -> String -> IO (Maybe Int)
_'getPropertyAsOpaque env i s
  | s == "tagName" = do
    tn <- _'elementTagName env i
    _v <- _'jsValFromString env tn
    (return . Just) $ _v
  | s == "textContent" = do
    tc <- _'nodeTextContent env i
    _v <- _'jsValFromString env tc
    (return . Just) $ _v
  | s == "childNodes" = do
    cn <- _'nodeChildNodes env i
    _v <-_'jsValFromArray env cn
    (return . Just) $ _v
  | otherwise =  error
  $  "This property is not implemented yet in MockJSVal: "
  <> s

_'invokeOn0 :: IORef MockBrowserInternal -> Int -> String -> IO Int
_'invokeOn0 env i s
  | s == "click" = do
    _'htmlElementClick env i
    return (negate 1)
  | otherwise =  error
  $  "This function is not implemented yet in MockJSVal: "
  <> s

_'invokeOn1 :: IORef MockBrowserInternal -> Int -> String -> Int -> IO Int
_'invokeOn1 env i s v
  | s == "appendChild" = do
    _'nodeAppendChild env i v
    return (negate 1)
  | s == "removeChild" = do
    _'nodeRemoveChild env i v
    return (negate 1)
  | otherwise =  error
  $  "This function is not implemented yet in MockJSVal: "
  <> s

_'invokeOn2
  :: IORef MockBrowserInternal -> Int -> String -> Int -> Int -> IO Int
_'invokeOn2 env i s k v
  | s == "setAttribute" = do
    _k <- _'castToString env k
    _v <- _'castToString env v
    maybe
      (error "key not a string")
      (\__k -> maybe
        (error "value not a string")
        (\__v -> do
          _'elementSetAttribute env i __k __v
          return (negate 1)
        )
        _v
      )
      _k
  | s == "addEventListener" = do
    _k <- _'castToString env k
    maybe
      (error "key not a string")
      (\__k -> do
        _'eventTargetAddEventListener env i __k v
        return (negate 1)
      )
      _k
  | s == "removeEventListener" = do
    _k <- _'castToString env k
    maybe
      (error "key not a string")
      (\__k -> do
        _'eventTargetRemoveEventListener env i __k v
        return (negate 1)
      )
      _k
  | s == "insertBefore" = do
    _'nodeInsertBefore env i k v
    return (negate 1)
  | otherwise =  error
  $  "This function is not implemented yet in MockJSVal: "
  <> s

_'setValue :: IORef MockBrowserInternal -> Int -> String -> Int -> IO ()
_'setValue env o k v = do
  _o <- _'castToObject env o
  maybe
    (error "Not an object")
    (\x -> do
      __o <- look env o
      let (MockJSObject _ _ lg) = __o
      wrt env o $ MockJSObject o (insert k v x) lg
    )
    _o


_'fetch :: IORef MockBrowserInternal -> String -> RequestInit Int -> IO Int
_'fetch env _ _ = do
  _o <- _'makeObject env
  return _o -- return an empty object for now

_'elementTagName :: IORef MockBrowserInternal -> Int -> IO String
_'elementTagName env elt = do
  _elt <- look env elt
  _elementTagName _elt

_'nodeChildNodes :: IORef MockBrowserInternal -> Int -> IO [Int]
_'nodeChildNodes env elt = do
  _elt <- look env elt
  _nodeChildNodes _elt

_'nodeTextContent :: IORef MockBrowserInternal -> Int -> IO String
_'nodeTextContent env elt = do
  _elt <- look env elt
  _nodeTextContent _elt

_'_freeCallback :: IORef MockBrowserInternal -> Int -> IO ()
_'_freeCallback env fn = do
  _fn    <- look env fn
  newLog <- __freeCallback _fn
  wrt env fn $ _withNewLog _fn newLog

_'htmlElementClick :: IORef MockBrowserInternal -> Int -> IO ()
_'htmlElementClick env elt = do
  _elt <- look env elt
  _htmlElemenetClick _elt

idEq :: String -> MockJSVal -> Bool
idEq txt (MockJSElement _ _ (MockAttributes atts _) _ _) =
  Just txt == (lookup "id" atts)
idEq _ _ = False

_'documentBody :: IORef MockBrowserInternal -> IO Int
_'documentBody ref = do
  mb <- readIORef ref
  let browser = unBrowser mb
  pt <- maybe (error "No body.") (\x -> pure $ _ptr x) $ lookup 0 browser
  return pt

_'documentHead :: IORef MockBrowserInternal -> IO Int
_'documentHead ref = pure (-1) -- need to implement in mock?


_documentGetElementByIdInternal :: MockJSVal -> String -> [Int]
_documentGetElementByIdInternal jsv@(MockJSElement _ _ _ ch _) txt =
  if (idEq txt jsv)
    then [_ptr jsv]
    else (foldr (++) [] $ fmap (\x -> _documentGetElementByIdInternal x txt) ch)
_documentGetElementByIdInternal _ _ = []

_'documentGetElementById
  :: IORef MockBrowserInternal -> String -> IO (Maybe Int)
_'documentGetElementById env txt = do
  body  <- _'documentBody env
  _body <- look env body
  let elts = _documentGetElementByIdInternal _body txt
  return $ if (null elts) then (Nothing) else (Just $ head elts)

_'nodeInsertBefore :: IORef MockBrowserInternal -> Int -> Int -> Int -> IO ()
_'nodeInsertBefore env parent newItem existingItem = do
  _parent       <- look env parent
  _newItem      <- look env newItem
  _existingItem <- look env existingItem
  (newKids, newLogParent, newLogNewItem, newLogExistingItem) <-
    _nodeInsertBefore _parent _newItem _existingItem
  wrt env parent $ _withNewLog (_withNewKids _parent newKids) newLogParent
  wrt env newItem $ _withNewLog _newItem newLogNewItem
  wrt env existingItem $ _withNewLog _existingItem newLogExistingItem

_'_makeHaskellCallback :: IORef MockBrowserInternal -> (Int -> IO ()) -> IO Int
_'_makeHaskellCallback env cb = do
  i <- incr env
  let elt = MockJSFunction i (\x -> cb $ _ptr x) [MadeCallback i]
  wrt env i elt
  return i

_'makeObject :: IORef MockBrowserInternal -> IO Int
_'makeObject env = do
  i <- incr env
  let elt = MockJSObject i empty []
  wrt env i elt
  return i


_'nodeRemoveChild :: IORef MockBrowserInternal -> Int -> Int -> IO ()
_'nodeRemoveChild env parent kid = do
  _parent                            <- look env parent
  _kid                               <- look env kid
  (newKids, newLogParent, newLogKid) <- _nodeRemoveChild _parent _kid
  wrt env parent $ _withNewLog (_withNewKids _parent newKids) newLogParent
  wrt env kid $ _withNewLog _kid newLogKid

_'eventTargetRemoveEventListener
  :: IORef MockBrowserInternal -> Int -> String -> Int -> IO ()
_'eventTargetRemoveEventListener env elt evt fn = do
  _elt                            <- look env elt
  _fn                             <- look env fn
  (newAttrs, newLogElt, newLogFn) <- _eventTargetRemoveEventListener _elt
                                                                     evt
                                                                     _fn
  wrt env elt $ _withNewLog (_withNewAttrs _elt newAttrs) newLogElt
  wrt env fn $ _withNewLog _fn newLogFn

_'elementSetAttribute
  :: IORef MockBrowserInternal -> Int -> String -> String -> IO ()
_'elementSetAttribute env elt nm attr = do
  _elt               <- look env elt
  (newAttrs, newLog) <- _elementSetAttribute _elt nm attr
  wrt env elt $ _withNewLog (_withNewAttrs _elt newAttrs) newLog

_castable
  :: (MockJSVal -> IO v) -> IORef MockBrowserInternal -> Int -> IO (Maybe v)
_castable cst env elt = do
  _elt <- look env elt
  v    <- cst _elt
  pure $ Just v

_assertByteString :: MockJSVal -> IO ByteString
_assertByteString (MockJSByteString _ v _) = pure v
_assertByteString _                        = error "Not a ByteString"

_'castToByteString = _castable _assertByteString

_assertBool :: MockJSVal -> IO Bool
_assertBool (MockJSBool _ v _) = pure v
_assertBool _                  = error "Not a bool"

_'castToBool = _castable _assertBool

_assertDouble :: MockJSVal -> IO Double
_assertDouble (MockJSDouble _ v _) = pure v
_assertDouble _                    = error "Not a double"

_'castToDouble = _castable _assertDouble

_assertInt :: MockJSVal -> IO Int
_assertInt (MockJSInt _ v _) = pure v
_assertInt _                 = error "Not an int"

_'castToInt = _castable _assertInt

_assertArray :: MockJSVal -> IO [Int]
_assertArray (MockJSArray _ v _) = pure v
_assertArray _                   = error "Not an array"

_'castToArray = _castable _assertArray

_assertString :: MockJSVal -> IO String
_assertString (MockJSString _ v _) = pure v
_assertString _                    = error "Not an array"

_'castToString = _castable _assertString

_assertObject :: MockJSVal -> IO (HashMap String Int)
_assertObject (MockJSObject _ v _) = pure v
_assertObject _                    = error "Not an array"

_'castToObject = _castable _assertObject


_'defaultRequestInit = RequestInit { _ri_method      = Nothing
                                   , _ri_headers     = Nothing
                                   , _ri_body        = Nothing
                                   , _ri_mode        = Nothing
                                   , _ri_credentials = Nothing
                                   , _ri_cache       = Nothing
                                   , _ri_redirect    = Nothing
                                   , _ri_referrer    = Nothing
                                   , _ri_integrity   = Nothing
                                   }

makeMockBrowserWithContext :: IORef MockBrowserInternal -> IO (Browserful Int)
makeMockBrowserWithContext r = return Browserful
  { castToArray            = _'castToArray r
  , castToBool             = _'castToBool r
  , castToByteString       = _'castToByteString r
  , castToDouble           = _'castToDouble r
  , castToInt              = _'castToInt r
  , castToString           = _'castToString r
  , consoleLog             = _'consoleLog r
  , defaultRequestInit     = _'defaultRequestInit
  , documentCreateElement  = _'documentCreateElement r
  , documentCreateTextNode = _'documentCreateTextNode r
  , documentBody           = _'documentBody r
  , documentGetElementById = _'documentGetElementById r
  , documentHead           = _'documentHead r
  , fetch                  = _'fetch r
  , _freeCallback          = _'_freeCallback r
  , getPropertyAsOpaque    = _'getPropertyAsOpaque r
  , jsValFromArray         = _'jsValFromArray r
  , jsValFromBool          = _'jsValFromBool r
  , jsValFromByteString    = _'jsValFromByteString r
  , jsValFromDouble        = _'jsValFromDouble r
  , jsValFromInt           = _'jsValFromInt r
  , jsValFromString        = _'jsValFromString r
  , invokeOn0              = _'invokeOn0 r
  , makeObject             = _'makeObject r
  , setValue               = _'setValue r
  , invokeOn1              = _'invokeOn1 r
  , invokeOn2              = _'invokeOn2 r
  , _makeHaskellCallback   = _'_makeHaskellCallback r
  , mathRandom             = _'mathRandom r
  }

defaultInternalBrowser :: IO (IORef MockBrowserInternal)
defaultInternalBrowser = do
  let body = MockJSElement 0
                           "body"
                           (MockAttributes empty empty)
                           []
                           [CreatedElement 0]
  newIORef MockBrowserInternal { unBrowser = singleton 0 body, unCtr = 1 }

makeMockBrowser :: IO (Browserful Int)
makeMockBrowser = do
  rf <- defaultInternalBrowser
  makeMockBrowserWithContext rf