defmodule TarakanWeb.API.FindingSignalsApiTest do
  @moduledoc """
  The three worker submission endpoints that had no HTTP surface until now:
  calibrated severity, code embeddings, and cluster detectors.
  """
  use TarakanWeb.ConnCase, async: true

  import Ecto.Query, only: [from: 2]

  alias Tarakan.Accounts.ApiCredentials
  alias Tarakan.Repo
  alias Tarakan.Scans.CanonicalFinding

  setup %{conn: conn} do
    submitter = github_account_fixture()
    repository = listed_github_repository_fixture(submitter)

    findings =
      Jason.encode!(%{
        "tarakan_scan_format" => 1,
        "findings" => [
          %{
            "file" => "lib/vulnerable.ex",
            "severity" => "critical",
            "title" => "Command injection #{System.unique_integer([:positive])}",
            "description" => "User input reaches System.cmd without escaping."
          }
        ]
      })

    scan_fixture(repository, submitter, %{"findings_json" => findings})
    finding = Repo.one!(from f in CanonicalFinding, where: f.repository_id == ^repository.id)

    reviewer = reviewer_account_fixture()

    {:ok, token, _cred} =
      ApiCredentials.create(reviewer, %{
        name: "signals",
        scopes: ["reviews:read", "reviews:verify"]
      })

    %{
      conn: put_req_header(conn, "authorization", "Bearer #{token}"),
      anon: build_conn(),
      repository: repository,
      finding: finding
    }
  end

  defp signal_path(repository, finding, suffix) do
    "/api/github.com/#{repository.owner}/#{repository.name}/findings/#{finding.public_id}/#{suffix}"
  end

  describe "POST .../severity" do
    test "stores a rescore beside the submitter's claim", ctx do
      conn =
        post(ctx.conn, signal_path(ctx.repository, ctx.finding, "severity"), %{
          "severity" => "low",
          "rubric" => "rubric-v1"
        })

      assert %{"calibrated_severity" => "low"} = json_response(conn, 200)
      assert Repo.get!(CanonicalFinding, ctx.finding.id).severity == "critical"
    end

    test "rejects a severity outside the scale", ctx do
      conn =
        post(ctx.conn, signal_path(ctx.repository, ctx.finding, "severity"), %{
          "severity" => "catastrophic",
          "rubric" => "rubric-v1"
        })

      assert %{"errors" => %{"calibrated_severity" => _}} = json_response(conn, 422)
    end
  end

  describe "POST .../embedding" do
    test "stores the vector and assigns a cluster", ctx do
      conn =
        post(ctx.conn, signal_path(ctx.repository, ctx.finding, "embedding"), %{
          "embedding" => [0.1, 0.9, 0.2],
          "embedding_model" => "test-embed-v1"
        })

      assert %{"code_pattern_key" => key} = json_response(conn, 200)
      assert key =~ ~r/^code:/
    end

    test "rejects an all-zero vector, which would match everything", ctx do
      conn =
        post(ctx.conn, signal_path(ctx.repository, ctx.finding, "embedding"), %{
          "embedding" => [0.0, 0.0, 0.0],
          "embedding_model" => "test-embed-v1"
        })

      assert %{"errors" => %{"embedding" => _}} = json_response(conn, 422)
    end
  end

  describe "POST /api/patterns/:key/rule" do
    @rule """
    rules:
      - id: tarakan-cmd-injection
        message: user input reaches System.cmd
        severity: ERROR
        languages: [elixir]
        patterns:
          - pattern: System.cmd($C, $A)
    """

    test "accepts a validated detector and marks it servable", ctx do
      conn =
        post(ctx.conn, "/api/patterns/code:abc/rule", %{
          "rule_yaml" => @rule,
          "language" => "elixir",
          "checked_count" => 3,
          "matched_count" => 2,
          "matched_finding_ids" => [Ecto.UUID.generate(), Ecto.UUID.generate()]
        })

      assert %{"servable" => true, "matched_count" => 2} = json_response(conn, 201)
    end

    test "a detector that matched nothing is stored but not servable", ctx do
      conn =
        post(ctx.conn, "/api/patterns/code:abc/rule", %{
          "rule_yaml" => @rule,
          "checked_count" => 3,
          "matched_count" => 0,
          "matched_finding_ids" => []
        })

      assert %{"servable" => false, "validated_at" => nil} = json_response(conn, 201)
    end

    test "rejects a match count with no receipts", ctx do
      conn =
        post(ctx.conn, "/api/patterns/code:abc/rule", %{
          "rule_yaml" => @rule,
          "checked_count" => 3,
          "matched_count" => 2,
          "matched_finding_ids" => []
        })

      assert %{"errors" => %{"matched_finding_ids" => _}} = json_response(conn, 422)
    end

    test "anonymous submissions are refused", ctx do
      conn = post(ctx.anon, "/api/patterns/code:abc/rule", %{"rule_yaml" => @rule})
      assert json_response(conn, 401)
    end
  end
end
