require "test_helper"

class ActionPack::WebAuthn::Authenticator::ResponseTest < ActiveSupport::TestCase
  include WebauthnTestHelper

  setup do
    ActionPack::WebAuthn::Current.host = "example.com"

    @challenge = webauthn_challenge
    @origin = "https://example.com"
    @client_data_json = {
      challenge: @challenge,
      origin: @origin,
      type: "webauthn.create"
    }.to_json

    @authenticator_data = build_authenticator_data
    @response = TestableResponse.new(
      client_data_json: @client_data_json,
      authenticator_data: @authenticator_data,
      origin: @origin
    )
  end

  class TestableResponse < ActionPack::WebAuthn::Authenticator::Response
    attr_reader :authenticator_data

    def initialize(authenticator_data:, **attrs)
      super(**attrs)
      @authenticator_data = authenticator_data
    end
  end

  test "parses client data JSON" do
    assert_equal @challenge, @response.client_data["challenge"]
    assert_equal @origin, @response.client_data["origin"]
  end

  test "valid? returns true when challenge and origin match" do
    assert @response.valid?
  end

  test "valid? returns false when challenge in client data is invalid" do
    client_data_json = {
      challenge: "not-a-valid-signed-challenge",
      origin: @origin,
      type: "webauthn.create"
    }.to_json

    response = TestableResponse.new(
      client_data_json: client_data_json,
      authenticator_data: @authenticator_data,
      origin: @origin
    )

    assert_not response.valid?
  end

  test "valid? returns false when origin does not match" do
    @response.origin = "https://evil.com"
    assert_not @response.valid?
  end

  test "validate! raises when challenge in client data is invalid" do
    client_data_json = {
      challenge: "not-a-valid-signed-challenge",
      origin: @origin,
      type: "webauthn.create"
    }.to_json

    response = TestableResponse.new(
      client_data_json: client_data_json,
      authenticator_data: @authenticator_data,
      origin: @origin
    )

    error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
      response.validate!
    end

    assert_match /Challenge (is invalid|has expired)/, error.message
  end

  test "validate! raises when origin does not match" do
    @response.origin = "https://evil.com"

    error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
      @response.validate!
    end

    assert_equal "Origin does not match", error.message
  end

  test "validate! raises when crossOrigin is true" do
    client_data_json = {
      challenge: @challenge,
      origin: @origin,
      type: "webauthn.create",
      crossOrigin: true
    }.to_json

    response = TestableResponse.new(
      client_data_json: client_data_json,
      authenticator_data: @authenticator_data,
      origin: @origin
    )

    error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
      response.validate!
    end

    assert_equal "Cross-origin requests are not supported", error.message
  end

  test "validate! raises when relying party ID does not match" do
    rp_id_hash = Digest::SHA256.digest("evil.com")
    flags = 0x05
    sign_count = 0

    bytes = []
    bytes.concat(rp_id_hash.bytes)
    bytes << flags
    bytes.concat([ sign_count ].pack("N").bytes)

    wrong_rp_data = ActionPack::WebAuthn::Authenticator::Data.decode(bytes.pack("C*"))

    response = TestableResponse.new(
      client_data_json: @client_data_json,
      authenticator_data: wrong_rp_data,
      origin: @origin
    )

    error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
      response.validate!
    end

    assert_equal "Relying party ID does not match", error.message
  end

  test "validate! raises when tokenBinding status is present" do
    client_data_json = {
      challenge: @challenge,
      origin: @origin,
      type: "webauthn.create",
      tokenBinding: { status: "present", id: "some-id" }
    }.to_json

    response = TestableResponse.new(
      client_data_json: client_data_json,
      authenticator_data: @authenticator_data,
      origin: @origin
    )

    error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
      response.validate!
    end

    assert_equal "Token binding is not supported", error.message
  end

  private
    def build_authenticator_data
      rp_id_hash = Digest::SHA256.digest("example.com")
      flags = 0x05 # user present + user verified
      sign_count = 0

      bytes = []
      bytes.concat(rp_id_hash.bytes)
      bytes << flags
      bytes.concat([ sign_count ].pack("N").bytes)

      ActionPack::WebAuthn::Authenticator::Data.decode(bytes.pack("C*"))
    end
end
