# frozen_string_literal: true

require "gouda/test_helper"

class GoudaSchedulerTest < ActiveSupport::TestCase
  include AssertHelper

  setup do
    Gouda::Workload.delete_all
    Gouda::JobFuse.delete_all
  end

  class TestJob < ActiveJob::Base
    self.queue_adapter = Gouda::Adapter.new

    def perform(regular = "ok", mandatory:, optional: "hidden")
    end
  end

  class FailingJob < ActiveJob::Base
    include Gouda::ActiveJobExtensions::Concurrency
    self.queue_adapter = Gouda::Adapter.new

    class MegaError < StandardError
    end

    retry_on StandardError, wait: :polynomially_longer, attempts: 5
    retry_on Gouda::InterruptError, wait: 0, attempts: 5
    retry_on MegaError, attempts: 3, wait: 0

    def perform
      raise MegaError.new "Kaboom!"
    end
  end

  test "keeps re-enqueueing cron jobs after failed job (also with kwargs)" do
    tab = {
      second_minutely: {
        cron: "*/1 * * * * *", # every second
        class: "GoudaSchedulerTest::FailingJob"
      }
    }

    assert_nothing_raised do
      Gouda::Scheduler.build_scheduler_entries_list!(tab)
      Gouda::Scheduler.upsert_workloads_from_entries_list!
    end

    assert_equal 1, Gouda::Workload.enqueued.count
    Gouda.worker_loop(n_threads: 1, check_shutdown: Gouda::TimerShutdownCheck.new(2))

    refute_empty Gouda::Workload.enqueued
    assert Gouda::Workload.count > 3
  end

  test "retries do not have a scheduler_key" do
    tab = {
      second_minutely: {
        cron: "*/1 * * * * *", # every second
        class: "GoudaSchedulerTest::FailingJob"
      }
    }

    assert_nothing_raised do
      Gouda::Scheduler.build_scheduler_entries_list!(tab)
      Gouda::Scheduler.upsert_workloads_from_entries_list!
    end

    assert_equal 1, Gouda::Workload.enqueued.count
    assert_equal "second_minutely_*/1 * * * * *_GoudaSchedulerTest::FailingJob", Gouda::Workload.enqueued.first.scheduler_key
    sleep(2)
    Gouda::Workload.checkout_and_perform_one(executing_on: "Unit test")

    assert_equal 1, Gouda::Workload.retried.reload.count
    assert_nil Gouda::Workload.retried.first.scheduler_key
    assert_equal "enqueued", Gouda::Workload.retried.first.state
  end

  test "re-inserts the next subsequent job after executing the queued one" do
    tab = {
      second_minutely: {
        cron: "*/1 * * * * *", # every second
        class: "GoudaSchedulerTest::TestJob",
        args: ["omg"],
        kwargs: {mandatory: "WOOHOO", optional: "yeah"},
        set: {priority: 150}
      }
    }

    assert_nothing_raised do
      Gouda::Scheduler.build_scheduler_entries_list!(tab)
    end

    assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
      3.times do
        Gouda::Scheduler.upsert_workloads_from_entries_list!
      end
    end

    job = Gouda::Workload.first
    assert_equal 1, Gouda::Workload.count

    sleep 1

    assert_equal job, Gouda::Workload.checkout_and_lock_one(executing_on: "test")

    assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
      3.times do
        Gouda::Scheduler.upsert_workloads_from_entries_list!
      end
    end

    assert_equal 2, Gouda::Workload.count
    Gouda::Workload.all.each(&:perform_and_update_state!)
    assert_equal 0, Gouda::Workload.errored.count
    workload = Gouda::Workload.find_by(scheduler_key: "second_minutely_*/1 * * * * *_GoudaSchedulerTest::TestJob")
    assert_equal 150, workload.priority
    assert_equal ["omg", {
      "optional" => "yeah",
      "mandatory" => "WOOHOO",
      "_aj_ruby2_keywords" => ["mandatory", "optional"]
    }], workload.serialized_params["arguments"]
  end

  test "accepts crontab with nil args" do
    tab = {
      first_hourly: {
        cron: "@hourly",
        class: "GoudaSchedulerTest::TestJob",
        args: [nil, nil]
      }
    }

    assert_nothing_raised do
      Gouda::Scheduler.build_scheduler_entries_list!(tab)
    end

    assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
      Gouda::Scheduler.upsert_workloads_from_entries_list!
    end

    assert_equal [nil, nil], Gouda::Workload.first.serialized_params["arguments"]
  end

  test "ensures a job that was scheduled but no longer present in the cron table gets force-finished without executing" do
    tab = {
      first_hourly: {
        cron: "@hourly",
        class: "GoudaSchedulerTest::TestJob",
        args: [nil, nil]
      }
    }

    assert_nothing_raised do
      Gouda::Scheduler.build_scheduler_entries_list!(tab)
    end

    Gouda::Workload.delete_all
    assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
      Gouda::Scheduler.upsert_workloads_from_entries_list!
    end

    # Update all workloads so that it is already time for it to be executed (as we use clock_timestamp()
    # time travel is not possible in those tests)
    Gouda::Workload.update_all(scheduled_at: Time.now - 2.minutes)

    workload = Gouda::Workload.checkout_and_lock_one(executing_on: "test")
    assert workload # Now this workload does get selected for execution
    workload.update(state: "enqueued") # Return it to the queue

    # Erase the crontab.
    # No need to enqueue next jobs in this test as there would not be jobs enqueued anyway
    assert_nothing_raised do
      Gouda::Scheduler.build_scheduler_entries_list!({})
    end

    assert_nil Gouda::Workload.checkout_and_lock_one(executing_on: "test"), "The workload should not be picked for execution now"
    just_finished_workload = Gouda::Workload.where(state: "finished").first!
    assert_equal "finished", just_finished_workload.state
    assert just_finished_workload.error
    assert_match(/scheduler/, just_finished_workload.error.fetch("message"))
  end

  test "is able to accept a crontab" do
    tab = {
      first_hourly: {
        cron: "@hourly",
        class: "GoudaSchedulerTest::TestJob",
        args: ["one"],
        kwargs: {mandatory: "Yeah"}
      },
      second_minutely: {
        cron: "*/1 * * * *",
        class: "GoudaSchedulerTest::TestJob",
        args: [6],
        kwargs: {mandatory: "Yeah", optional: "something"}
      },
      third_hourly_with_args_and_kwargs: {
        cron: "@hourly",
        class: "GoudaSchedulerTest::TestJob",
        args: [1],
        kwargs: {mandatory: "alright"}
      },
      interval: {
        interval_seconds: 250,
        class: "GoudaSchedulerTest::TestJob",
        args: [4],
        kwargs: {mandatory: "tasty"}
      }
    }
    assert_nothing_raised do
      Gouda::Scheduler.build_scheduler_entries_list!(tab)
    end

    travel_to Time.utc(2023, 6, 23, 20, 0)
    assert_changes_by(-> { Gouda::Workload.count }, exactly: 4) do
      3.times do
        Gouda::Scheduler.upsert_workloads_from_entries_list!
      end
    end

    tab[:fifth] = {
      cron: "@hourly",
      class: "GoudaSchedulerTest::TestJob",
      kwargs: {mandatory: "good"}
    }

    Gouda::Scheduler.build_scheduler_entries_list!(tab)
    assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
      Gouda::Scheduler.upsert_workloads_from_entries_list!
    end

    assert tab.delete(:fifth)
    Gouda::Scheduler.build_scheduler_entries_list!(tab)
    assert_changes_by(-> { Gouda::Workload.count }, exactly: -1) do
      Gouda::Scheduler.upsert_workloads_from_entries_list!
    end

    Gouda::Workload.all.each(&:perform_and_update_state!)
    assert_equal 0, Gouda::Workload.errored.count
    assert_equal [6, {
      "optional" => "something",
      "mandatory" => "Yeah",
      "_aj_ruby2_keywords" => ["mandatory", "optional"]
    }], Gouda::Workload.find_by(scheduler_key: "second_minutely_*/1 * * * *_GoudaSchedulerTest::TestJob").serialized_params["arguments"]
  end
end
