content: -- This file is part of the Wire Server implementation.
  --
  -- Copyright (C) 2025 Wire Swiss GmbH <opensource@wire.com>
  --
  -- This program is free software: you can redistribute it and/or modify it under
  -- the terms of the GNU Affero General Public License as published by the Free
  -- Software Foundation, either version 3 of the License, or (at your option) any
  -- later version.
  --
  -- This program is distributed in the hope that it will be useful, but WITHOUT
  -- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  -- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  -- details.
  --
  -- You should have received a copy of the GNU Affero General Public License along
  -- with this program. If not, see <https://www.gnu.org/licenses/>.
  
  module Test.CargoHold.API.LogJSON
    ( withStructuredJSONLogger,
    )
  where
  
  import qualified Control.Concurrent as CC
  import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
  import Data.Aeson (Value)
  import qualified Data.Aeson as A
  import qualified Data.ByteString.Char8 as BS8
  import qualified Data.ByteString.Lazy as LBS
  import qualified GHC.IO.Handle as IOH
  import Imports
  import qualified System.IO as IO
  import qualified System.IO.Error as IOE
  import System.Logger.Extended
  import System.Posix.IO (closeFd, createPipe, fdToHandle)
  import System.Posix.Types (Fd)
  import UnliftIO.Exception (bracket, catch)
  
  -- | Run an action with a logger configured to emit Structured JSON to stdout,
  -- capturing stdout via a POSIX pipe into memory.
  withStructuredJSONLogger :: forall a. (Logger -> IO a) -> IO (a, [Value])
  withStructuredJSONLogger action = bracket acquire release use
    where
      acquire :: IO (Logger, IO.Handle, Fd, Fd, IO.Handle, Chan (Maybe Value), ThreadId)
      acquire = do
        (rfd, wfd) <- createPipe
        rH <- fdToHandle rfd
        wH <- fdToHandle wfd
        -- Keep a copy of stdout to restore later (not strictly needed now)
        oldStdout <- IOH.hDuplicate IO.stdout
        -- Configure logger to write directly to our pipe via /proc/self/fd/<wfd>
        let fdPath = "/proc/self/fd/" ++ show wfd
        logger <-
          new
            . setRenderer structuredJSONRenderer
            . setOutput (Path fdPath)
            . setLogLevel Info
            . setFormat Nothing
            $ defSettings
        ch <- newChan
        tid <- CC.forkIO $ do
          eAll <- IOE.tryIOError (BS8.hGetContents rH)
          case eAll of
            Left _ -> writeChan ch Nothing
            Right contents -> do
              let linesBS = BS8.split '\n' contents
              forM_ linesBS $ \ln ->
                case A.decode (LBS.fromStrict ln) of
                  Just v -> writeChan ch (Just v)
                  Nothing -> pure ()
              writeChan ch Nothing
        pure (logger, oldStdout, rfd, wfd, wH, ch, tid)
  
      release :: (Logger, IO.Handle, Fd, Fd, IO.Handle, Chan (Maybe Value), ThreadId) -> IO ()
      release (_logger, oldStdout, rfd, _wfd, _wH, _ch, tid) = do
        -- Restore stdout and close pipe fds
        IOH.hDuplicateTo oldStdout IO.stdout
        IO.hClose oldStdout
        closeFd rfd `catch` \(_ :: SomeException) -> pure ()
        CC.killThread tid `catch` \(_ :: SomeException) -> pure ()
  
      use :: (Logger, IO.Handle, Fd, Fd, IO.Handle, Chan (Maybe Value), ThreadId) -> IO (a, [Value])
      use (logger, _oldStdout, _rfd, _wfd, wH, ch, _tid) = do
        r <- action logger
        flush logger >> close logger
        -- Close the write handle to signal EOF
        IO.hClose wH
        let drain acc = do
              mv <- readChan ch
              case mv of
                Nothing -> pure (reverse acc)
                Just v -> drain (v : acc)
        logs <- drain []
        pure (r, logs)
  