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

  alias Tarakan.Repo
  alias Tarakan.Scans.CanonicalFinding
  alias Tarakan.SecurityPosture

  # Posture reads the fix stamps directly, so tests set them rather than
  # driving a full quorum for every finding.
  defp finding_fixture(repository, attrs) do
    now = DateTime.utc_now()

    %CanonicalFinding{}
    |> Ecto.Changeset.change(
      Map.merge(
        %{
          repository_id: repository.id,
          fingerprint: "fp-#{System.unique_integer([:positive])}",
          file_path: "lib/app.ex",
          line_start: 1,
          severity: "high",
          title: "Finding #{System.unique_integer([:positive])}",
          description: "Body.",
          status: "open",
          first_seen_commit_sha: random_commit_sha(),
          last_seen_commit_sha: random_commit_sha(),
          inserted_at: now,
          updated_at: now
        },
        attrs
      )
    )
    |> Repo.insert!()
  end

  defp days_ago(days), do: DateTime.add(DateTime.utc_now(), -days * 86_400, :second)

  describe "repository_posture/1" do
    test "reports no median until something is fixed" do
      repository = listed_github_repository_fixture()
      finding_fixture(repository, %{status: "verified", verified_at: days_ago(10)})

      posture = SecurityPosture.repository_posture(repository)

      assert posture.median_days_to_fix == nil
      assert posture.fixed_count == 0
      assert posture.open_count == 1
      assert posture.verified_open_count == 1
      assert posture.grade == :unrated
      assert_in_delta posture.oldest_open_days, 10.0, 0.1
    end

    test "takes the median across fixed findings, measured from verification" do
      repository = listed_github_repository_fixture()

      # Fixed after 2, 4 and 12 days: median is 4.
      for days <- [2, 4, 12] do
        finding_fixture(repository, %{
          status: "fixed",
          verified_at: days_ago(days + 30),
          fixed_at: days_ago(30),
          fixed_commit_sha: random_commit_sha()
        })
      end

      posture = SecurityPosture.repository_posture(repository)

      assert posture.median_days_to_fix == 4.0
      assert posture.fixed_count == 3
      assert posture.open_count == 0
      assert posture.grade == :fast
    end

    test "grades slower repositories down without hiding the backlog" do
      repository = listed_github_repository_fixture()

      finding_fixture(repository, %{
        status: "fixed",
        verified_at: days_ago(200),
        fixed_at: days_ago(50),
        fixed_commit_sha: random_commit_sha()
      })

      finding_fixture(repository, %{status: "open", inserted_at: days_ago(120)})

      posture = SecurityPosture.repository_posture(repository)

      assert posture.median_days_to_fix == 150.0
      assert posture.grade == :stale
      assert posture.open_count == 1
      assert_in_delta posture.oldest_open_days, 120.0, 0.1
    end
  end

  describe "leaderboard/1" do
    test "ranks fastest first and excludes thin track records" do
      fast = listed_github_repository_fixture()
      slow = listed_hosted_repository_fixture()
      thin = listed_hosted_repository_fixture()

      for _index <- 1..3 do
        finding_fixture(fast, %{
          status: "fixed",
          verified_at: days_ago(31),
          fixed_at: days_ago(30),
          fixed_commit_sha: random_commit_sha()
        })

        finding_fixture(slow, %{
          status: "fixed",
          verified_at: days_ago(90),
          fixed_at: days_ago(30),
          fixed_commit_sha: random_commit_sha()
        })
      end

      finding_fixture(thin, %{
        status: "fixed",
        verified_at: days_ago(31),
        fixed_at: days_ago(30),
        fixed_commit_sha: random_commit_sha()
      })

      rows = SecurityPosture.leaderboard(min_fixed: 3)

      assert [first, second] = rows
      assert first.repository_id == fast.id
      assert first.median_days_to_fix == 1.0
      assert second.repository_id == slow.id
      assert second.median_days_to_fix == 60.0
      refute Enum.any?(rows, &(&1.repository_id == thin.id))
    end
  end

  describe "badge/1" do
    test "reports the median when one exists" do
      repository = listed_github_repository_fixture()

      finding_fixture(repository, %{
        status: "fixed",
        verified_at: days_ago(33),
        fixed_at: days_ago(30),
        fixed_commit_sha: random_commit_sha()
      })

      assert %{label: "security", message: "3d median fix"} = SecurityPosture.badge(repository)
    end

    test "falls back to the open count, then to no findings" do
      repository = listed_github_repository_fixture()
      assert %{message: "no findings"} = SecurityPosture.badge(repository)

      finding_fixture(repository, %{status: "open"})
      assert %{message: "1 open"} = SecurityPosture.badge(repository)
    end
  end
end
