content: -- This file is part of the Wire Server implementation.
  --
  -- Copyright (C) 2022 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 Index.Create where
  
  import API.Search.Util (mkBHEnv)
  import Brig.App (initHttpManagerWithTLSConfig)
  import Brig.Index.Eval qualified as IndexEval
  import Brig.Index.Options
  import Brig.Index.Options qualified as IndexOpts
  import Brig.Options (Opts (galley))
  import Brig.Options qualified as BrigOpts
  import Control.Lens ((.~))
  import Control.Lens.Combinators (none)
  import Data.Text qualified as Text
  import Data.Text.Encoding qualified as Text
  import Database.Bloodhound qualified as ES
  import Imports
  import System.Logger.Class qualified as Log
  import System.Random as Random
  import Test.Tasty
  import Test.Tasty.HUnit
  import URI.ByteString
  
  -- FUTUREWORK: Make Bloodhound capable of getting a mapping and add test here to make sure
  -- the intended mapping is set on the created index. Also add a test to ensure when the index
  -- already exists, the existing mapping is not updated.
  spec :: BrigOpts.Opts -> IO TestTree
  spec brigOpts =
    pure $
      testGroup
        "brig-index create"
        [ testCase "should create an index when it is not present" $ testCreateIndexWhenNotPresent brigOpts,
          testCase "should not update anything when index already exists" $ testCreateIndexWhenPresent brigOpts
        ]
  
  testCreateIndexWhenNotPresent :: BrigOpts.Opts -> Assertion
  testCreateIndexWhenNotPresent brigOpts = do
    let (ES.Server esURL) = brigOpts.elasticsearch.url
    case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of
      Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e
      Right esURI -> do
        indexName <- ES.IndexName . Text.pack <$> replicateM 20 (Random.randomRIO ('a', 'z'))
        let replicas = 2
            shards = 2
            refreshInterval = 5
        let connSettings =
              ESConnectionSettings
                { esServer = esURI,
                  esIndex = indexName,
                  esCaCert = brigOpts.elasticsearch.caCert,
                  esInsecureSkipVerifyTls = brigOpts.elasticsearch.insecureSkipVerifyTls,
                  esCredentials = brigOpts.elasticsearch.credentials,
                  esMigrationIndexName = Nothing
                }
        let esSettings =
              IndexOpts.localElasticSettings
                & IndexOpts.esConnection .~ connSettings
                & IndexOpts.esIndexReplicas .~ ES.ReplicaCount replicas
                & IndexOpts.esIndexShardCount .~ shards
                & IndexOpts.esIndexRefreshInterval .~ refreshInterval
        devNullLogger <- Log.create (Log.Path "/dev/null")
        IndexEval.runCommand devNullLogger (IndexOpts.Create esSettings (galley brigOpts))
        mgr <- liftIO $ initHttpManagerWithTLSConfig connSettings.esInsecureSkipVerifyTls connSettings.esCaCert
        let bEnv = (mkBHEnv esURL mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")}
        ES.runBH bEnv $ do
          indexExists <- ES.indexExists indexName
          lift $
            assertBool "Index should exist" indexExists
          eitherIndexSettings <- ES.getIndexSettings indexName
          lift $ do
            case eitherIndexSettings of
              Left err -> fail $ "Failed to fetch index settings with error: " <> show err
              Right indexSettings -> do
                assertEqual "Shard count should be set" (ES.ShardCount replicas) (ES.indexShards . ES.sSummaryFixedSettings $ indexSettings)
                assertEqual "Replica count should be set" (ES.ReplicaCount replicas) (ES.indexReplicas . ES.sSummaryFixedSettings $ indexSettings)
                -- Check if the `RefreshInterval` is part of `UpdateIndexSettings`.
                -- There can be more settings. E.g. ElasticSearch 7 has these:
                -- `[RefreshInterval 5s, RoutingAllocationInclude (NodeAttrFilter {nodeAttrFilterName = NodeAttrName "_tier_preference", nodeAttrFilterValues = "data_content" :| []} :| [])]`
                assertBool "Refresh interval should be set" $ (ES.RefreshInterval refreshInterval) `elem` (ES.sSummaryUpdateable indexSettings)
  
  testCreateIndexWhenPresent :: BrigOpts.Opts -> Assertion
  testCreateIndexWhenPresent brigOpts = do
    let (ES.Server esURL) = brigOpts.elasticsearch.url
    case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of
      Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e
      Right esURI -> do
        indexName <- ES.IndexName . Text.pack <$> replicateM 20 (Random.randomRIO ('a', 'z'))
        let replicas = 2
            shards = 2
            refreshInterval = 5
            connSettings =
              ESConnectionSettings
                { esServer = esURI,
                  esIndex = indexName,
                  esCaCert = brigOpts.elasticsearch.caCert,
                  esInsecureSkipVerifyTls = brigOpts.elasticsearch.insecureSkipVerifyTls,
                  esCredentials = brigOpts.elasticsearch.credentials,
                  esMigrationIndexName = Nothing
                }
            esSettings =
              IndexOpts.localElasticSettings
                & IndexOpts.esConnection .~ connSettings
                & IndexOpts.esIndexReplicas .~ ES.ReplicaCount replicas
                & IndexOpts.esIndexShardCount .~ shards
                & IndexOpts.esIndexRefreshInterval .~ refreshInterval
        mgr <- liftIO $ initHttpManagerWithTLSConfig connSettings.esInsecureSkipVerifyTls connSettings.esCaCert
        let bEnv = (mkBHEnv esURL mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")}
        ES.runBH bEnv $ do
          _ <- ES.createIndex (ES.IndexSettings (ES.ShardCount 1) (ES.ReplicaCount 1)) indexName
          indexExists <- ES.indexExists indexName
          lift $
            assertBool "Index should exist" indexExists
        devNullLogger <- Log.create (Log.Path "/dev/null")
        IndexEval.runCommand devNullLogger (IndexOpts.Create esSettings (galley brigOpts))
        ES.runBH bEnv $ do
          indexExists <- ES.indexExists indexName
          lift $
            assertBool "Index should still exist" indexExists
          eitherIndexSettings <- ES.getIndexSettings indexName
          lift $ do
            case eitherIndexSettings of
              Left err -> fail $ "Failed to fetch index settings with error: " <> show err
              Right indexSettings -> do
                assertEqual "Shard count should not be updated" (ES.ShardCount 1) (ES.indexShards . ES.sSummaryFixedSettings $ indexSettings)
                assertEqual "Replica count should not be updated" (ES.ReplicaCount 1) (ES.indexReplicas . ES.sSummaryFixedSettings $ indexSettings)
                -- Ensure that the `RefreshInterval` is not part of `UpdateIndexSettings`.
                -- There can be more settings. E.g. ElasticSearch 7 has this by default:
                -- `[RoutingAllocationInclude (NodeAttrFilter {nodeAttrFilterName = NodeAttrName "_tier_preference", nodeAttrFilterValues = "data_content" :| []} :| [])]`
                assertBool "Refresh interval should not be updated" $
                  none
                    ( \case
                        ES.RefreshInterval _ -> True
                        _otherwise -> False
                    )
                    (ES.sSummaryUpdateable indexSettings)
  