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

  alias Tarakan.Profiles

  test "get_profile is case-insensitive and preloads identities" do
    account = account_fixture(%{handle: "signalghost"})

    profile = Profiles.get_profile("SignalGhost")
    assert profile.id == account.id
    assert Ecto.assoc_loaded?(profile.identities)
  end

  test "get_profile returns nil for an unknown handle" do
    assert Profiles.get_profile("nobody-here") == nil
  end

  test "contribution stats count only public work" do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)
    scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(2)})

    verifier = reviewer_account_fixture()
    confirmation_fixture(scan, verifier)

    submitter_stats = Profiles.contribution_stats(submitter)
    assert submitter_stats.reviews == 1
    assert submitter_stats.findings == 2
    assert submitter_stats.repositories == 1
    assert submitter_stats.verdicts == 0

    verifier_stats = Profiles.contribution_stats(verifier)
    # One legacy report check is projected onto both canonical findings.
    assert verifier_stats.verdicts == 2
    assert verifier_stats.reviews == 0
  end

  test "a restricted review does not count toward a profile" do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)
    scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})

    {:ok, _restricted} =
      Tarakan.Scans.update_visibility(
        Tarakan.Accounts.Scope.for_account(moderator_account_fixture()),
        scan,
        "restricted",
        %{
          "moderation_reason" => "takedown_review",
          "moderation_notes" => "Restricted in this test to confirm profiles stay public-only."
        }
      )

    stats = Profiles.contribution_stats(submitter)
    assert stats.reviews == 0
    assert stats.findings == 0
  end

  test "repeated report occurrences count as one canonical finding" do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)

    for _ <- 1..2 do
      scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
    end

    stats = Profiles.contribution_stats(submitter)
    assert stats.reviews == 2
    assert stats.findings == 1
  end

  test "recent activity lists reviews and verdicts newest first" do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)
    scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
    confirmation_fixture(scan, submitter |> then(fn _ -> reviewer_account_fixture() end))

    [review | _] = Profiles.recent_activity(submitter)
    assert review.kind == :review
    assert review.repository.id == repository.id
  end

  test "list helpers surface repositories, reviews, findings, and checks" do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)
    scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(2)})
    verifier = reviewer_account_fixture()
    confirmation_fixture(scan, verifier)

    repos = Profiles.list_repositories(submitter)
    assert length(repos) == 1
    assert hd(repos).id == repository.id

    reviews = Profiles.list_reviews(submitter)
    assert length(reviews) == 1
    assert hd(reviews).id == scan.id
    assert hd(reviews).findings_count == 2
    assert hd(reviews).commit_sha == scan.commit_sha

    findings = Profiles.list_findings(submitter)
    assert length(findings) == 2
    assert Enum.all?(findings, &is_binary(&1.public_id))
    assert Enum.all?(findings, &(&1.repository.id == repository.id))

    checks = Profiles.list_checks(verifier)
    assert length(checks) == 2
    assert Enum.all?(checks, &(&1.verdict == "confirmed"))
    assert Enum.all?(checks, &(&1.repository.id == repository.id))
  end

  describe "badges/1 and badges_for/1" do
    alias Tarakan.Repo
    alias Tarakan.Repositories.Repository
    alias Tarakan.Scans.{CanonicalFinding, FindingCheck, Scan}

    defp insert_repository(account, name) do
      %Repository{}
      |> Repository.registration_changeset(%{
        host: "github.com",
        owner: "badge-org",
        name: name
      })
      |> Ecto.Changeset.put_change(:submitted_by_id, account.id)
      |> Repo.insert!()
      |> listed_repository_fixture()
    end

    defp insert_scan(repository, account) do
      %Scan{}
      |> Scan.submission_changeset(valid_scan_attributes())
      |> Ecto.Changeset.put_change(:repository_id, repository.id)
      |> Ecto.Changeset.put_change(:submitted_by_id, account.id)
      |> Repo.insert!()
    end

    defp insert_canonical(repository, status) do
      unique = System.unique_integer([:positive])

      %CanonicalFinding{}
      |> CanonicalFinding.changeset(%{
        repository_id: repository.id,
        fingerprint: "badge-fingerprint-#{unique}",
        file_path: "lib/example/badge_#{unique}.ex",
        severity: "high",
        title: "Badge fixture finding #{unique}",
        description: "Inserted directly to pin a canonical finding's settled status.",
        status: status,
        first_seen_commit_sha: random_commit_sha(),
        last_seen_commit_sha: random_commit_sha()
      })
      |> Repo.insert!()
    end

    defp insert_check(canonical, account, verdict) do
      %FindingCheck{canonical_finding_id: canonical.id, account_id: account.id}
      |> FindingCheck.changeset(%{
        commit_sha: random_commit_sha(),
        verdict: verdict,
        provenance: "human",
        notes: "Inserted directly to pin a check's verdict for badge tests."
      })
      |> Repo.insert!()
    end

    test "first_blood goes to the account that scanned a listed repository first" do
      first = github_account_fixture()
      late = github_account_fixture()
      repository = listed_github_repository_fixture(first)

      scan_fixture(repository, first, %{"findings_json" => findings_json_fixture(1)})
      scan_fixture(repository, late, %{"findings_json" => findings_json_fixture(1)})

      assert :first_blood in Profiles.badges(first)
      refute :first_blood in Profiles.badges(late)
    end

    test "century requires scans on 100 distinct listed repositories" do
      prolific = github_account_fixture()

      for index <- 1..100 do
        repository = insert_repository(prolific, "century-#{index}")
        insert_scan(repository, prolific)
      end

      assert :century in Profiles.badges(prolific)

      almost = github_account_fixture()

      for index <- 1..99 do
        repository = insert_repository(almost, "almost-#{index}")
        insert_scan(repository, almost)
      end

      refute :century in Profiles.badges(almost)
    end

    test "eradicator requires a fixed check on a canonical that settled fixed" do
      checker = github_account_fixture()
      repository = listed_github_repository_fixture(github_account_fixture())

      settled_fixed = insert_canonical(repository, "fixed")
      insert_check(settled_fixed, checker, "fixed")

      assert :eradicator in Profiles.badges(checker)

      # A fixed verdict on a finding that settled otherwise does not count.
      other = github_account_fixture()
      settled_verified = insert_canonical(repository, "verified")
      insert_check(settled_verified, other, "fixed")

      refute :eradicator in Profiles.badges(other)
    end

    test "exorcist requires a public scan carrying a regression finding" do
      submitter = github_account_fixture()
      repository = listed_github_repository_fixture(submitter)

      scan = scan_fixture(repository, submitter, %{"findings_json" => findings_json_fixture(1)})
      [occurrence] = Repo.preload(scan, :findings).findings

      refute :exorcist in Profiles.badges(submitter)

      occurrence
      |> Ecto.Changeset.change(disposition: "regression")
      |> Repo.update!()

      assert :exorcist in Profiles.badges(submitter)
    end

    test "sharpshooter requires 10 distinct correct checks" do
      repository = listed_github_repository_fixture(github_account_fixture())
      sharpshooter = github_account_fixture()

      for _ <- 1..10 do
        canonical = insert_canonical(repository, "verified")
        insert_check(canonical, sharpshooter, "confirmed")
      end

      assert :sharpshooter in Profiles.badges(sharpshooter)

      # Nine correct checks, or checks that missed the settled status, do not count.
      almost = github_account_fixture()
      wrong = github_account_fixture()

      for _ <- 1..9 do
        canonical = insert_canonical(repository, "verified")
        insert_check(canonical, almost, "confirmed")
      end

      for _ <- 1..10 do
        canonical = insert_canonical(repository, "verified")
        insert_check(canonical, wrong, "disputed")
      end

      refute :sharpshooter in Profiles.badges(almost)
      refute :sharpshooter in Profiles.badges(wrong)
    end

    test "an account without qualifying public work has no badges" do
      account = github_account_fixture()
      assert Profiles.badges(account) == []
    end

    test "badges_for/1 returns a batched map and skips badgeless accounts" do
      first = github_account_fixture()
      late = github_account_fixture()
      idle = github_account_fixture()
      repository = listed_github_repository_fixture(first)

      scan_fixture(repository, first, %{"findings_json" => findings_json_fixture(1)})
      scan_fixture(repository, late, %{"findings_json" => findings_json_fixture(1)})

      result = Profiles.badges_for([first.id, late.id, idle.id])

      assert result[first.id] == [:first_blood]
      refute Map.has_key?(result, late.id)
      refute Map.has_key?(result, idle.id)

      assert Profiles.badges_for([]) == %{}
    end
  end
end
