-- 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.FeatureFlags.MlsE2EId where

import qualified API.Galley as Public
import qualified Data.Aeson as A
import SetupHelpers
import Test.FeatureFlags.Util
import Testlib.Prelude

mlsE2EId1 :: Value
mlsE2EId1 :: Value
mlsE2EId1 =
  [Pair] -> Value
object
    [ String
"status" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"enabled",
      String
"config"
        String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [Pair] -> Value
object
          [ String
"crlProxy" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"https://example.com",
            String
"verificationExpiration" String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Scientific -> Value
A.Number Scientific
86400,
            String
"useProxyOnMobile" String -> Bool -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Bool
False
          ]
    ]

testMLSE2EId :: (HasCallStack) => APIAccess -> App ()
testMLSE2EId :: HasCallStack => APIAccess -> App ()
testMLSE2EId APIAccess
access = do
  invalid <-
    Value
mlsE2EId1
      Value -> (Value -> App Value) -> App Value
forall a b. a -> (a -> b) -> b
& if (APIAccess
access APIAccess -> APIAccess -> Bool
forall a. Eq a => a -> a -> Bool
== APIAccess
InternalAPI)
        then -- the internal API is not as strict as the public one, so we need to tweak the invalid config some more
          String -> Value -> Value -> App Value
forall a b.
(HasCallStack, MakesValue a, ToJSON b) =>
String -> b -> a -> App Value
setField String
"config.crlProxy" ([Pair] -> Value
object [])
        else String -> Value -> App Value
forall a. (HasCallStack, MakesValue a) => String -> a -> App Value
removeField String
"config.crlProxy"
  mlsE2EId2 <-
    mlsE2EId1
      & setField "config.verificationExpiration" (A.Number 86401)
      & setField "config.useProxyOnMobile" True
  mkFeatureTests "mlsE2EId"
    & addUpdate mlsE2EId1
    & addUpdate mlsE2EId2
    & addInvalidUpdate invalid
    & runFeatureTests OwnDomain access

testPatchE2EId :: (HasCallStack) => App ()
testPatchE2EId :: HasCallStack => App ()
testPatchE2EId = do
  Domain -> String -> Value -> App ()
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> String -> Value -> App ()
checkPatch Domain
OwnDomain String
"mlsE2EId" ([Pair] -> Value
object [String
"lockStatus" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"locked"])
  Domain -> String -> Value -> App ()
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> String -> Value -> App ()
checkPatch Domain
OwnDomain String
"mlsE2EId" ([Pair] -> Value
object [String
"status" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"enabled"])
  Domain -> String -> Value -> App ()
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> String -> Value -> App ()
checkPatch Domain
OwnDomain String
"mlsE2EId"
    (Value -> App ()) -> Value -> App ()
forall a b. (a -> b) -> a -> b
$ [Pair] -> Value
object [String
"lockStatus" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"locked", String
"status" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"enabled"]
  Domain -> String -> Value -> App ()
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> String -> Value -> App ()
checkPatch Domain
OwnDomain String
"mlsE2EId"
    (Value -> App ()) -> Value -> App ()
forall a b. (a -> b) -> a -> b
$ [Pair] -> Value
object
      [ String
"lockStatus" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"unlocked",
        String
"config"
          String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [Pair] -> Value
object
            [ String
"crlProxy" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"https://example.com",
              String
"verificationExpiration" String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Scientific -> Value
A.Number Scientific
86401,
              String
"useProxyOnMobile" String -> Bool -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Bool
True
            ]
      ]

  Domain -> String -> Value -> App ()
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> String -> Value -> App ()
checkPatch Domain
OwnDomain String
"mlsE2EId"
    (Value -> App ()) -> Value -> App ()
forall a b. (a -> b) -> a -> b
$ [Pair] -> Value
object
      [ String
"config"
          String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [Pair] -> Value
object
            [ String
"crlProxy" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"https://example.com",
              String
"verificationExpiration" String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Scientific -> Value
A.Number Scientific
86401,
              String
"useProxyOnMobile" String -> Bool -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Bool
True
            ]
      ]

testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => App ()
testMlsE2EConfigCrlProxyRequired :: HasCallStack => App ()
testMlsE2EConfigCrlProxyRequired = do
  (owner, tid, _) <- Domain -> Int -> App (Value, String, [Value])
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> Int -> App (Value, String, [Value])
createTeam Domain
OwnDomain Int
1
  let configWithoutCrlProxy =
        [Pair] -> Value
object
          [ String
"config"
              String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [Pair] -> Value
object
                [ String
"useProxyOnMobile" String -> Bool -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Bool
False,
                  String
"verificationExpiration" String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Scientific -> Value
A.Number Scientific
86400
                ],
            String
"status" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"enabled"
          ]

  -- From API version 6 onwards, the CRL proxy is required, so the request should fail when it's not provided
  bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithoutCrlProxy) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
400
    Response
resp.json App Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"label" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"mls-e2eid-missing-crl-proxy"

  configWithCrlProxy <-
    configWithoutCrlProxy
      & setField "config.useProxyOnMobile" True
      & setField "config.crlProxy" "https://crl-proxy.example.com"
      & setField "status" "enabled"

  -- The request should succeed when the CRL proxy is provided
  bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithCrlProxy) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200

  -- Assert that the feature config got updated correctly
  expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited"
  checkFeature "mlsE2EId" owner tid expectedResponse

testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => App ()
testMlsE2EConfigCrlProxyNotRequiredInV5 :: HasCallStack => App ()
testMlsE2EConfigCrlProxyNotRequiredInV5 = do
  (owner, tid, _) <- Domain -> Int -> App (Value, String, [Value])
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> Int -> App (Value, String, [Value])
createTeam Domain
OwnDomain Int
1
  let configWithoutCrlProxy =
        [Pair] -> Value
object
          [ String
"config"
              String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= [Pair] -> Value
object
                [ String
"useProxyOnMobile" String -> Bool -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Bool
False,
                  String
"verificationExpiration" String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Scientific -> Value
A.Number Scientific
86400
                ],
            String
"status" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"enabled"
          ]

  -- In API version 5, the CRL proxy is not required, so the request should succeed
  bindResponse (Public.setTeamFeatureConfigVersioned (ExplicitVersion 5) owner tid "mlsE2EId" configWithoutCrlProxy) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200

  -- Assert that the feature config got updated correctly
  expectedResponse <-
    configWithoutCrlProxy
      & setField "lockStatus" "unlocked"
      & setField "ttl" "unlimited"
      & setField "config.crlProxy" "https://crlproxy.example.com"
  checkFeature "mlsE2EId" owner tid expectedResponse