defmodule X509.PathValidationTest do
  @default_scenario "rfc5280::chain-untrusted-root"

  def all_scenarios do
    "x509-limbo/limbo.json"
    |> File.read!()
    |> Jason.decode!()
    |> Map.get("testcases")
  end

  def all_scenarios_with_prefix(prefix) do
    all_scenarios()
    |> Enum.filter(fn scenario -> String.starts_with?(scenario["id"], prefix) end)
  end

  def load_scenario(id) do
    all_scenarios()
    |> Enum.find(&match?(%{"id" => ^id}, &1))
  end

  # sample test case:
  # %{
  #   "conflicts_with" => [],
  #   "description" => "Produces the following **invalid** chain:\n\n```\nroot -> ICA -> EE\n```\n\nThe intermediate CA is missing the BasicConstraints extension, which is disallowed\nunder RFC 5280 4.2.1.9:\n\n> Conforming CAs MUST include this extension in all CA certificates\n> that contain public keys used to validate digital signatures on\n> certificates and MUST mark the extension as critical in such\n> certificates.",
  #   "expected_peer_name" => %{"kind" => "DNS", "value" => "example.com"},
  #   "expected_peer_names" => [],
  #   "expected_result" => "FAILURE",
  #   "extended_key_usage" => [],
  #   "features" => [],
  #   "id" => "rfc5280::intermediate-ca-missing-basic-constraints",
  #   "key_usage" => [],
  #   "max_chain_depth" => nil,
  #   "peer_certificate" => "-----BEGIN CERTIFICATE-----\nMIIB/zCCAaagAwIBAgIUcCrpe2tuH1wjScMNDUbRjWyanjUwCgYIKoZIzj0EAwIw\najE5MDcGA1UECwwwNTc1NjkyODYyMTI4OTMzNTQ3MDY5NjE1NzEzMjMwMzU0MDc0\nNDA3NDg3MDgxODkzMS0wKwYDVQQDDCR4NTA5LWxpbWJvLWludGVybWVkaWF0ZS1w\nYXRobGVuLU5vbmUwIBcNNzAwMTAxMDAwMDAxWhgPMjk2OTA1MDMwMDAwMDFaMBYx\nFDASBgNVBAMMC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\n7uVRgZYxIKi5fQJsRMg7/fMthp6hDMWQoWn7+RF4wujYXjmrOABtxR7R8w0jB7/7\nOae1ZScFDja+XBDqMSzz76N8MHowHQYDVR0OBBYEFKgopYpP0qDkvwFnqeB4WqqM\nIrlVMB8GA1UdIwQYMBaAFGMpEL97MhdZzYNuBCvp5CYdo0rlMAsGA1UdDwQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDATAWBgNVHREEDzANggtleGFtcGxlLmNvbTAK\nBggqhkjOPQQDAgNHADBEAiBwgobT+DPME1kbb/hD13yMaYOjIjOFMjVZcD1qOSZW\nRgIgUFaw7bMjSwSMejTMQ7UKJl0Upe3UcH2Tmgl9nfRFXzI=\n-----END CERTIFICATE-----\n",
  #   "signature_algorithms" => [],
  #   "trusted_certs" => ["-----BEGIN CERTIFICATE-----\nMIIBjzCCATWgAwIBAgIUZNb19Bpqj/cq1hqbWdePjrDLDaUwCgYIKoZIzj0EAwIw\nGjEYMBYGA1UEAwwPeDUwOS1saW1iby1yb290MCAXDTcwMDEwMTAwMDAwMVoYDzI5\nNjkwNTAzMDAwMDAxWjAaMRgwFgYDVQQDDA94NTA5LWxpbWJvLXJvb3QwWTATBgcq\nhkjOPQIBBggqhkjOPQMBBwNCAAQqRB/bxpf7V8x8bqsVw4TeIu97YBHhZXI8TZdU\n75Y0LOgKIb2cypq+rNbiANN5sxyvqDYgnx64WD1TokKzVSvFo1cwVTAPBgNVHRMB\nAf8EBTADAQH/MAsGA1UdDwQEAwICBDAWBgNVHREEDzANggtleGFtcGxlLmNvbTAd\nBgNVHQ4EFgQUZ2bE/A9V+ggMZW5rC+1deHwOuxcwCgYIKoZIzj0EAwIDSAAwRQIg\nMolr6fQ+LiHQPFZk1PtK5J6quihEBy0UUHDwUu3gJrICIQDBT1V6QrKV1bF4rktO\ny3X9xC4g9FLbPmh7VT1jyjt5QQ==\n-----END CERTIFICATE-----\n"],
  #   "untrusted_intermediates" => [],
  #   "validation_kind" => "SERVER",
  #   "validation_time" => nil
  # }

  def run(scenario \\ nil)

  def run(nil), do: run(@default_scenario)

  def run(scenario_name) when is_binary(scenario_name) do
    scenario_name
    |> load_scenario()
    |> run()
  end

  def run(scenario) do
    IO.puts("ID: #{scenario["id"]}")

    case {path_validation(
            scenario["peer_certificate"],
            scenario["trusted_certs"],
            scenario["untrusted_intermediates"]
          ), scenario["expected_result"]} do
      {{:ok, _}, "SUCCESS"} ->
        IO.puts("Validation passed, as expected\n")

      {{:ok, _}, "FAILURE"} ->
        IO.puts("Description:\n#{scenario["description"]}")
        IO.puts("Validation passed, but should have failed!\n")

      {{:error, reason}, "SUCCESS"} ->
        IO.puts("Description:\n#{scenario["description"]}")
        IO.puts("Validation failed with #{inspect(reason)}, but should have passed!\n")

      {{:error, reason}, "FAILURE"} ->
        IO.puts("Validation failed with #{inspect(reason)}, as expected\n")
    end
  end

  defp pem_to_der!(pem) do
    pem
    |> X509.Certificate.from_pem!()
    |> X509.Certificate.to_der()
  end

  defp path_validation(peer_pem, cacerts_pem, chain_pem) do
    with {:ok, peer} <- X509.Certificate.from_pem(peer_pem),
         cacerts = Enum.map(cacerts_pem, &pem_to_der!/1),
         chain = Enum.map(chain_pem, &pem_to_der!/1),
         {:ok, cacert} <- select_ca_cert(cacerts, hd(chain ++ [peer])) do
      full_chain = Enum.reverse(chain) ++ [cacert]
      :public_key.pkix_path_validation(peer, full_chain, [])
    end
  end

  defp select_ca_cert(cacerts, cert) do
    case Enum.find(cacerts, &:public_key.pkix_is_issuer(cert, &1)) do
      nil ->
        {:error, :not_trusted}

      cacert ->
        {:ok, cacert}
    end
  end
end
