defmodule Tarakan.Scans.CanonicalFindingTest do
  use ExUnit.Case, async: true

  alias Tarakan.Scans.CanonicalFinding

  describe "disclosure_changeset/2" do
    test "accepts valid disclosure metadata" do
      changeset =
        CanonicalFinding.disclosure_changeset(%CanonicalFinding{}, %{
          reproduction_steps: "1. POST /login with ' OR 1=1 --",
          affected_versions: ">= 1.0, < 1.4.2",
          cwe_id: "CWE-89",
          cve_id: "CVE-2026-12345",
          vendor_notified_at: ~U[2026-07-01 12:00:00.000000Z]
        })

      assert changeset.valid?
    end

    test "rejects malformed CWE and CVE identifiers" do
      for cwe_id <- ["89", "cwe-89", "CWE-", "CWE-89x"] do
        changeset =
          CanonicalFinding.disclosure_changeset(%CanonicalFinding{}, %{cwe_id: cwe_id})

        assert %{cwe_id: [_ | _]} = errors_on(changeset)
      end

      for cve_id <- ["CVE-26-1234", "CVE-2026-123", "CVE-2026-12345678", "cve-2026-1234"] do
        changeset =
          CanonicalFinding.disclosure_changeset(%CanonicalFinding{}, %{cve_id: cve_id})

        assert %{cve_id: [_ | _]} = errors_on(changeset)
      end
    end

    test "caps reproduction steps and affected versions length" do
      changeset =
        CanonicalFinding.disclosure_changeset(%CanonicalFinding{}, %{
          reproduction_steps: String.duplicate("a", 10_001),
          affected_versions: String.duplicate("1", 501)
        })

      assert %{reproduction_steps: [_ | _], affected_versions: [_ | _]} = errors_on(changeset)
    end

    test "ignores non-disclosure attributes" do
      changeset =
        CanonicalFinding.disclosure_changeset(%CanonicalFinding{}, %{
          title: "must not be cast",
          status: "fixed"
        })

      assert changeset.valid?
      assert changeset.changes == %{}
    end
  end

  defp errors_on(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
      Regex.replace(~r"%{(\w+)}", message, fn _, key ->
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      end)
    end)
  end
end
