# frozen_string_literal: true

require 'jwe/enc/a128cbc_hs256'
require 'jwe/enc/a192cbc_hs384'
require 'jwe/enc/a256cbc_hs512'
require 'jwe/enc/a128gcm'
require 'jwe/enc/a192gcm'
require 'jwe/enc/a256gcm'

describe JWE::Enc do
  describe '.for' do
    it 'returns an instance for the specified enc' do
      expect(JWE::Enc.for('A128GCM')).to be_a JWE::Enc::A128gcm
    end

    it 'raises an error for a not-implemented enc' do
      expect { JWE::Enc.for('ERSA-4096-MAGIC') }.to raise_error(JWE::NotImplementedError)
    end
  end
end

gcm = [
  {
    class: JWE::Enc::A128gcm,
    keylen: 16,
    helloworld: "\"\xC6\xE4h\x8AI\x83\x90v\xAF\xE2\x11".b,
    tag: "\x85|\xF7\xE1\x94\tVG\x84\xE1\xA8\x81\a\xF4\xC60".b,
    ivlen: 12,
    iv: "\x0" * 12
  },
  {
    class: JWE::Enc::A192gcm,
    keylen: 24,
    helloworld: "\x9F\xA4\xEC\xCCa\x86\tRO\xD7\xE3\x8D".b,
    tag: "\xF6\xC0\xB8\x91A\xB1\xF0}\xD4u\xD0_\xCD\xA7\x17'".b,
    ivlen: 12,
    iv: "\x0" * 12
  },
  {
    class: JWE::Enc::A256gcm,
    keylen: 32,
    helloworld: "\xFDq\xDC\xDD\x87\x9DK\x97\x03G\x99\f".b,
    tag: "\xC6\xF1\r\xDD\x14\x7Fqf,6\x0EK\x7F\x9D\x1D\t".b,
    ivlen: 12,
    iv: "\x0" * 12
  },
  {
    class: JWE::Enc::A128cbcHs256,
    keylen: 32,
    helloworld: "\a\x02F\xA4m%\xDFH\xB4\xA4.\xBF:\xBF$\xE2".b,
    tag: "\xDE$t\xBA\x8B\xEE\u001Df\x81\a\xC1\xBB\x98\xDFl\xF2".b,
    ivlen: 16,
    iv: "\x0" * 16
  },
  {
    class: JWE::Enc::A192cbcHs384,
    keylen: 48,
    helloworld: "p\xFES\xF0\xB4\xCC]8\x1D\xDE\x8Dt\xE7tMh".b,
    tag: "\xA8a\x04kRJ\x06`tp6\x8E\x9Ba\xE1e\xF6\xDA\"\x15\xEBk\xFDm".b,
    ivlen: 16,
    iv: "\x0" * 16
  },
  {
    class: JWE::Enc::A256cbcHs512,
    keylen: 64,
    helloworld: "c\xFD\\\xB9Z\xB6\xE3\xB7\xEE\xA1\xD8\xDF\xB5\xB2\xF8\xEB".b,
    tag: "wC\xE3:\x91\x89W\x97\xBE\xB0\xBD\xEAo\xC66\x9F\xB82\xFDn\xA7.\u0014l\xFC2\xD7\xDFq\xB5[\xC6".b,
    ivlen: 16,
    iv: "\x0" * 16
  }
]

