# frozen_string_literal: true

RSpec.describe JWT do
  describe '.decode for JWK usecase' do
    let(:keypair)       { test_pkey('rsa-2048-private.pem') }
    let(:jwk)           { JWT::JWK.new(keypair) }
    let(:valid_key)     { jwk.export }
    let(:public_jwks) { { keys: [valid_key, { kid: 'not_the_correct_one', kty: 'oct', k: 'secret' }] } }
    let(:token_payload) { { 'data' => 'something' } }
    let(:token_headers) { { kid: jwk.kid } }
    let(:algorithm)     { 'RS512' }
    let(:signed_token)  { described_class.encode(token_payload, jwk.signing_key, algorithm, token_headers) }

    context 'when JWK features are used manually' do
      it 'is able to decode the token' do
        payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm] }) do |header, _payload|
          JWT::JWK.import(public_jwks[:keys].find { |key| key[:kid] == header['kid'] }).verify_key
        end
        expect(payload).to eq(token_payload)
      end
    end

    context 'when jwk keys are given as an array' do
      context 'and kid is in the set' do
        it 'is able to decode the token' do
          payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks })
          expect(payload).to eq(token_payload)
        end
      end

      context 'and kid is not in the set' do
        before do
          public_jwks[:keys].first[:kid] = 'NOT_A_MATCH'
        end
        it 'raises an exception' do
          expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error(
            JWT::DecodeError, /Could not find public key for kid .*/
          )
        end
      end

      context 'and x5t is in the set' do
        let(:x5t) { Base64.urlsafe_encode64(OpenSSL::Digest::SHA1.new(keypair.to_der).digest, padding: false) }
        let(:valid_key) { jwk.export.merge({ x5t: x5t }) }
        let(:token_headers) { { x5t: x5t } }
        it 'is able to decode the token' do
          payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks, key_fields: [:x5t] })
          expect(payload).to eq(token_payload)
        end
      end

      context 'and both kid and x5t is in the set' do
        let(:x5t) { Base64.urlsafe_encode64(OpenSSL::Digest::SHA1.new(keypair.to_der).digest, padding: false) }
        let(:valid_key) { jwk.export.merge({ x5t: x5t }) }
        let(:token_headers) { { x5t: x5t, kid: 'NOT_A_MATCH' } }
        it 'is able to decode the token based on the priority of the key defined in key_fields' do
          payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks, key_fields: %i[x5t kid] })
          expect(payload).to eq(token_payload)
        end
      end

      context 'no keys are found in the set' do
        let(:public_jwks) { { keys: [] } }
        it 'raises an exception' do
          expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error(
            JWT::DecodeError, /No keys found in jwks/
          )
        end
      end

      context 'token does not know the kid' do
        let(:token_headers) { {} }
        it 'raises an exception' do
          expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error(
            JWT::DecodeError, 'No key id (kid) or x5t found from token headers'
          )
        end
      end
    end

    context 'when jwk keys are loaded using a proc/lambda' do
      it 'decodes the token' do
        payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: ->(_opts) { public_jwks } })
        expect(payload).to eq(token_payload)
      end
    end

    context 'when jwk keys are rotated' do
      it 'decodes the token' do
        key_loader = ->(options) { options[:invalidate] ? public_jwks : { keys: [] } }
        payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader })
        expect(payload).to eq(token_payload)
      end
    end

    context 'when jwk keys are loaded from JSON with string keys' do
      it 'decodes the token' do
        key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) }
        payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader })
        expect(payload).to eq(token_payload)
      end
    end

    context 'when the token kid is nil' do
      let(:token_headers) { {} }
      context 'and allow_nil_kid is specified' do
        it 'decodes the token' do
          key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) }
          payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: key_loader, allow_nil_kid: true })
          expect(payload).to eq(token_payload)
        end
      end
    end

    context 'when the token kid is not a string' do
      let(:token_headers) { { kid: 5 } }
      it 'raises an exception' do
        expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) }.to raise_error(
          JWT::DecodeError, 'Invalid type for kid header parameter'
        )
      end
    end

    context 'mixing algorithms using kid header' do
      let(:hmac_jwk)           { JWT::JWK.new('secret') }
      let(:rsa_jwk)            { JWT::JWK.new(test_pkey('rsa-2048-private.pem')) }
      let(:ec_jwk_secp384r1)   { JWT::JWK.new(test_pkey('ec384-private.pem')) }
      let(:ec_jwk_secp521r1)   { JWT::JWK.new(test_pkey('ec384-private.pem')) }
      let(:jwks)               { { keys: [hmac_jwk.export(include_private: true), rsa_jwk.export, ec_jwk_secp384r1.export, ec_jwk_secp521r1.export] } }

      context 'when RSA key is pointed to as HMAC secret' do
        let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: rsa_jwk.kid }) }

        it 'raises JWT::DecodeError' do
          expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
        end
      end

      context 'when EC key is pointed to as HMAC secret' do
        let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, 'is not really relevant in the scenario', 'HS256', { kid: ec_jwk_secp384r1.kid }) }

        it 'raises JWT::DecodeError' do
          expect { described_class.decode(signed_token, nil, true, algorithms: ['HS256'], jwks: jwks) }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
        end
      end

      context 'when EC key is pointed to as RSA public key' do
        let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: ec_jwk_secp384r1.kid }) }

        it 'fails in some way' do
          expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to(
            raise_error(JWT::VerificationError, 'Signature verification raised')
          )
        end
      end

      context 'when HMAC secret is pointed to as RSA public key' do
        let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: hmac_jwk.kid }) }

        it 'fails in some way' do
          expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to(
            raise_error(NoMethodError, /undefined method .*verify/)
          )
        end
      end

      context 'when HMAC secret is pointed to as EC public key' do
        let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES384', { kid: hmac_jwk.kid }) }

        it 'fails in some way' do
          expect { described_class.decode(signed_token, nil, true, algorithms: ['ES384'], jwks: jwks) }.to(
            raise_error(JWT::DecodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance')
          )
        end
      end

      context 'when ES384 key is pointed to as ES512 key' do
        let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES512', { kid: ec_jwk_secp521r1.kid }) }

        it 'fails in some way' do
          expect { described_class.decode(signed_token, nil, true, algorithms: ['ES512'], jwks: jwks) }.to(
            raise_error(JWT::IncorrectAlgorithm, 'payload algorithm is ES512 but ES384 signing key was provided')
          )
        end
      end
    end
  end
end
