# frozen_string_literal: true

require "spec_helper"

require "securerandom"
require "webauthn/authenticator_attestation_response"
require "webauthn/configuration"
require "webauthn/public_key_credential_with_attestation"

RSpec.describe "PublicKeyCredentialWithAttestation" do
  describe "#verify" do
    let(:public_key_credential) do
      WebAuthn::PublicKeyCredentialWithAttestation.new(
        type: type,
        id: id,
        raw_id: raw_id,
        authenticator_attachment: authenticator_attachment,
        response: attestation_response
      )
    end

    let(:type) { "public-key" }
    let(:id) { WebAuthn::Encoders::Base64UrlEncoder.encode(raw_id) }
    let(:raw_id) { SecureRandom.random_bytes(16) }
    let(:authenticator_attachment) { 'platform' }

    let(:attestation_response) do
      response = client.create(challenge: raw_challenge)["response"]

      WebAuthn::AuthenticatorAttestationResponse.new(
        attestation_object: response["attestationObject"],
        client_data_json: response["clientDataJSON"]
      )
    end

    let(:client) { WebAuthn::FakeClient.new(origin, encoding: false) }
    let(:challenge) { WebAuthn::Encoders::Base64UrlEncoder.encode(raw_challenge) }
    let(:raw_challenge) { fake_challenge }
    let(:origin) { fake_origin }

    before do
      WebAuthn.configuration.allowed_origins = [origin]
    end

    it "works" do
      expect(public_key_credential.verify(challenge)).to be_truthy

      expect(public_key_credential.id).not_to be_empty
      expect(public_key_credential.public_key).not_to be_empty
      expect(public_key_credential.sign_count).to eq(0)
    end

    context "when type is invalid" do
      context "because it is missing" do
        let(:type) { nil }

        it "fails" do
          expect { public_key_credential.verify(challenge) }.to raise_error(RuntimeError)
        end
      end

      context "because it is something else" do
        let(:type) { "password" }

        it "fails" do
          expect { public_key_credential.verify(challenge) }.to raise_error(RuntimeError)
        end
      end
    end

    context "when id is invalid" do
      context "because it is missing" do
        let(:id) { nil }

        it "fails" do
          expect { public_key_credential.verify(challenge) }.to raise_error(RuntimeError)
        end
      end

      context "because it is not the base64url of raw id" do
        let(:id) { WebAuthn::Encoders::Base64UrlEncoder.encode(raw_id + "a") }

        it "fails" do
          expect { public_key_credential.verify(challenge) }.to raise_error(RuntimeError)
        end
      end
    end

    context "when challenge class is invalid" do
      it "raise error" do
        expect {
          public_key_credential.verify(nil)
        }.to raise_error(WebAuthn::PublicKeyCredential::InvalidChallengeError)
      end
    end

    context "when challenge value is invalid" do
      it "fails" do
        expect {
          public_key_credential.verify(WebAuthn::Encoders::Base64UrlEncoder.encode("another challenge"))
        }.to raise_error(WebAuthn::ChallengeVerificationError)
      end
    end

    context "when clientExtensionResults" do
      context "are not received" do
        let(:public_key_credential) do
          WebAuthn::PublicKeyCredentialWithAttestation.new(
            type: type,
            id: id,
            raw_id: raw_id,
            client_extension_outputs: nil,
            response: attestation_response
          )
        end

        it "works" do
          expect(public_key_credential.verify(challenge)).to be_truthy

          expect(public_key_credential.client_extension_outputs).to be_nil
        end
      end

      context "are received" do
        let(:public_key_credential) do
          WebAuthn::PublicKeyCredentialWithAttestation.new(
            type: type,
            id: id,
            raw_id: raw_id,
            client_extension_outputs: { "appid" => "true" },
            response: attestation_response
          )
        end

        it "works" do
          expect(public_key_credential.verify(challenge)).to be_truthy

          expect(public_key_credential.client_extension_outputs).to eq({ "appid" => "true" })
        end
      end
    end

    context "when authentication extension input" do
      context "is not received" do
        let(:attestation_response) do
          response = client.create(challenge: raw_challenge, extensions: nil)["response"]

          WebAuthn::AuthenticatorAttestationResponse.new(
            attestation_object: response["attestationObject"],
            client_data_json: response["clientDataJSON"]
          )
        end

        it "works" do
          expect(public_key_credential.verify(challenge)).to be_truthy

          expect(public_key_credential.authenticator_extension_outputs).to be_nil
        end
      end

      context "is received" do
        let(:attestation_response) do
          response = client.create(
            challenge: raw_challenge,
            extensions: { "txAuthSimple" => "Could you please verify yourself?" }
          )["response"]

          WebAuthn::AuthenticatorAttestationResponse.new(
            attestation_object: response["attestationObject"],
            client_data_json: response["clientDataJSON"]
          )
        end

        it "works" do
          expect(public_key_credential.verify(challenge)).to be_truthy

          expect(public_key_credential.authenticator_extension_outputs)
            .to eq({ "txAuthSimple" => "Could you please verify yourself?" })
        end
      end
    end

    context "when user_presence" do
      context "is not set" do
        it "correcly delegates its value to the response" do
          expect(attestation_response).to receive(:verify).with(anything, hash_including(user_presence: nil))

          public_key_credential.verify(challenge)
        end
      end

      context "is set to false" do
        it "correcly delegates its value to the response" do
          expect(attestation_response).to receive(:verify).with(anything, hash_including(user_presence: false))

          public_key_credential.verify(challenge, user_presence: false)
        end
      end

      context "is set to true" do
        it "correcly delegates its value to the response" do
          expect(attestation_response).to receive(:verify).with(anything, hash_including(user_presence: true))

          public_key_credential.verify(challenge, user_presence: true)
        end
      end
    end
  end
end