gcm.each do |group|
  describe group[:class] do
    let(:klass) { group[:class] }
    let(:key) { 'a' * group[:keylen] }
    let(:plaintext) { 'hello world!' }

    describe '#encrypt' do
      context 'when an invalid key is used' do
        it 'raises an error' do
          enc = klass.new('small')
          expect { enc.encrypt('plain', 'auth') }.to raise_error(JWE::BadCEK)
        end
      end

      context 'with a valid key' do
        it 'returns the encrypted payload' do
          enc = klass.new(key, group[:iv])
          expect(enc.encrypt(plaintext, '').b).to eq group[:helloworld]
        end

        it 'sets an authentication tag' do
          enc = klass.new(key, group[:iv])
          enc.encrypt(plaintext, '')
          expect(enc.tag).to eq group[:tag]
        end
      end
    end

    describe '#decrypt' do
      context 'when an invalid key is used' do
        it 'raises an error' do
          enc = klass.new('small')
          expect { enc.decrypt('plain', 'auth') }.to raise_error(JWE::BadCEK)
        end
      end

      context 'with a valid key' do
        context 'when a valid tag is authenticated' do
          it 'returns the plaintext' do
            enc = klass.new(key, group[:iv])
            enc.tag = group[:tag]
            expect(enc.decrypt(group[:helloworld], '')).to eq plaintext
          end
        end

        context 'when the tag is not valid' do
          it 'raises an error' do
            enc = klass.new(key, group[:iv])
            enc.tag = 'random'
            expect { enc.decrypt(group[:helloworld], '') }.to raise_error(JWE::InvalidData)
          end
        end

        context 'when the tag is not set' do
          it 'raises an error' do
            enc = klass.new(key, group[:iv])
            expect { enc.decrypt(group[:helloworld], '') }.to raise_error(JWE::InvalidData)
          end
        end

        context 'when the tag is not 16 bytes' do
          it 'raises an error' do
            enc = klass.new(key, group[:iv])
            enc.tag = group[:tag][0...-1]
            expect { enc.decrypt(group[:helloworld], '') }.to raise_error(JWE::InvalidData)
          end
        end

        context 'when the ciphertext is not valid' do
          it 'raises an error' do
            enc = klass.new(key, group[:iv])
            enc.tag = group[:tag]
            expect { enc.decrypt('random', '') }.to raise_error(JWE::InvalidData)
          end
        end
      end
    end

    describe '#cipher' do
      context 'when the cipher is not supported by the OpenSSL lib' do
        it 'raises an error' do
          enc = klass.new
          allow(enc).to receive(:cipher_name) { 'bad-cipher-128' }
          expect { enc.cipher }.to raise_error(JWE::NotImplementedError)
        end
      end

      context 'when the cipher is supported' do
        it 'returns the cipher object' do
          enc = klass.new
          allow(enc).to receive(:cipher_name) { OpenSSL::Cipher.ciphers.first }
          expect(enc.cipher).to be_an OpenSSL::Cipher
        end
      end
    end

    describe '#cek' do
      context 'when a key is not specified in initialization' do
        it "returns a randomly generated #{group[:keylen]}-bytes key" do
          expect(klass.new.cek.length).to eq group[:keylen]
        end
      end

      context 'when a cek is given' do
        it 'returns the cek' do
          expect(klass.new('cek').cek).to eq 'cek'
        end
      end
    end

    describe '#iv' do
      context 'when an iv is not specified in initialization' do
        it "returns a randomly generated #{group[:ivlen]}-bytes iv" do
          expect(klass.new.iv.length).to eq group[:ivlen]
        end
      end

      context 'when a iv is given' do
        it 'returns the iv' do
          expect(klass.new('cek', 'iv').iv).to eq 'iv'
        end
      end
    end

    describe '.available?' do
      context 'when the cipher is not available' do
        it 'is false' do
          allow_any_instance_of(klass).to receive(:cipher) { raise JWE::NotImplementedError.new }
          expect(klass.available?).to be_falsey
        end
      end

      context 'when the cipher is available' do
        it 'is true' do
          allow_any_instance_of(klass).to receive(:cipher)
          expect(klass.available?).to be_truthy
        end
      end
    end

    describe 'full roundtrip' do
      it 'decrypts the ciphertext to the original plaintext' do
        enc = klass.new
        ciphertext = enc.encrypt(plaintext, '')
        expect(enc.decrypt(ciphertext, '')).to eq plaintext
      end
    end
  end
end
