# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.ConfigDBTest do
  use Pleroma.DataCase, async: true
  import Pleroma.Factory
  alias Pleroma.ConfigDB

  test "get_by_key/1" do
    config = insert(:config)
    insert(:config)

    assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key})
  end

  test "create/1" do
    {:ok, config} = ConfigDB.create(%{group: ":pleroma", key: ":some_key", value: "some_value"})
    assert config == ConfigDB.get_by_params(%{group: ":pleroma", key: ":some_key"})
  end

  test "update/1" do
    config = insert(:config)
    {:ok, updated} = ConfigDB.update(config, %{value: "some_value"})
    loaded = ConfigDB.get_by_params(%{group: config.group, key: config.key})
    assert loaded == updated
  end

  test "get_all_as_keyword/0" do
    saved = insert(:config)
    insert(:config, group: ":quack", key: ":level", value: ConfigDB.to_binary(:info))
    insert(:config, group: ":quack", key: ":meta", value: ConfigDB.to_binary([:none]))

    insert(:config,
      group: ":quack",
      key: ":webhook_url",
      value: ConfigDB.to_binary("https://hooks.slack.com/services/KEY/some_val")
    )

    config = ConfigDB.get_all_as_keyword()

    assert config[:pleroma] == [
             {ConfigDB.from_string(saved.key), ConfigDB.from_binary(saved.value)}
           ]

    assert config[:quack] == [
             level: :info,
             meta: [:none],
             webhook_url: "https://hooks.slack.com/services/KEY/some_val"
           ]
  end

  describe "update_or_create/1" do
    test "common" do
      config = insert(:config)
      key2 = "another_key"

      params = [
        %{group: "pleroma", key: key2, value: "another_value"},
        %{group: config.group, key: config.key, value: "new_value"}
      ]

      assert Repo.all(ConfigDB) |> length() == 1

      Enum.each(params, &ConfigDB.update_or_create(&1))

      assert Repo.all(ConfigDB) |> length() == 2

      config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key})
      config2 = ConfigDB.get_by_params(%{group: "pleroma", key: key2})

      assert config1.value == ConfigDB.transform("new_value")
      assert config2.value == ConfigDB.transform("another_value")
    end

    test "partial update" do
      config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: :val2))

      {:ok, _config} =
        ConfigDB.update_or_create(%{
          group: config.group,
          key: config.key,
          value: [key1: :val1, key3: :val3]
        })

      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})

      value = ConfigDB.from_binary(updated.value)
      assert length(value) == 3
      assert value[:key1] == :val1
      assert value[:key2] == :val2
      assert value[:key3] == :val3
    end

    test "deep merge" do
      config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: [k1: :v1, k2: "v2"]))

      {:ok, config} =
        ConfigDB.update_or_create(%{
          group: config.group,
          key: config.key,
          value: [key1: :val1, key2: [k2: :v2, k3: :v3], key3: :val3]
        })

      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})

      assert config.value == updated.value

      value = ConfigDB.from_binary(updated.value)
      assert value[:key1] == :val1
      assert value[:key2] == [k1: :v1, k2: :v2, k3: :v3]
      assert value[:key3] == :val3
    end

    test "only full update for some keys" do
      config1 = insert(:config, key: ":ecto_repos", value: ConfigDB.to_binary(repo: Pleroma.Repo))

      config2 =
        insert(:config, group: ":cors_plug", key: ":max_age", value: ConfigDB.to_binary(18))

      {:ok, _config} =
        ConfigDB.update_or_create(%{
          group: config1.group,
          key: config1.key,
          value: [another_repo: [Pleroma.Repo]]
        })

      {:ok, _config} =
        ConfigDB.update_or_create(%{
          group: config2.group,
          key: config2.key,
          value: 777
        })

      updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
      updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})

      assert ConfigDB.from_binary(updated1.value) == [another_repo: [Pleroma.Repo]]
      assert ConfigDB.from_binary(updated2.value) == 777
    end

    test "full update if value is not keyword" do
      config =
        insert(:config,
          group: ":tesla",
          key: ":adapter",
          value: ConfigDB.to_binary(Tesla.Adapter.Hackney)
        )

      {:ok, _config} =
        ConfigDB.update_or_create(%{
          group: config.group,
          key: config.key,
          value: Tesla.Adapter.Httpc
        })

      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})

      assert ConfigDB.from_binary(updated.value) == Tesla.Adapter.Httpc
    end

    test "only full update for some subkeys" do
      config1 =
        insert(:config,
          key: ":emoji",
          value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
        )

      config2 =
        insert(:config,
          key: ":assets",
          value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
        )

      {:ok, _config} =
        ConfigDB.update_or_create(%{
          group: config1.group,
          key: config1.key,
          value: [groups: [c: 3, d: 4], key: [b: 2]]
        })

      {:ok, _config} =
        ConfigDB.update_or_create(%{
          group: config2.group,
          key: config2.key,
          value: [mascots: [c: 3, d: 4], key: [b: 2]]
        })

      updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
      updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})

      assert ConfigDB.from_binary(updated1.value) == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
      assert ConfigDB.from_binary(updated2.value) == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
    end
  end

  describe "delete/1" do
    test "error on deleting non existing setting" do
      {:error, error} = ConfigDB.delete(%{group: ":pleroma", key: ":key"})
      assert error =~ "Config with params %{group: \":pleroma\", key: \":key\"} not found"
    end

    test "full delete" do
      config = insert(:config)
      {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key})
      assert Ecto.get_meta(deleted, :state) == :deleted
      refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
    end

    test "partial subkeys delete" do
      config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]))

      {:ok, deleted} =
        ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})

      assert Ecto.get_meta(deleted, :state) == :loaded

      assert deleted.value == ConfigDB.to_binary(key: [a: 1])

      updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})

      assert updated.value == deleted.value
    end

    test "full delete if remaining value after subkeys deletion is empty list" do
      config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2]))

      {:ok, deleted} =
        ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})

      assert Ecto.get_meta(deleted, :state) == :deleted

      refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
    end
  end

  describe "transform/1" do
    test "string" do
      binary = ConfigDB.transform("value as string")
      assert binary == :erlang.term_to_binary("value as string")
      assert ConfigDB.from_binary(binary) == "value as string"
    end

    test "boolean" do
      binary = ConfigDB.transform(false)
      assert binary == :erlang.term_to_binary(false)
      assert ConfigDB.from_binary(binary) == false
    end

    test "nil" do
      binary = ConfigDB.transform(nil)
      assert binary == :erlang.term_to_binary(nil)
      assert ConfigDB.from_binary(binary) == nil
    end

    test "integer" do
      binary = ConfigDB.transform(150)
      assert binary == :erlang.term_to_binary(150)
      assert ConfigDB.from_binary(binary) == 150
    end

    test "atom" do
      binary = ConfigDB.transform(":atom")
      assert binary == :erlang.term_to_binary(:atom)
      assert ConfigDB.from_binary(binary) == :atom
    end

    test "ssl options" do
      binary = ConfigDB.transform([":tlsv1", ":tlsv1.1", ":tlsv1.2"])
      assert binary == :erlang.term_to_binary([:tlsv1, :"tlsv1.1", :"tlsv1.2"])
      assert ConfigDB.from_binary(binary) == [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
    end

    test "pleroma module" do
      binary = ConfigDB.transform("Pleroma.Bookmark")
      assert binary == :erlang.term_to_binary(Pleroma.Bookmark)
      assert ConfigDB.from_binary(binary) == Pleroma.Bookmark
    end

    test "pleroma string" do
      binary = ConfigDB.transform("Pleroma")
      assert binary == :erlang.term_to_binary("Pleroma")
      assert ConfigDB.from_binary(binary) == "Pleroma"
    end

    test "phoenix module" do
      binary = ConfigDB.transform("Phoenix.Socket.V1.JSONSerializer")
      assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer)
      assert ConfigDB.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer
    end

    test "tesla module" do
      binary = ConfigDB.transform("Tesla.Adapter.Hackney")
      assert binary == :erlang.term_to_binary(Tesla.Adapter.Hackney)
      assert ConfigDB.from_binary(binary) == Tesla.Adapter.Hackney
    end

    test "ExSyslogger module" do
      binary = ConfigDB.transform("ExSyslogger")
      assert binary == :erlang.term_to_binary(ExSyslogger)
      assert ConfigDB.from_binary(binary) == ExSyslogger
    end

    test "Quack.Logger module" do
      binary = ConfigDB.transform("Quack.Logger")
      assert binary == :erlang.term_to_binary(Quack.Logger)
      assert ConfigDB.from_binary(binary) == Quack.Logger
    end

    test "Swoosh.Adapters modules" do
      binary = ConfigDB.transform("Swoosh.Adapters.SMTP")
      assert binary == :erlang.term_to_binary(Swoosh.Adapters.SMTP)
      assert ConfigDB.from_binary(binary) == Swoosh.Adapters.SMTP
      binary = ConfigDB.transform("Swoosh.Adapters.AmazonSES")
      assert binary == :erlang.term_to_binary(Swoosh.Adapters.AmazonSES)
      assert ConfigDB.from_binary(binary) == Swoosh.Adapters.AmazonSES
    end

    test "sigil" do
      binary = ConfigDB.transform("~r[comp[lL][aA][iI][nN]er]")
      assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/)
      assert ConfigDB.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/
    end

    test "link sigil" do
      binary = ConfigDB.transform("~r/https:\/\/example.com/")
      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/)
      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/
    end

    test "link sigil with um modifiers" do
      binary = ConfigDB.transform("~r/https:\/\/example.com/um")
      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/um)
      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/um
    end

    test "link sigil with i modifier" do
      binary = ConfigDB.transform("~r/https:\/\/example.com/i")
      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/i)
      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/i
    end

    test "link sigil with s modifier" do
      binary = ConfigDB.transform("~r/https:\/\/example.com/s")
      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/s)
      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/s
    end

    test "raise if valid delimiter not found" do
      assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn ->
        ConfigDB.transform("~r/https://[]{}<>\"'()|example.com/s")
      end
    end

    test "2 child tuple" do
      binary = ConfigDB.transform(%{"tuple" => ["v1", ":v2"]})
      assert binary == :erlang.term_to_binary({"v1", :v2})
      assert ConfigDB.from_binary(binary) == {"v1", :v2}
    end

    test "proxy tuple with localhost" do
      binary =
        ConfigDB.transform(%{
          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
        })

      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, :localhost, 1234}})
      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, :localhost, 1234}}
    end

    test "proxy tuple with domain" do
      binary =
        ConfigDB.transform(%{
          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
        })

      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, 'domain.com', 1234}})
      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, 'domain.com', 1234}}
    end

    test "proxy tuple with ip" do
      binary =
        ConfigDB.transform(%{
          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]
        })

      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}})
      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
    end

    test "tuple with n childs" do
      binary =
        ConfigDB.transform(%{
          "tuple" => [
            "v1",
            ":v2",
            "Pleroma.Bookmark",
            150,
            false,
            "Phoenix.Socket.V1.JSONSerializer"
          ]
        })

      assert binary ==
               :erlang.term_to_binary(
                 {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
               )

      assert ConfigDB.from_binary(binary) ==
               {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
    end

    test "map with string key" do
      binary = ConfigDB.transform(%{"key" => "value"})
      assert binary == :erlang.term_to_binary(%{"key" => "value"})
      assert ConfigDB.from_binary(binary) == %{"key" => "value"}
    end

    test "map with atom key" do
      binary = ConfigDB.transform(%{":key" => "value"})
      assert binary == :erlang.term_to_binary(%{key: "value"})
      assert ConfigDB.from_binary(binary) == %{key: "value"}
    end

    test "list of strings" do
      binary = ConfigDB.transform(["v1", "v2", "v3"])
      assert binary == :erlang.term_to_binary(["v1", "v2", "v3"])
      assert ConfigDB.from_binary(binary) == ["v1", "v2", "v3"]
    end

    test "list of modules" do
      binary = ConfigDB.transform(["Pleroma.Repo", "Pleroma.Activity"])
      assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity])
      assert ConfigDB.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity]
    end

    test "list of atoms" do
      binary = ConfigDB.transform([":v1", ":v2", ":v3"])
      assert binary == :erlang.term_to_binary([:v1, :v2, :v3])
      assert ConfigDB.from_binary(binary) == [:v1, :v2, :v3]
    end

    test "list of mixed values" do
      binary =
        ConfigDB.transform([
          "v1",
          ":v2",
          "Pleroma.Repo",
          "Phoenix.Socket.V1.JSONSerializer",
          15,
          false
        ])

      assert binary ==
               :erlang.term_to_binary([
                 "v1",
                 :v2,
                 Pleroma.Repo,
                 Phoenix.Socket.V1.JSONSerializer,
                 15,
                 false
               ])

      assert ConfigDB.from_binary(binary) == [
               "v1",
               :v2,
               Pleroma.Repo,
               Phoenix.Socket.V1.JSONSerializer,
               15,
               false
             ]
    end

    test "simple keyword" do
      binary = ConfigDB.transform([%{"tuple" => [":key", "value"]}])
      assert binary == :erlang.term_to_binary([{:key, "value"}])
      assert ConfigDB.from_binary(binary) == [{:key, "value"}]
      assert ConfigDB.from_binary(binary) == [key: "value"]
    end

    test "keyword with partial_chain key" do
      binary =
        ConfigDB.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}])

      assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1)
      assert ConfigDB.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1]
    end

    test "keyword" do
      binary =
        ConfigDB.transform([
          %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
          %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
          %{"tuple" => [":migration_lock", nil]},
          %{"tuple" => [":key1", 150]},
          %{"tuple" => [":key2", "string"]}
        ])

      assert binary ==
               :erlang.term_to_binary(
                 types: Pleroma.PostgresTypes,
                 telemetry_event: [Pleroma.Repo.Instrumenter],
                 migration_lock: nil,
                 key1: 150,
                 key2: "string"
               )

      assert ConfigDB.from_binary(binary) == [
               types: Pleroma.PostgresTypes,
               telemetry_event: [Pleroma.Repo.Instrumenter],
               migration_lock: nil,
               key1: 150,
               key2: "string"
             ]
    end

    test "complex keyword with nested mixed childs" do
      binary =
        ConfigDB.transform([
          %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
          %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
          %{"tuple" => [":link_name", true]},
          %{"tuple" => [":proxy_remote", false]},
          %{"tuple" => [":common_map", %{":key" => "value"}]},
          %{
            "tuple" => [
              ":proxy_opts",
              [
                %{"tuple" => [":redirect_on_failure", false]},
                %{"tuple" => [":max_body_length", 1_048_576]},
                %{
                  "tuple" => [
                    ":http",
                    [%{"tuple" => [":follow_redirect", true]}, %{"tuple" => [":pool", ":upload"]}]
                  ]
                }
              ]
            ]
          }
        ])

      assert binary ==
               :erlang.term_to_binary(
                 uploader: Pleroma.Uploaders.Local,
                 filters: [Pleroma.Upload.Filter.Dedupe],
                 link_name: true,
                 proxy_remote: false,
                 common_map: %{key: "value"},
                 proxy_opts: [
                   redirect_on_failure: false,
                   max_body_length: 1_048_576,
                   http: [
                     follow_redirect: true,
                     pool: :upload
                   ]
                 ]
               )

      assert ConfigDB.from_binary(binary) ==
               [
                 uploader: Pleroma.Uploaders.Local,
                 filters: [Pleroma.Upload.Filter.Dedupe],
                 link_name: true,
                 proxy_remote: false,
                 common_map: %{key: "value"},
                 proxy_opts: [
                   redirect_on_failure: false,
                   max_body_length: 1_048_576,
                   http: [
                     follow_redirect: true,
                     pool: :upload
                   ]
                 ]
               ]
    end

    test "common keyword" do
      binary =
        ConfigDB.transform([
          %{"tuple" => [":level", ":warn"]},
          %{"tuple" => [":meta", [":all"]]},
          %{"tuple" => [":path", ""]},
          %{"tuple" => [":val", nil]},
          %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
        ])

      assert binary ==
               :erlang.term_to_binary(
                 level: :warn,
                 meta: [:all],
                 path: "",
                 val: nil,
                 webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
               )

      assert ConfigDB.from_binary(binary) == [
               level: :warn,
               meta: [:all],
               path: "",
               val: nil,
               webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
             ]
    end

    test "complex keyword with sigil" do
      binary =
        ConfigDB.transform([
          %{"tuple" => [":federated_timeline_removal", []]},
          %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
          %{"tuple" => [":replace", []]}
        ])

      assert binary ==
               :erlang.term_to_binary(
                 federated_timeline_removal: [],
                 reject: [~r/comp[lL][aA][iI][nN]er/],
                 replace: []
               )

      assert ConfigDB.from_binary(binary) ==
               [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []]
    end

    test "complex keyword with tuples with more than 2 values" do
      binary =
        ConfigDB.transform([
          %{
            "tuple" => [
              ":http",
              [
                %{
                  "tuple" => [
                    ":key1",
                    [
                      %{
                        "tuple" => [
                          ":_",
                          [
                            %{
                              "tuple" => [
                                "/api/v1/streaming",
                                "Pleroma.Web.MastodonAPI.WebsocketHandler",
                                []
                              ]
                            },
                            %{
                              "tuple" => [
                                "/websocket",
                                "Phoenix.Endpoint.CowboyWebSocket",
                                %{
                                  "tuple" => [
                                    "Phoenix.Transports.WebSocket",
                                    %{
                                      "tuple" => [
                                        "Pleroma.Web.Endpoint",
                                        "Pleroma.Web.UserSocket",
                                        []
                                      ]
                                    }
                                  ]
                                }
                              ]
                            },
                            %{
                              "tuple" => [
                                ":_",
                                "Phoenix.Endpoint.Cowboy2Handler",
                                %{"tuple" => ["Pleroma.Web.Endpoint", []]}
                              ]
                            }
                          ]
                        ]
                      }
                    ]
                  ]
                }
              ]
            ]
          }
        ])

      assert binary ==
               :erlang.term_to_binary(
                 http: [
                   key1: [
                     _: [
                       {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
                       {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
                        {Phoenix.Transports.WebSocket,
                         {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
                       {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
                     ]
                   ]
                 ]
               )

      assert ConfigDB.from_binary(binary) == [
               http: [
                 key1: [
                   {:_,
                    [
                      {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
                      {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
                       {Phoenix.Transports.WebSocket,
                        {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
                      {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
                    ]}
                 ]
               ]
             ]
    end
  end
end
