# frozen_string_literal: true

require 'spec_helper'
require 'tempfile'

describe Prawn::Document::Security do
  describe 'Password padding' do
    include described_class

    it 'truncates long passwords' do
      pw = 'Long long string' * 30
      padded = pad_password(pw)
      expect(padded.length).to eq(32)
      expect(padded).to eq(pw[0, 32])
    end

    it 'pads short passwords' do
      pw = 'abcd'
      padded = pad_password(pw)
      expect(padded.length).to eq(32)
      expect(padded).to eq(pw + Prawn::Document::Security::PASSWORD_PADDING[0, 28])
    end

    it 'fullies pad null passwords' do
      pw = ''
      padded = pad_password(pw)
      expect(padded.length).to eq(32)
      expect(padded).to eq(Prawn::Document::Security::PASSWORD_PADDING)
    end
  end

  describe 'Setting permissions' do
    def doc_with_permissions(permissions)
      pdf = Prawn::Document.new

      # Make things easier to test
      pdf.singleton_class.__send__(:public, :permissions_value)
      # class << pdf
      #   public :permissions_value
      # end

      pdf.encrypt_document(permissions: permissions)
      pdf
    end

    it 'defaults to full permissions' do
      expect(doc_with_permissions({}).permissions_value).to eq(0xFFFFFFFF)
      expect(
        doc_with_permissions(
          print_document: true,
          modify_contents: true,
          copy_contents: true,
          modify_annotations: true,
        ).permissions_value,
      )
        .to eq(0xFFFFFFFF)
    end

    it 'clears the appropriate bits for each permission flag' do
      expect(doc_with_permissions(print_document: false).permissions_value)
        .to eq(0b1111_1111_1111_1111_1111_1111_1111_1011)
      expect(doc_with_permissions(modify_contents: false).permissions_value)
        .to eq(0b1111_1111_1111_1111_1111_1111_1111_0111)
      expect(doc_with_permissions(copy_contents: false).permissions_value)
        .to eq(0b1111_1111_1111_1111_1111_1111_1110_1111)
      expect(doc_with_permissions(modify_annotations: false).permissions_value)
        .to eq(0b1111_1111_1111_1111_1111_1111_1101_1111)
    end

    it 'raise_errors ArgumentError if invalid option is provided' do
      expect {
        doc_with_permissions(modify_document: false)
      }.to raise_error(ArgumentError)
    end
  end

  describe 'Encryption keys' do
    # Since PDF::Reader doesn't read encrypted PDF files, we just take the
    # roundabout method of verifying each step of the encryption. This works
    # fine because the encryption method is deterministic.

    let(:pdf) do
      Prawn::Document.new do |pdf|
        class << pdf
          public :owner_password_hash, :user_password_hash, :user_encryption_key
        end
        pdf.encrypt_document(
          user_password: 'foo',
          owner_password: 'bar',
          permissions: { print_document: false },
        )
      end
    end

    it 'calculates the correct owner hash' do
      expect(pdf.owner_password_hash.unpack1('H*'))
        .to match(/^61CA855012/i)
    end

    it 'calculates the correct user hash' do
      expect(pdf.user_password_hash.unpack1('H*'))
        .to match(/^6BC8C51031/i)
    end

    it 'calculates the correct user_encryption_key' do
      expect(pdf.user_encryption_key.unpack1('H*').upcase)
        .to eq('B100AB6429')
    end
  end

  describe 'encrypted_pdf_object' do
    it 'delegates to PdfObject for simple types' do
      expect(PDF::Core.encrypted_pdf_object(true, nil, nil, nil)).to eq('true')
      expect(PDF::Core.encrypted_pdf_object(42, nil, nil, nil)).to eq('42')
    end

    it 'encrypts strings properly' do
      expect(PDF::Core.encrypted_pdf_object('foo', '12345', 123, 0))
        .to eq('<4ad6e3>')
    end

    it 'encrypts literal strings properly' do
      expect(
        PDF::Core.encrypted_pdf_object(
          PDF::Core::LiteralString.new('foo'), '12345', 123, 0,
        ),
      ).to eq(bin_string("(J\xD6\xE3)"))

      expect(
        PDF::Core.encrypted_pdf_object(
          PDF::Core::LiteralString.new("\xAF\xC5fh\x9A\x14\x97,\xD3,\x06\x87\xCDSS"), nil, 123, 0,
        ),
      ).to eq(
        bin_string("(2&\\(\x02P\x92\x9C\e\xAF\\)\\r\x83\x94\x11\x0F)"),
      )
    end

    it 'encrypts time properly' do
      expect(
        PDF::Core.encrypted_pdf_object(
          Time.utc(10_002, 0o4, 26, 10, 17, 10), '12345', 123, 0,
        ),
      ).to eq bin_string("(h\x83\xBD\xDC\xE9\x99\\r\xD3/!\x14\xD5%\xBE\xF6\x17\xA3\x9B\xC5\xFE&+\xD8\x93)")
    end

    it 'properlies handle compound types' do
      expect(PDF::Core.encrypted_pdf_object({ Bar: 'foo' }, '12345', 123, 0))
        .to eq("<< /Bar <4ad6e3>\n>>")
      expect(PDF::Core.encrypted_pdf_object(%w[foo bar], '12345', 123, 0))
        .to eq('[<4ad6e3> <4ed8fe>]')
    end
  end

  describe 'Reference#encrypted_object' do
    it 'encrypts references properly' do
      ref = PDF::Core::Reference.new(1, ['foo'])
      expect(ref.encrypted_object(nil)).to eq("1 0 obj\n[<4fca3f>]\nendobj\n")
    end

    it 'encrypts references with streams properly' do
      ref = PDF::Core::Reference.new(1, {})
      ref << 'foo'
      result = bin_string("1 0 obj\n<< /Length 3\n>>\nstream\nO\xCA?\nendstream\nendobj\n")
      expect(ref.encrypted_object(nil)).to eq(result)
    end
  end

  describe 'String#encrypted_object' do
    it 'encrypts stream properly' do
      stream = PDF::Core::Stream.new
      stream << 'foo'
      result = bin_string("stream\nO\xCA?\nendstream\n")
      expect(stream.encrypted_object(nil, 1, 0)).to eq(result)
    end
  end
end
