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.AuditLogTest (tests) where
  
  import CargoHold.API.AuditLog
  import CargoHold.S3 (AssetAuditLogMetadata (..), S3AssetMeta (..))
  import qualified CargoHold.Types.V3 as V3
  import qualified Codec.MIME.Type as MIME
  import Data.Aeson ((.=))
  import qualified Data.Aeson as Aeson
  import Data.ByteString.Conversion (toByteString')
  import Data.Domain (Domain (..), domainText)
  import Data.Id (UserId, botUserId)
  import Data.Misc (IpAddr)
  import Data.Qualified
  import qualified Data.Text as T
  import Data.Text.Encoding (decodeUtf8)
  import Imports
  import System.Logger.Extended (LoggerT, runWithLogger)
  import Test.CargoHold.API.LogJSON
  import Test.Tasty
  import Test.Tasty.QuickCheck as QC
  import URI.ByteString
  import qualified Wire.API.Asset as Asset
  import qualified Wire.Arbitrary ()
  
  tests :: TestTree
  tests =
    testGroup
      "CargoHold.API.AuditLog"
      [ QC.testProperty "logUpload matches expected JSON" propLogUpload,
        QC.testProperty "logDownload with meta matches expected JSON" propLogDownload,
        QC.testProperty "logSignedURLCreation (maybe meta) matches expected JSON" propLogSignedURLCreation,
        QC.testProperty "logDownloadRemoteAsset matches expected JSON" propLogDownloadRemoteAsset
      ]
  
  propLogUpload :: Domain -> V3.Principal -> AssetAuditLogMetadata -> T.Text -> QC.Property
  propLogUpload dom princ meta pathTxt = QC.ioProperty $ do
    let lprin = toLocalUnsafe dom princ
    (_, logs) <- withStructuredJSONLogger $ \logger ->
      runWithLogger logger (logUpload lprin (Just meta) pathTxt :: LoggerT IO ())
    pure $ case logs of
      (obj : _) ->
        let expected =
              Aeson.object
                [ "level" .= ("Info" :: T.Text),
                  "msgs" .= ["Asset audit log: upload" :: T.Text],
                  "audit" .= ("True" :: T.Text),
                  "event" .= ("file-upload" :: T.Text),
                  "uploader.type" .= uploaderType princ,
                  "uploader.id" .= uploaderId princ,
                  "uploader.domain" .= domainText dom,
                  "conversation.id" .= T.pack (show (qUnqualified meta.convId)),
                  "conversation.domain" .= domainText (qDomain meta.convId),
                  "file.name" .= meta.filename,
                  "file.type" .= MIME.showType (Asset.unAssetMIMEType meta.filetype),
                  "upload.path" .= pathTxt
                ]
         in obj QC.=== expected
      _ -> counterexample "No logs emitted" False
  
  propLogDownload :: Domain -> V3.Principal -> V3.Principal -> AssetAuditLogMetadata -> T.Text -> Maybe IpAddr -> QC.Property
  propLogDownload dom owner downloader meta pathTxt mIp = QC.ioProperty $ do
    let qDownloader = Qualified downloader dom
        s3meta =
          S3AssetMeta
            { v3AssetOwner = owner,
              v3AssetToken = Nothing,
              v3AssetType = MIME.Type (MIME.Application "octet-stream") [],
              v3AssetAuditLogMetadata = Just meta
            }
    (_, logs) <- withStructuredJSONLogger $ \logger ->
      runWithLogger logger (logDownload (Just qDownloader) mIp s3meta pathTxt :: LoggerT IO ())
    pure $ case logs of
      (obj : _) ->
        let expected =
              Aeson.object
                [ "level" .= ("Info" :: T.Text),
                  "msgs" .= ["Asset audit log: download" :: T.Text],
                  "audit" .= ("True" :: T.Text),
                  "event" .= ("file-download" :: T.Text),
                  "downloader.type" .= uploaderType downloader,
                  "downloader.id" .= uploaderId downloader,
                  "downloader.domain" .= domainText dom,
                  "downloader.backend.ip" .= maybe "N/A" (decodeUtf8 . toByteString') mIp,
                  "conversation.id" .= T.pack (show (qUnqualified meta.convId)),
                  "conversation.domain" .= domainText (qDomain meta.convId),
                  "file.name" .= meta.filename,
                  "file.type" .= MIME.showType (Asset.unAssetMIMEType meta.filetype),
                  "download.path" .= pathTxt
                ]
         in obj QC.=== expected
      _ -> counterexample "No logs emitted" False
  
  propLogSignedURLCreation :: Qualified V3.Principal -> Maybe AssetAuditLogMetadata -> QC.Property
  propLogSignedURLCreation qDownloader mMeta = QC.ioProperty $ do
    let uriEither = parseURI strictURIParserOptions "https://example.test/path/ok"
        uri = either (error . show) id uriEither
        mS3Meta =
          ( \meta ->
              S3AssetMeta
                { v3AssetOwner = qUnqualified qDownloader,
                  v3AssetToken = Nothing,
                  v3AssetType = MIME.Type (MIME.Application "octet-stream") [],
                  v3AssetAuditLogMetadata = Just meta
                }
          )
            <$> mMeta
    (_, logs) <- withStructuredJSONLogger $ \logger ->
      runWithLogger logger (logSignedURLCreation (Just qDownloader) mS3Meta uri :: LoggerT IO ())
    pure $ case logs of
      (obj : _) ->
        let (cid, cdom, fname, ftype) = case mMeta of
              Just meta ->
                ( T.pack (show (qUnqualified meta.convId)),
                  domainText (qDomain meta.convId),
                  meta.filename,
                  MIME.showType (Asset.unAssetMIMEType meta.filetype)
                )
              Nothing -> ("N/A", "N/A", "N/A", "N/A")
            expected =
              Aeson.object
                [ "level" .= ("Info" :: T.Text),
                  "msgs" .= ["Asset audit log: signed URL creation" :: T.Text],
                  "audit" .= ("True" :: T.Text),
                  "event" .= ("download-url-creation" :: T.Text),
                  "downloader.type" .= uploaderType (qUnqualified qDownloader),
                  "downloader.id" .= uploaderId (qUnqualified qDownloader),
                  "downloader.domain" .= domainText (qDomain qDownloader),
                  "download.url.host" .= ("example.test" :: T.Text),
                  "download.url.path" .= ("/path/ok" :: T.Text),
                  "file.name" .= fname,
                  "file.type" .= ftype,
                  "conversation.id" .= cid,
                  "conversation.domain" .= cdom
                ]
         in obj QC.=== expected
      _ -> counterexample "No logs emitted" False
  
  propLogDownloadRemoteAsset :: Domain -> UserId -> Domain -> Maybe IpAddr -> QC.Property
  propLogDownloadRemoteAsset domLocal uid domRemote mIp = QC.ioProperty $ do
    let luid = toLocalUnsafe domLocal uid
        rmt = toRemoteUnsafe domRemote ()
    (_, logs) <- withStructuredJSONLogger $ \logger ->
      runWithLogger logger (logDownloadRemoteAsset luid rmt mIp :: LoggerT IO ())
    pure $ case logs of
      (obj : _) ->
        let expected =
              Aeson.object
                [ "level" .= ("Info" :: T.Text),
                  "msgs" .= ["Asset audit log: remote download" :: T.Text],
                  "audit" .= ("True" :: T.Text),
                  "event" .= ("file-download" :: T.Text),
                  "downloader.type" .= ("user" :: T.Text),
                  "downloader.id" .= T.pack (show uid),
                  "downloader.domain" .= domainText domLocal,
                  "remote.domain" .= domainText domRemote,
                  "remote.backend.ip" .= maybe "N/A" (decodeUtf8 . toByteString') mIp
                ]
         in obj QC.=== expected
      _ -> counterexample "No logs emitted" False
  
  uploaderType :: V3.Principal -> T.Text
  uploaderType = \case
    V3.UserPrincipal {} -> "user" :: T.Text
    V3.BotPrincipal {} -> "bot"
    V3.ProviderPrincipal {} -> "provider"
  
  uploaderId :: V3.Principal -> T.Text
  uploaderId = \case
    V3.UserPrincipal u -> T.pack (show u)
    V3.BotPrincipal b -> T.pack (show (botUserId b))
    V3.ProviderPrincipal p -> T.pack (show p)
  