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

  alias Tarakan.Accounts.{Account, Scope}
  alias Tarakan.Credits
  alias Tarakan.Repo
  alias Tarakan.Work
  alias Tarakan.Work.ReviewTask

  defp opener(credits) do
    account = active_account_fixture() |> fund_credits(credits)
    {account, Scope.for_account(account)}
  end

  defp moderator_scope do
    account =
      active_account_fixture()
      |> Account.authorization_changeset(%{
        state: "active",
        platform_role: "moderator",
        trust_tier: "reviewer"
      })
      |> Repo.update!()

    Scope.for_account(account)
  end

  defp attrs(scan) do
    %{
      "commit_sha" => scan.commit_sha,
      "title" => "Paid review of the auth handler",
      "description" => "Look at the session handling at the pinned commit."
    }
  end

  describe "open_paid_task/3" do
    setup do
      submitter = github_account_fixture()
      repository = listed_github_repository_fixture(submitter)
      scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})

      %{repository: repository, scan: scan}
    end

    test "spends credits and opens a public job in one step", %{
      repository: repository,
      scan: scan
    } do
      cost = Work.paid_task_cost()
      {account, scope} = opener(cost * 2)

      assert {:ok, %ReviewTask{} = task} =
               Work.open_paid_task(scope, repository, attrs(scan))

      # Open and public immediately: no steward has to publish it.
      assert task.status == "open"
      assert task.visibility == "public"
      assert task.repository_id == repository.id
      assert task.created_by_id == account.id
      assert task.commit_sha == scan.commit_sha

      assert Credits.balance(account) == cost

      assert [entry] =
               Repo.all(
                 from item in Credits.Entry,
                   where:
                     item.kind == "spend_review_job" and item.subject_type == "review_task" and
                       item.subject_id == ^task.id
               )

      assert entry.amount == -cost
    end

    test "an opener without the credits gets nothing, and no job is left behind", %{
      repository: repository,
      scan: scan
    } do
      {account, scope} = opener(Work.paid_task_cost() - 1)
      before = Repo.aggregate(ReviewTask, :count)

      assert {:error, :insufficient_credits} =
               Work.open_paid_task(scope, repository, attrs(scan))

      # The debit and the insert share a transaction, so a rejected payment
      # must not leave an unpaid job sitting in the public queue.
      assert Repo.aggregate(ReviewTask, :count) == before
      assert Credits.balance(account) == Work.paid_task_cost() - 1
    end

    test "falls back to the repository head when no commit is given", %{repository: repository} do
      {_account, scope} = opener(Work.paid_task_cost())

      assert {:ok, %ReviewTask{} = task} =
               Work.open_paid_task(scope, repository, %{
                 "title" => "Review the tip",
                 "description" => "Whatever is current."
               })

      assert String.length(task.commit_sha) == 40
    end

    test "requires an account in good standing", %{repository: repository, scan: scan} do
      probation = account_fixture() |> fund_credits(Work.paid_task_cost())

      assert {:error, _reason} =
               Work.open_paid_task(Scope.for_account(probation), repository, attrs(scan))
    end
  end

  describe "refund_review_job/2" do
    setup do
      submitter = github_account_fixture()
      repository = listed_github_repository_fixture(submitter)
      scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
      cost = Work.paid_task_cost()
      {account, scope} = opener(cost)

      {:ok, task} = Work.open_paid_task(scope, repository, attrs(scan))

      %{account: account, task: task, cost: cost}
    end

    test "returns the credits to the opener", %{account: account, task: task, cost: cost} do
      assert Credits.balance(account) == 0

      assert {:ok, _entry} = Work.refund_review_job(moderator_scope(), task)
      assert Credits.balance(account) == cost
    end

    test "refunding twice does not pay twice", %{account: account, task: task, cost: cost} do
      scope = moderator_scope()

      assert {:ok, _entry} = Work.refund_review_job(scope, task)
      assert {:ok, :already_refunded} = Work.refund_review_job(scope, task)

      assert Credits.balance(account) == cost
    end

    test "a job nobody paid for cannot be refunded", %{task: task} do
      free_task = %ReviewTask{task | id: task.id + 9_999}

      assert {:error, :not_a_paid_job} = Work.refund_review_job(moderator_scope(), free_task)
    end

    test "requires moderator standing", %{task: task} do
      assert {:error, _reason} =
               Work.refund_review_job(Scope.for_account(active_account_fixture()), task)
    end
  end
end
