# frozen_string_literal: true

require 'spec_helper'

describe Prawn::Text do
  describe 'NBSP' do
    it 'is defined' do
      expect(Prawn::Text::NBSP).to eq("\u00a0")
    end
  end

  describe '#height_of' do
    let(:pdf) { create_pdf }

    it 'returns the height that would be required to print a particular string of text' do
      original_y = pdf.y
      pdf.text('Foo')
      new_y = pdf.y
      expect(pdf.height_of('Foo')).to be_within(0.0001).of(original_y - new_y)
    end

    it 'omits the gap below the last descender if :final_gap => false is given' do
      original_y = pdf.y
      pdf.text('Foo', final_gap: false)
      new_y = pdf.y
      expect(pdf.height_of('Foo', final_gap: false))
        .to be_within(0.0001).of(original_y - new_y)
    end

    it 'raise_errors CannotFit if a too-small width is given' do
      expect {
        pdf.height_of('text', width: 1)
      }.to raise_error(Prawn::Errors::CannotFit)
    end

    it 'raises NotImplementedError if :indent_paragraphs option is provided' do
      expect {
        pdf.height_of(
          'hai',
          width: 300,
          indent_paragraphs: 60,
        )
      }.to raise_error(NotImplementedError)
    end

    it 'does not raise Prawn::Errors::UnknownOption if :final_gap option is provided' do
      expect {
        pdf.height_of('hai', width: 300, final_gap: true)
      }.to_not raise_error
    end
  end

  describe '#text' do
    let(:pdf) { create_pdf }

    it 'does not fail when @output is nil when Prawn::Text::Formatted::LineWrap#finalize_line is called' do
      # need a document with margins for these particulars to produce the
      # condition that was throwing the error
      pdf = Prawn::Document.new
      expect { pdf.text('transparency ' * 150, size: 18) }
        .to_not raise_error
    end

    it 'allows drawing empty strings to the page' do
      pdf.text(' ')
      text = PDF::Inspector::Text.analyze(pdf.render)
      # If anything is rendered to the page, it should be whitespace.
      expect(text.strings).to all(match(/\A\s*\z/))
    end

    it 'ignores call when string is nil' do
      expect(pdf.text(nil)).to be false
    end

    it 'correctlies render empty paragraphs' do
      pdf.text("text\n\ntext")
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(pdf.page_count).to eq(1)
      expect(text.strings.reject(&:empty?)).to eq(%w[text text])
    end

    it 'correctlies render empty paragraphs with :indent_paragraphs' do
      pdf.text("text\n\ntext", indent_paragraphs: 5)
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(pdf.page_count).to eq(1)
      expect(text.strings.reject(&:empty?)).to eq(%w[text text])
    end

    it 'correctly renders strings ending with empty paragraphs and :inline_format and :indent_paragraphs' do
      pdf.text("text\n\n", inline_format: true, indent_paragraphs: 5)
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(pdf.page_count).to eq(1)
      expect(text.strings).to eq(['text'])
    end

    it 'defaults to use kerning information' do
      pdf.text('hello world')
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.kerned[0]).to be true
    end

    it 'is able to disable kerning with an option' do
      pdf.text('hello world', kerning: false)
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.kerned[0]).to be false
    end

    it 'is able to disable kerning document-wide' do
      pdf.default_kerning(false)
      pdf.default_kerning = false
      pdf.text('hello world')
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.kerned[0]).to be false
    end

    it 'option should be able to override document-wide kerning disabling' do
      pdf.default_kerning = false
      pdf.text('hello world', kerning: true)
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.kerned[0]).to be true
    end

    it 'raise_errors ArgumentError if :at option included' do
      expect { pdf.text('hai', at: [0, 0]) }.to raise_error(ArgumentError)
    end

    it 'advances down the document based on font_height' do
      position = pdf.y
      pdf.text('Foo')

      expect(pdf.y).to be_within(0.0001).of(position - pdf.font.height)

      position = pdf.y
      pdf.text("Foo\nBar\nBaz")
      expect(pdf.y).to be_within(0.0001).of(position - (3 * pdf.font.height))
    end

    it 'advances down the document based on font_height with size option' do
      position = pdf.y
      pdf.text('Foo', size: 15)

      pdf.font_size = 15
      expect(pdf.y).to be_within(0.0001).of(position - pdf.font.height)

      position = pdf.y
      pdf.text("Foo\nBar\nBaz")
      expect(pdf.y).to be_within(0.0001).of(position - (3 * pdf.font.height))
    end

    it 'advances down the document based on font_height with leading option' do
      position = pdf.y
      leading = 2
      pdf.text('Foo', leading: leading)

      expect(pdf.y).to be_within(0.0001)
        .of(position - pdf.font.height - leading)

      position = pdf.y
      pdf.text("Foo\nBar\nBaz")
      expect(pdf.y).to be_within(0.0001).of(position - (3 * pdf.font.height))
    end

    it 'advances only to the bottom of the final descender if final_gap is false' do
      position = pdf.y
      pdf.text('Foo', final_gap: false)

      expect(pdf.y).to be_within(0.0001).of(position - pdf.font.ascender - pdf.font.descender)

      position = pdf.y
      pdf.text("Foo\nBar\nBaz", final_gap: false)
      expect(pdf.y).to be_within(0.0001)
        .of(position - (2 * pdf.font.height) - pdf.font.ascender - pdf.font.descender)
    end

    it 'is able to print text starting at the last line of a page' do
      pdf.move_cursor_to(pdf.font.height)
      pdf.text('hello world')
      pages = PDF::Inspector::Page.analyze(pdf.render).pages
      expect(pages.size).to eq(1)
    end

    it 'defaults to 12 point helvetica' do
      pdf.text('Blah')
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings[0][:name]).to eq(:Helvetica)
      expect(text.font_settings[0][:size]).to eq(12)
      expect(text.strings.first).to eq('Blah')
    end

    it 'allows setting font size' do
      pdf.text('Blah', size: 16)
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings[0][:size]).to eq(16)
    end

    it 'allows setting a default font size' do
      pdf.font_size = 16
      pdf.text('Blah')
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings[0][:size]).to eq(16)
    end

    it 'allows overriding default font for a single instance' do
      pdf.font_size = 16

      pdf.text('Blah', size: 11)
      pdf.text('Blaz')
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings[0][:size]).to eq(11)
      expect(text.font_settings[1][:size]).to eq(16)
    end

    it 'allows setting a font size transaction with a block' do
      pdf.font_size(16) do
        pdf.text('Blah')
      end

      pdf.text('blah')

      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings[0][:size]).to eq(16)
      expect(text.font_settings[1][:size]).to eq(12)
    end

    it 'allows manual setting the font size when in a font size block' do
      pdf.font_size(16) do
        pdf.text('Foo')
        pdf.text('Blah', size: 11)
        pdf.text('Blaz')
      end
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings[0][:size]).to eq(16)
      expect(text.font_settings[1][:size]).to eq(11)
      expect(text.font_settings[2][:size]).to eq(16)
    end

    it 'allows registering of built-in font_settings on the fly' do
      pdf.font('Times-Roman')
      pdf.text('Blah')
      pdf.font('Courier')
      pdf.text('Blaz')
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings[0][:name]).to eq(:'Times-Roman')
      expect(text.font_settings[1][:name]).to eq(:Courier)
    end

    it 'utilises the same default font across multiple pages' do
      pdf.text('Blah')
      pdf.start_new_page
      pdf.text('Blaz')
      text = PDF::Inspector::Text.analyze(pdf.render)

      expect(text.font_settings.size).to eq(2)
      expect(text.font_settings[0][:name]).to eq(:Helvetica)
      expect(text.font_settings[1][:name]).to eq(:Helvetica)
    end

    it 'raise_errors an exception when an unknown font is used' do
      expect { pdf.font('Pao bu') }.to raise_error(Prawn::Errors::UnknownFont)
    end

    it 'does not raise an exception when providing Pathname instance as font' do
      expect { pdf.font(Pathname.new("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")) }
        .to_not raise_error
    end

    it 'correctlies render a utf-8 string when using a built-in font' do
      str = '©' # copyright symbol
      pdf.text(str)

      # grab the text from the rendered PDF and ensure it matches
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.strings.first).to eq(str)
    end

    it 'correctlies render a utf-8 string when using a TTF font' do
      str = '©' # copyright symbol
      pdf.font("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")
      pdf.text(str)

      # grab the text from the rendered PDF and ensure it matches
      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.strings.first).to eq(str)
    end

    it 'subsets mixed low-ASCII and non-ASCII characters when they can be subsetted together' do
      str = 'It’s super effective!'
      pdf.font("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")
      pdf.text(str)

      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.strings.first).to eq(str)
    end

    it 'correctly renders a string with higher bit characters across a page break when using a built-in font' do
      str = '©'
      pdf.move_cursor_to(pdf.font.height)
      pdf.text("#{str}\n#{str}")

      pages = PDF::Inspector::Page.analyze(pdf.render).pages
      expect(pages.size).to eq(2)
      expect(pages[0][:strings]).to eq([str])
      expect(pages[1][:strings]).to eq([str])
    end

    it 'correctly renders a string with higher bit characters across a page break when using a built-in font and ' \
      ':indent_paragraphs option' do
      str = '©'
      pdf.move_cursor_to(pdf.font.height)
      pdf.text("#{str}\n#{str}", indent_paragraphs: 20)

      pages = PDF::Inspector::Page.analyze(pdf.render).pages
      expect(pages.size).to eq(2)
      expect(pages[0][:strings]).to eq([str])
      expect(pages[1][:strings]).to eq([str])
    end

    it 'raises an exception when a utf-8 incompatible string is rendered' do
      str = "Blah \xDD"
      expect { pdf.text(str) }.to raise_error(Prawn::Errors::IncompatibleStringEncoding)
    end

    it 'does not raise an exception when a shift-jis string is rendered' do
      datafile = "#{Prawn::DATADIR}/shift_jis_text.txt"
      sjis_str = File.open(datafile, 'r:shift_jis', &:gets)
      pdf.font("#{Prawn::DATADIR}/fonts/gkai00mp.ttf")

      # Expect that the call to text will not raise an encoding error
      expect { pdf.text(sjis_str) }.to_not raise_error
    end

    it 'calls move_past_bottom when printing more text than can fit between the current document.y and bounds.bottom' do
      pdf.y = pdf.font.height
      pdf.text('Hello')
      pdf.text('World')
      pages = PDF::Inspector::Page.analyze(pdf.render).pages
      expect(pages.size).to eq(2)
      expect(pages[0][:strings]).to eq(['Hello'])
      expect(pages[1][:strings]).to eq(['World'])
    end

    describe 'with :indent_paragraphs option' do
      it 'indents the paragraphs' do
        hello = 'hello ' * 50
        hello2 = 'hello ' * 50
        pdf.text("#{hello}\n#{hello2}", indent_paragraphs: 60)
        text = PDF::Inspector::Text.analyze(pdf.render)
        expect(text.strings[0]).to eq(('hello ' * 19).strip)
        expect(text.strings[1]).to eq(('hello ' * 21).strip)
        expect(text.strings[3]).to eq(('hello ' * 19).strip)
        expect(text.strings[4]).to eq(('hello ' * 21).strip)
      end

      it 'indents from right side when using :rtl direction' do
        para1 = 'The rain in spain falls mainly on the plains ' * 3
        para2 = 'The rain in spain falls mainly on the plains ' * 3

        pdf.text("#{para1}\n#{para2}", indent_paragraphs: 60, direction: :rtl)

        text = PDF::Inspector::Text.analyze(pdf.render)

        lines = text.strings
        x_positions = text.positions.map { |e| e[0] }

        # NOTE: The code below reflects Prawn's current kerning behavior for RTL
        # text, which isn't necessarily correct. If we change that behavior,
        # this test will need to be updated.

        expect(x_positions[0]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - 60 - pdf.width_of(lines[0].reverse, kerning: true),
          ),
        )

        expect(x_positions[1]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - pdf.width_of(lines[1].reverse, kerning: true),
          ),
        )

        expect(x_positions[2]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - 60 - pdf.width_of(lines[2].reverse, kerning: true),
          ),
        )

        expect(x_positions[3]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - pdf.width_of(lines[3].reverse, kerning: true),
          ),
        )
      end

      it 'indents from right side when document has :rtl direction' do
        para1 = 'The rain in spain falls mainly on the plains ' * 3
        para2 = 'The rain in spain falls mainly on the plains ' * 3

        pdf.text_direction = :rtl
        pdf.text("#{para1}\n#{para2}", indent_paragraphs: 60)

        text = PDF::Inspector::Text.analyze(pdf.render)

        lines = text.strings
        x_positions = text.positions.map { |e| e[0] }

        # NOTE: The code below reflects Prawn's current kerning behavior for RTL
        # text, which isn't necessarily correct. If we change that behavior,
        # this test will need to be updated.

        expect(x_positions[0]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - 60 - pdf.width_of(lines[0].reverse, kerning: true),
          ),
        )

        expect(x_positions[1]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - pdf.width_of(lines[1].reverse, kerning: true),
          ),
        )

        expect(x_positions[2]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - 60 - pdf.width_of(lines[2].reverse, kerning: true),
          ),
        )

        expect(x_positions[3]).to(
          be_within(0.001).of(
            pdf.bounds.absolute_right - pdf.width_of(lines[3].reverse, kerning: true),
          ),
        )
      end

      it 'indents from left side when using :ltr direction' do
        para1 = 'The rain in spain falls mainly on the plains ' * 3
        para2 = 'The rain in spain falls mainly on the plains ' * 3

        pdf.text("#{para1}\n#{para2}", indent_paragraphs: 60, direction: :ltr)

        text = PDF::Inspector::Text.analyze(pdf.render)

        x_positions = text.positions.map { |e| e[0] }

        expect(x_positions[0]).to eq(60)
        expect(x_positions[1]).to eq(0)

        expect(x_positions[2]).to eq(60)
        expect(x_positions[3]).to eq(0)
      end

      it 'indents from left side when document has :ltr direction' do
        para1 = 'The rain in spain falls mainly on the plains ' * 3
        para2 = 'The rain in spain falls mainly on the plains ' * 3

        pdf.text_direction = :ltr
        pdf.text("#{para1}\n#{para2}", indent_paragraphs: 60)

        text = PDF::Inspector::Text.analyze(pdf.render)

        x_positions = text.positions.map { |e| e[0] }

        expect(x_positions[0]).to eq(60)
        expect(x_positions[1]).to eq(0)

        expect(x_positions[2]).to eq(60)
        expect(x_positions[3]).to eq(0)
      end

      describe 'when paragraph has only one line, it should not add additional leading' do
        let(:leading) { 100 }

        it 'adds leading only once' do
          original_y = pdf.y
          pdf.text('hello', indent_paragraphs: 10, leading: leading)
          expect(original_y - pdf.y).to be < leading * 2
        end
      end

      describe 'when :final_gap is set to false, there should still be a gap between the first line and the rest' do
        it 'spaces the first and the second line correctly' do
          text = 'hello ' * 30
          original_y = pdf.y
          # First rendering with no indentation so we know what the correct
          # render height should be
          pdf.text(text, final_gap: false)
          height_with_no_indentation = original_y - pdf.y
          y_between_paragraphs = pdf.y
          # The indentation size doesn't matter, it's about triggering
          # a different path in the code
          pdf.text(text, indent_paragraphs: 1, final_gap: false)
          height_with_indentation = y_between_paragraphs - pdf.y
          expect(height_with_indentation).to be_within(0.0001)
            .of(height_with_no_indentation)
        end
      end

      describe 'single line height with no final_gap should not depend on indentation' do
        it 'does not affect the height of a single line' do
          text = 'hello'
          original_y = pdf.y
          # First rendering with no indentation so we know what the correct
          # render height should be
          pdf.text(text, final_gap: false)
          height_with_no_indentation = original_y - pdf.y
          y_between_paragraphs = pdf.y
          # The indentation size doesn't matter, it's about triggering
          # a different path in the code
          pdf.text(text, indent_paragraphs: 1, final_gap: false)
          height_with_indentation = y_between_paragraphs - pdf.y
          expect(height_with_indentation).to be_within(0.0001)
            .of(height_with_no_indentation)
        end
      end

      describe 'when wrap to new page, and first line of new page is not the start of a new paragraph, that line ' \
        'should not be indented' do
        it 'indents the paragraphs' do
          hello = 'hello ' * 50
          hello2 = 'hello ' * 50
          pdf.move_cursor_to(pdf.font.height)
          pdf.text("#{hello}\n#{hello2}", indent_paragraphs: 60)
          text = PDF::Inspector::Text.analyze(pdf.render)
          expect(text.strings[0]).to eq(('hello ' * 19).strip)
          expect(text.strings[1]).to eq(('hello ' * 21).strip)
          expect(text.strings[3]).to eq(('hello ' * 19).strip)
          expect(text.strings[4]).to eq(('hello ' * 21).strip)
        end
      end

      describe 'when wrap to new page, and first line of new page is the start of a new paragraph, that line should ' \
        'be indented' do
        it 'indents the paragraphs' do
          hello = 'hello ' * 50
          hello2 = 'hello ' * 50
          pdf.move_cursor_to(pdf.font.height * 3)
          pdf.text("#{hello}\n#{hello2}", indent_paragraphs: 60)
          text = PDF::Inspector::Text.analyze(pdf.render)
          expect(text.strings[0]).to eq(('hello ' * 19).strip)
          expect(text.strings[1]).to eq(('hello ' * 21).strip)
          expect(text.strings[3]).to eq(('hello ' * 19).strip)
          expect(text.strings[4]).to eq(('hello ' * 21).strip)
        end
      end
    end

    describe 'kerning' do
      it 'respects text kerning setting (document default)' do
        allow(pdf.font).to receive(:compute_width_of)
          .with('VAT', hash_including(kerning: true))
          .and_return(10)

        pdf.text('VAT')

        expect(pdf.font).to have_received(:compute_width_of)
          .with('VAT', hash_including(kerning: true))
      end

      it 'respects text kerning setting (kerning=true)' do
        allow(pdf.font).to receive(:compute_width_of)
          .with('VAT', hash_including(kerning: true))
          .at_least(:once)
          .and_return(10)
        pdf.text('VAT', kerning: true)

        expect(pdf.font).to have_received(:compute_width_of)
          .with('VAT', hash_including(kerning: true))
          .at_least(:once)
      end

      it 'respects text kerning setting (kerning=false)' do
        allow(pdf.font).to receive(:compute_width_of)
          .with('VAT', hash_including(kerning: false))
          .at_least(:once)
          .and_return(10)
        pdf.text('VAT', kerning: false)

        expect(pdf.font).to have_received(:compute_width_of)
          .with('VAT', hash_including(kerning: false))
          .at_least(:once)
      end
    end

    describe '#shrink_to_fit with special utf-8 text' do
      it 'does not throw an exception', :unresolved, issue: 603 do
        expect {
          Prawn::Document.new(page_size: 'A4', margin: [2, 2, 2, 2]) do |pdf|
            add_unicode_fonts(pdf)
            pdf.bounding_box([1, 1], width: 90, height: 50) do
              pdf.text(
                "Sample Text\nSAMPLE SAMPLE SAMPLEoddělení ZMĚN\nSAMPLE",
                overflow: :shrink_to_fit,
              )
            end
          end
        }.to_not raise_error
      end
    end

    def add_unicode_fonts(pdf)
      dejavu = "#{::Prawn::BASEDIR}/data/fonts/DejaVuSans.ttf"
      pdf.font_families.update(
        'dejavu' => {
          normal: dejavu,
          italic: dejavu,
          bold: dejavu,
          bold_italic: dejavu,
        },
      )
      pdf.fallback_fonts = ['dejavu']
    end

    describe 'fallback_fonts' do
      it 'preserves font style' do
        create_pdf

        pdf.fallback_fonts(['Helvetica'])
        pdf.font('Times-Roman', style: :italic) do
          pdf.text('hello')
        end

        text = PDF::Inspector::Text.analyze(pdf.render)
        fonts_used = text.font_settings.map { |e| e[:name] }

        expect(fonts_used.length).to eq(1)
        expect(fonts_used[0]).to eq(:'Times-Italic')
        expect(text.strings[0]).to eq('hello')
      end
    end
  end
end
