... This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
... If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

(.using
 [library
  [lux (.except)
   [abstract
    ["[0]" monad]]
   [concurrency
    ["[0]" future (.only Future) (.use "[1]#[0]" monad)]]
   [control
    ["[0]" io (.only IO)]]
   [error
    ["[0]" try (.only Try) (.use "[1]#[0]" functor)]
    ["[0]" exception]]
   [data
    ["[0]" product]
    ["[0]" text (.only)
     ["%" \\injection]]
    [collection
     ["[0]" list]]]
   [math
    ["[0]" random]
    [number
     [/64
      ["n" natural]
      ["i" integer]]]]
   [macro
    ["[0]" expansion]
    ["[0]" template]]
   [test
    ["_" property (.only Test)]
    ["[0]" unit]]]]
 [\\library
  ["[0]" / (.only)
   [//
    [file (.only Path)]
    ["[0]" environment
     ["[1]" \\projection (.only Environment)]]]]])

(template.with [<name> <command> <type> <prep>]
  [(the <name>
     (-> <type>
         [Environment Path /.Command (List /.Argument)])
     (|>> <prep>
          list
          [environment.empty "~" <command>]))]

  [[echo! "echo" Text (|>)]
   [sleep! "sleep" Natural %.natural]])

(the (can_wait! process)
  (-> (/.Process Future) unit.Test)
  (|> (by process await [])
      (future#each (|>> (try#each (i.= /.normal))
                        (try.else false)
                        (unit.coverage [/.Exit /.normal])))
      future#conjoint))

(the (can_read! expected process)
  (-> Text (/.Process Future) (Future Bit))
  (|> (by process read [])
      (future#each (|>> (try#each (text.= expected))
                        (try.else false)))))

(the (can_destroy! process)
  (-> (/.Process Future) (Future Bit))
  (monad.let future.monad
    [?destroy (by process destroy [])
     ?await (by process await [])]
    (pure (and (when ?destroy
                 {try.#Success _}
                 true
                 
                 {try.#Failure error}
                 false)
               (when ?await
                 {try.#Success _}
                 false
                 
                 {try.#Failure error}
                 true)))))

(expansion.let [<shell_coverage> (these [/.Command /.Argument])]
  (the .public (spec shell)
    (-> (/.Shell Future)
        Test)
    (<| (_.for [/.Shell
                /.execute
                
                /.Process
                /.read /.fail /.write /.destroy /.await])
        (monad.let [! random.monad]
          [message (random.alphabetic 10)
           seconds (by ! each (|>> (n.% 5) (n.+ 5)) random.natural)]
          (pure (monad.let [! future.monad]
                  [?echo (by shell execute (..echo! message))
                   ?sleep (by shell execute (..sleep! seconds))]
                  (when [?echo ?sleep]
                    [{try.#Success echo} {try.#Success sleep}]
                    (monad.let !
                      [can_read! (..can_read! message echo)
                       can_destroy! (..can_destroy! sleep)]
                      (all unit.and
                           (unit.coverage <shell_coverage>
                             (and can_read!
                                  can_destroy!))
                           (..can_wait! echo)
                           ))
                    
                    _
                    (unit.coverage <shell_coverage>
                      false))))))))

(exception.the dead)

(the (memory [environment working_directory command arguments])
  (-> [Environment Path /.Command (List /.Argument)]
      (/.Memory Bit))
  (implementation
   (the (on_read dead?)
     (if dead?
         (exception.except ..dead [])
         (monad.let try.monad
           [echo (try.of_maybe "" (list.item 0 arguments))]
           (pure [dead? echo]))))
   
   (the (on_fail dead?)
     (if dead?
         (exception.except ..dead [])
         {try.#Success [dead? ""]}))
   
   (the (on_write message dead?)
     (if dead?
         (exception.except ..dead [])
         {try.#Success dead?}))

   (the (on_destroy dead?)
     (if dead?
         (exception.except ..dead [])
         {try.#Success true}))

   (the (on_await dead?)
     (if dead?
         (exception.except ..dead [])
         {try.#Success [true /.normal]}))))

(the (io_shell command oops input destruction exit)
  (-> /.Command Text Text Text /.Exit (/.Shell IO))
  (implementation
   (the (execute [environment working_directory command arguments])
     (<| io.io
         {try.#Success}
         (is (/.Process IO))
         (implementation
          (the (read _)
            (io.io {try.#Success command}))
          (the (fail _)
            (io.io {try.#Success oops}))
          (the (write message)
            (io.io {try.#Failure message}))
          (the (destroy _)
            (io.io {try.#Failure destruction}))
          (the (await _)
            (io.io {try.#Success exit})))))))

(the .public test
  Test
  (<| (_.covering /._)
      (all _.and
           (_.for [/.Memory /.memory
                   /.on_read /.on_fail /.on_write /.on_destroy /.on_await

                   /.future]
                  (..spec (/.future (/.memory (|>> ..memory {try.#Success})
                                              false))))
           (_.coverage [/.error]
             (not (i.= /.normal /.error)))
           (monad.let random.monad
             [command (random.alphabetic 5)
              oops (random.alphabetic 5)
              input (random.alphabetic 5)
              destruction (random.alphabetic 5)
              exit random.integer
              .let [shell (/.future (..io_shell command oops input destruction exit))]]
             (pure (monad.let [! future.monad]
                     [verdict (monad.let (try.with !)
                                [process (by shell execute [environment.empty "~" command (list)])
                                 read (by process read [])
                                 failure (by process fail [])
                                 wrote! (monad.let !
                                          [write (by process write input)]
                                          (pure {try.#Success (when write
                                                                {try.#Success _}
                                                                false
                                                                
                                                                {try.#Failure write}
                                                                (text.= input write))}))
                                 destroyed! (monad.let !
                                              [destroy (by process destroy [])]
                                              (pure {try.#Success (when destroy
                                                                    {try.#Success _}
                                                                    false
                                                                    
                                                                    {try.#Failure destroy}
                                                                    (text.= destruction destroy))}))
                                 await (by process await [])]
                                (pure (and (text.= command read)
                                           (text.= oops failure)
                                           wrote!
                                           destroyed!
                                           (i.= exit await))))]
                     (unit.coverage [/.Shell]
                       (try.else false verdict)))))
           )))
