defmodule Tarakan.LeaderboardTest do
  use Tarakan.DataCase, async: true

  alias Tarakan.Leaderboard
  alias Tarakan.Repo
  alias Tarakan.Scans.CanonicalFinding

  # Verifies a submitter's review through the real quorum machinery so they
  # earn verification reputation.
  defp verified_contributor(findings) when is_integer(findings) do
    verified_contributor_with_json(findings_json_fixture(findings))
  end

  defp verified_contributor_with_json(findings_json) do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)
    scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json})
    confirmation_fixture(scan, reviewer_account_fixture())
    confirmation_fixture(scan, reviewer_account_fixture())
    submitter
  end

  defp findings_json_with_severity(severity) do
    Jason.encode!(%{
      "tarakan_scan_format" => 1,
      "findings" => [
        %{
          "file" => "lib/example/module.ex",
          "line_start" => 10,
          "line_end" => 15,
          "severity" => severity,
          "title" => "Filtered finding #{System.unique_integer([:positive])}",
          "description" => "Leaderboard filter fixture."
        }
      ]
    })
  end

  test "ranks verified contributors by reputation and excludes zero-rep accounts" do
    _bigger = verified_contributor(3)
    _smaller = verified_contributor(1)

    unverified =
      github_account_fixture() |> then(fn a -> listed_github_repository_fixture(a) && a end)

    entries = Leaderboard.top(:reputation)

    # Every ranked account has positive reputation; the account that only
    # registered a repo (no verified work) is absent.
    assert Enum.all?(entries, &(&1.reputation.total > 0))
    refute Enum.any?(entries, &(&1.account.id == unverified.id))

    totals = Enum.map(entries, & &1.reputation.total)
    assert totals == Enum.sort(totals, :desc)
    # The contributor with more confirmed findings tops the board.
    assert hd(entries).stats.findings == 3
  end

  test "sorting by findings orders by finding count" do
    _many = verified_contributor(3)
    _few = verified_contributor(1)

    entries = Leaderboard.top(:findings)
    findings = Enum.map(entries, & &1.stats.findings)
    assert findings == Enum.sort(findings, :desc)
  end

  test "an empty record yields an empty leaderboard" do
    assert Leaderboard.top() == []
  end

  test "the severity filter keeps only contributors with verified findings of that severity" do
    critical = verified_contributor_with_json(findings_json_with_severity("critical"))
    low = verified_contributor_with_json(findings_json_with_severity("low"))

    entries = Leaderboard.top(:reputation, severity: "critical")
    assert Enum.any?(entries, &(&1.account.id == critical.id))
    refute Enum.any?(entries, &(&1.account.id == low.id))

    unfiltered = Leaderboard.top(:reputation)
    assert Enum.any?(unfiltered, &(&1.account.id == low.id))
  end

  test "the window filter keeps only contributors with recent verified work" do
    old = verified_contributor_with_json(findings_json_with_severity("high"))

    past = DateTime.add(DateTime.utc_now(), -40, :day) |> DateTime.truncate(:second)
    Repo.update_all(CanonicalFinding, set: [inserted_at: past])

    recent = verified_contributor_with_json(findings_json_with_severity("high"))

    entries = Leaderboard.top(:reputation, window: 7)
    assert Enum.any?(entries, &(&1.account.id == recent.id))
    refute Enum.any?(entries, &(&1.account.id == old.id))

    all_time = Leaderboard.top(:reputation, window: :all)
    assert Enum.any?(all_time, &(&1.account.id == old.id))
  end

  test "rows expose the account's slashed stake count" do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)

    verified =
      scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})

    confirmation_fixture(verified, reviewer_account_fixture())
    confirmation_fixture(verified, reviewer_account_fixture())

    # A second report (a distinct finding) is refuted by a dispute quorum.
    refuted =
      scan_fixture(repository, submitter, %{
        "findings_json" => findings_json_with_severity("high")
      })

    confirmation_fixture(refuted, reviewer_account_fixture(), "disputed")
    confirmation_fixture(refuted, reviewer_account_fixture(), "disputed")

    entry = Enum.find(Leaderboard.top(:reputation), &(&1.account.id == submitter.id))
    assert entry.slashed_stakes == 1
  end
end
