defmodule Sanbase.Alert.MetricTriggerSettingsTest do
  use Sanbase.DataCase, async: false

  import Sanbase.Factory
  import Sanbase.TestHelpers
  import ExUnit.CaptureLog

  alias Sanbase.Alert.UserTrigger
  alias Sanbase.Alert.Evaluator
  alias Sanbase.Metric
  alias Sanbase.Alert.Trigger.MetricTriggerSettings

  setup_all_with_mocks([
    {
      Sanbase.Timeline.TimelineEvent,
      [:passthrough],
      maybe_create_event_async: fn user_trigger_tuple, _, _ -> user_trigger_tuple end
    }
  ]) do
    []
  end

  describe "metrics with text selector" do
    setup do
      # Clean children on exit, otherwise DB calls from async tasks can be attempted
      clean_task_supervisor_children()
      Sanbase.Cache.clear_all(:alerts_evaluator_cache)

      user = insert(:user, user_settings: %{settings: %{alert_notify_telegram: true}})
      Sanbase.Accounts.UserSettings.set_telegram_chat_id(user.id, 123_123_123_123)

      %{user: user}
    end

    test "signal with text selector works", context do
      %{user: user} = context

      trigger_settings = %{
        type: "metric_signal",
        metric: "social_volume_total",
        target: %{text: "random text"},
        channel: "telegram",
        operation: %{above: 300}
      }

      {:ok, trigger} =
        UserTrigger.create_user_trigger(user, %{
          title: "Generic title",
          is_public: true,
          cooldown: "12h",
          settings: trigger_settings
        })

      Sanbase.Mock.prepare_mock(HTTPoison, :get, mock_fun())
      |> Sanbase.Mock.run_with_mocks(fn ->
        [triggered] =
          MetricTriggerSettings.type()
          |> UserTrigger.get_active_triggers_by_type()
          |> Evaluator.run()

        assert triggered.id == trigger.id
      end)
    end

    defp mock_fun() do
      [
        fn ->
          {:ok,
           %HTTPoison.Response{
             status_code: 200,
             body: "{\"data\":{\"2024-06-07T00:00:00Z\":100}}"
           }}
        end,
        fn ->
          {:ok,
           %HTTPoison.Response{
             status_code: 200,
             body: "{\"data\":{\"2024-06-07T00:00:00Z\":5000}}"
           }}
        end
      ]
      |> Sanbase.Mock.wrap_consecutives(arity: 3)
    end
  end

  describe "metrics with slug selector" do
    setup do
      # Clean children on exit, otherwise DB calls from async tasks can be attempted
      clean_task_supervisor_children()

      Sanbase.Cache.clear_all(:alerts_evaluator_cache)

      user = insert(:user, user_settings: %{settings: %{alert_notify_telegram: true}})
      Sanbase.Accounts.UserSettings.set_telegram_chat_id(user.id, 123_123_123_123)

      project = Sanbase.Factory.insert(:random_project)

      %{user: user, project: project}
    end

    test "marketcap alert with TOTAL_MARKET slug works", context do
      %{user: user} = context

      trigger_settings = %{
        type: "metric_signal",
        metric: "marketcap_usd",
        target: %{slug: "TOTAL_MARKET"},
        channel: "telegram",
        operation: %{above: 300_000_000_000}
      }

      {:ok, trigger} =
        UserTrigger.create_user_trigger(user, %{
          title: "Generic title",
          is_public: true,
          cooldown: "12h",
          settings: trigger_settings
        })

      # Return a fun with arity 5 that will return different results
      # for consecutive calls
      mock_fun =
        [
          fn -> {:ok, %{"TOTAL_MARKET" => 400_000_000_000}} end,
          fn -> {:ok, %{"TOTAL_MARKET" => 401_000_000_000}} end
        ]
        |> Sanbase.Mock.wrap_consecutives(arity: 4)

      Sanbase.Mock.prepare_mock(Sanbase.Metric, :aggregated_timeseries_data, mock_fun)
      |> Sanbase.Mock.run_with_mocks(fn ->
        [triggered] =
          MetricTriggerSettings.type()
          |> UserTrigger.get_active_triggers_by_type()
          |> Evaluator.run()

        assert triggered.id == trigger.id

        payload = triggered.trigger.settings.payload |> Map.values() |> hd()

        assert payload =~ "The total market's Marketcap in USD is above 300.00 Billion* 💥\n\n"
        assert payload =~ "Was: 400.00 Billion\nNow: 401.00 Billion\n\n"
        assert payload =~ "\*_Generated by the value of the metric at"
      end)
    end

    test "signal with a metric works - above operation", context do
      %{project: project, user: user} = context

      trigger_settings = %{
        type: "metric_signal",
        metric: "active_addresses_24h",
        target: %{slug: project.slug},
        channel: "telegram",
        operation: %{above: 300}
      }

      {:ok, trigger} =
        UserTrigger.create_user_trigger(user, %{
          title: "Generic title",
          is_public: true,
          cooldown: "12h",
          settings: trigger_settings
        })

      # Return a fun with arity 5 that will return different results
      # for consecutive calls
      mock_fun =
        [
          fn -> {:ok, %{project.slug => 100}} end,
          fn -> {:ok, %{project.slug => 5000}} end
        ]
        |> Sanbase.Mock.wrap_consecutives(arity: 5)

      Sanbase.Mock.prepare_mock(
        Sanbase.Clickhouse.MetricAdapter,
        :aggregated_timeseries_data,
        mock_fun
      )
      |> Sanbase.Mock.run_with_mocks(fn ->
        [triggered] =
          MetricTriggerSettings.type()
          |> UserTrigger.get_active_triggers_by_type()
          |> Evaluator.run()

        assert triggered.id == trigger.id

        payload = triggered.trigger.settings.payload |> Map.values() |> hd()

        first_line =
          "[##{project.ticker}](https://app.santiment.net/charts?slug=#{project.slug}) | *#{project.name}'s Active Addresses for the last 24 hours is above 300* 💥\n\n"

        assert payload =~ first_line
        assert payload =~ "Was: 100\nNow: 5,000.00\n\n"
        assert payload =~ "\*_Generated by the value of the metric at"
      end)
    end

    test "signal with a metric and extra explanation works ", context do
      %{project: project, user: user} = context

      trigger_settings = %{
        type: "metric_signal",
        metric: "active_addresses_24h",
        target: %{slug: project.slug},
        channel: "telegram",
        operation: %{above: 300},
        extra_explanation:
          "A surge in tranaction volume *may suggest a growing interest and interaction* with the token"
      }

      {:ok, trigger} =
        UserTrigger.create_user_trigger(user, %{
          title: "Generic title",
          is_public: true,
          cooldown: "12h",
          settings: trigger_settings
        })

      # Return a fun with arity 5 that will return different results
      # for consecutive calls
      mock_fun =
        [
          fn -> {:ok, %{project.slug => 100}} end,
          fn -> {:ok, %{project.slug => 5000}} end
        ]
        |> Sanbase.Mock.wrap_consecutives(arity: 5)

      Sanbase.Mock.prepare_mock(
        Sanbase.Clickhouse.MetricAdapter,
        :aggregated_timeseries_data,
        mock_fun
      )
      |> Sanbase.Mock.run_with_mocks(fn ->
        [triggered] =
          MetricTriggerSettings.type()
          |> UserTrigger.get_active_triggers_by_type()
          |> Evaluator.run()

        assert triggered.id == trigger.id
        payload = triggered.trigger.settings.payload |> Map.values() |> hd()

        first_line =
          "[##{project.ticker}](https://app.santiment.net/charts?slug=#{project.slug}) | *#{project.name}'s Active Addresses for the last 24 hours is above 300* 💥\n\n"

        assert payload =~ first_line
        assert payload =~ "Was: 100\nNow: 5,000.00\n\n"

        assert payload =~
                 "🧐 A surge in tranaction volume *may suggest a growing interest and interaction* with the token\n\n"

        assert payload =~ "\*_Generated by the value of the metric at"
      end)
    end

    test "signal with metric works - percent change operation", context do
      %{project: project, user: user} = context

      trigger_settings = %{
        type: "metric_signal",
        metric: "active_addresses_24h",
        target: %{slug: project.slug},
        channel: "telegram",
        operation: %{percent_up: 100}
      }

      {:ok, trigger} =
        UserTrigger.create_user_trigger(user, %{
          title: "Generic title",
          is_public: true,
          cooldown: "12h",
          settings: trigger_settings
        })

      mock_fun =
        [
          fn -> {:ok, %{project.slug => 100}} end,
          fn -> {:ok, %{project.slug => 500}} end
        ]
        |> Sanbase.Mock.wrap_consecutives(arity: 5)

      Sanbase.Mock.prepare_mock(
        Sanbase.Clickhouse.MetricAdapter,
        :aggregated_timeseries_data,
        mock_fun
      )
      |> Sanbase.Mock.run_with_mocks(fn ->
        [triggered] =
          MetricTriggerSettings.type()
          |> UserTrigger.get_active_triggers_by_type()
          |> Evaluator.run()

        assert triggered.id == trigger.id
      end)
    end

    test "can create triggers with all available metrics with min interval less than 5 min",
         context do
      %{project: project, user: user} = context

      Metric.available_metrics(
        filter: :min_interval_less_or_equal,
        filter_interval: "5m"
      )
      |> Enum.shuffle()
      |> Enum.take(100)
      |> Enum.each(fn metric ->
        trigger_settings = %{
          type: "metric_signal",
          metric: metric,
          target: %{slug: project.slug},
          channel: "telegram",
          operation: %{above: 300}
        }

        {:ok, _} =
          UserTrigger.create_user_trigger(user, %{
            title: "Generic title",
            is_public: true,
            cooldown: "12h",
            settings: trigger_settings
          })
      end)
    end

    test "cannot create triggers with random metrics", context do
      %{project: project, user: user} = context

      metrics = Enum.map(1..100, fn _ -> rand_str() end)

      Enum.each(metrics, fn metric ->
        trigger_settings = %{
          type: "metric_signal",
          metric: metric,
          target: %{slug: project.slug},
          channel: "telegram",
          operation: %{above: 300}
        }

        assert capture_log(fn ->
                 {:error, error_msg} =
                   UserTrigger.create_user_trigger(user, %{
                     title: "Generic title",
                     is_public: true,
                     cooldown: "12h",
                     settings: trigger_settings
                   })

                 assert error_msg =~ "not supported, is deprecated or is mistyped"
               end)
      end)
    end

    test "wrong metric name disables the alert", context do
      trigger_settings = %{
        type: "metric_signal",
        metric: "active_addresses_24h",
        target: %{slug: context.project.slug},
        channel: "telegram",
        operation: %{percent_up: 100}
      }

      # aggregated_timeseries_data should be called 2 times, but after the first call that returns
      # error we go to the error handling phase, not calling anything else
      mock_fun =
        [
          fn ->
            {:error,
             "The metric 'active_addresses_24h' is not supported, is deprecated or is mistyped."}
          end
        ]
        |> Sanbase.Mock.wrap_consecutives(arity: 4)

      Sanbase.Mock.prepare_mock(Sanbase.Metric, :aggregated_timeseries_data, mock_fun)
      |> Sanbase.Mock.run_with_mocks(fn ->
        {:ok, ut} =
          UserTrigger.create_user_trigger(context.user, %{
            title: "title",
            is_public: true,
            cooldown: "1h",
            settings: trigger_settings
          })

        # Clear the result of the filter
        Sanbase.Cache.clear_all()
        Sanbase.Cache.clear_all(:alerts_evaluator_cache)

        log =
          capture_log(fn ->
            assert [_] = Sanbase.Alert.Scheduler.run_alert(MetricTriggerSettings)
          end)

        assert log =~ "Auto disable alert"
        assert log =~ "active_addresses_24h"
        assert log =~ "not supported, is deprecated or is mistyped"
        {:ok, user_trigger} = Sanbase.Alert.UserTrigger.by_user_and_id(ut.user_id, ut.id)

        assert user_trigger.trigger.is_active == false
      end)
    end
  end

  test "alert with invalid time_window does not run and is disabled" do
    user = insert(:user)

    trigger_settings = %{
      type: "metric_signal",
      metric: "active_addresses_24h",
      target: %{slug: "santiment"},
      channel: "telegram",
      time_window: "11111111d",
      operation: %{percent_up: 200.0}
    }

    # To bypass the validation check on insertion use the Factory directly
    user_trigger =
      insert(:user_trigger,
        user: user,
        trigger: %{title: "Generic title", is_public: false, settings: trigger_settings}
      )

    assert user_trigger.trigger.is_active == true

    triggered =
      MetricTriggerSettings.type()
      |> UserTrigger.get_active_triggers_by_type()
      |> Evaluator.run()

    assert triggered == []

    {:ok, user_trigger} = UserTrigger.by_user_and_id(user_trigger.user_id, user_trigger.id)
    assert user_trigger.trigger.is_active == false
  end
end
