defmodule Tarakan.InfestationSwarmSweepTest do
  use Tarakan.DataCase, async: false
  use Oban.Testing, repo: Tarakan.Repo

  alias Tarakan.FindingMemory
  alias Tarakan.Infestations.{Swarm, SwarmSweep}
  alias Tarakan.Repo
  alias Tarakan.Work.ReviewTask

  setup do
    previous = Application.get_env(:tarakan, :swarm_sweep, [])
    on_exit(fn -> Application.put_env(:tarakan, :swarm_sweep, previous) end)

    submitter = github_account_fixture()
    other = github_account_fixture()
    repo_a = listed_github_repository_fixture(submitter)

    {:ok, repo_b} = Tarakan.Repositories.register_github_repository("acme/widget", other)
    repo_b = listed_repository_fixture(repo_b)

    title = "Sweep sample #{System.unique_integer([:positive])}"

    scan_fixture(repo_a, submitter, %{"findings_json" => findings_json(title, "lib/a.ex")})
    scan_fixture(repo_b, other, %{"findings_json" => findings_json(title, "src/b.rs")})

    # The swarm exists for instances the automatic per-report job missed.
    cancel_auto_jobs()

    %{pattern_key: FindingMemory.pattern_key(title)}
  end

  defp configure(overrides) do
    base = [
      actor_handle: nil,
      patterns_per_run: 25,
      jobs_per_run: 40,
      max_jobs_per_pattern: 8,
      min_repos: 2,
      days: 90
    ]

    Application.put_env(:tarakan, :swarm_sweep, Keyword.merge(base, overrides))
  end

  test "does nothing at all until an actor is configured" do
    configure([])

    assert :ok = perform_job(SwarmSweep, %{})
    assert verify_jobs("open") == 0
    assert Repo.all(Swarm) == []
  end

  test "does nothing when the configured actor is missing or unprivileged" do
    configure(actor_handle: "nobody-here")
    assert :ok = perform_job(SwarmSweep, %{})
    assert verify_jobs("open") == 0

    member = account_fixture()
    configure(actor_handle: member.handle)

    assert :ok = perform_job(SwarmSweep, %{})
    assert verify_jobs("open") == 0
    assert Repo.all(Swarm) == []
  end

  test "swarms patterns nobody opened, as the configured actor", %{pattern_key: pattern_key} do
    actor = moderator_account_fixture()
    configure(actor_handle: actor.handle)

    assert :ok = perform_job(SwarmSweep, %{})

    assert verify_jobs("open") == 2
    assert [%Swarm{pattern_key: ^pattern_key, jobs_opened: 2}] = Repo.all(Swarm)

    assert Repo.all(from t in ReviewTask, where: t.status == "open", select: t.created_by_id)
           |> Enum.uniq() == [actor.id]
  end

  test "one generic pattern cannot spend the whole run", %{pattern_key: pattern_key} do
    actor = moderator_account_fixture()
    configure(actor_handle: actor.handle, max_jobs_per_pattern: 1)

    assert :ok = perform_job(SwarmSweep, %{})

    assert verify_jobs("open") == 1
    assert [%Swarm{pattern_key: ^pattern_key, jobs_opened: 1}] = Repo.all(Swarm)
  end

  test "spends nothing beyond the run budget" do
    actor = moderator_account_fixture()
    configure(actor_handle: actor.handle, jobs_per_run: 0)

    assert :ok = perform_job(SwarmSweep, %{})
    assert verify_jobs("open") == 0
  end

  test "respects the cooldown a manual press left behind", %{pattern_key: pattern_key} do
    actor = moderator_account_fixture()
    configure(actor_handle: actor.handle)

    assert :ok = perform_job(SwarmSweep, %{})
    assert verify_jobs("open") == 2

    # Second night, same pattern: still cooling, so nothing new is opened.
    assert :ok = perform_job(SwarmSweep, %{})
    assert verify_jobs("open") == 2
    assert length(Repo.all(Swarm)) == 1
    assert Tarakan.Infestations.swarm_cooldown_seconds(pattern_key) > 0
  end

  defp findings_json(title, path) do
    Jason.encode!(%{
      "tarakan_scan_format" => 1,
      "findings" => [
        %{
          "file" => path,
          "line_start" => 1,
          "line_end" => 2,
          "severity" => "high",
          "title" => title,
          "description" => "Shared issue class for sweep tests."
        }
      ]
    })
  end

  defp cancel_auto_jobs do
    Repo.update_all(from(t in ReviewTask, where: t.kind == "verify_findings"),
      set: [status: "cancelled"]
    )
  end

  defp verify_jobs(status) do
    Repo.aggregate(
      from(t in ReviewTask, where: t.kind == "verify_findings" and t.status == ^status),
      :count
    )
  end
end
