# encoding: utf-8
require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")

describe "Outline" do
  before(:each) do
    @pdf = Prawn::Document.new do
      text "Page 1. This is the first Chapter. "
      start_new_page
      text "Page 2. More in the first Chapter. "
      start_new_page
      outline.define do
        section 'Chapter 1', :destination => 1, :closed => true do
          page :destination => 1, :title => 'Page 1'
          page :destination => 2, :title => 'Page 2'
        end
      end
    end
  end

  describe "outline encoding" do
    it "should store all outline titles as UTF-16" do
      render_and_find_objects
      @hash.values.each do |obj|
        if obj.is_a?(Hash) && obj[:Title]
          title = obj[:Title].dup
          title.force_encoding(Encoding::UTF_16LE)
          expect(title.valid_encoding?).to eq(true)
        end
      end
    end
  end

  describe "#generate_outline" do
    before(:each) do
      render_and_find_objects
    end

    it "should create a root outline dictionary item" do
      expect(@outline_root).not_to be_nil
    end

    it "should set the first and last top items of the root outline dictionary item" do
      expect(referenced_object(@outline_root[:First])).to eq(@section_1)
      expect(referenced_object(@outline_root[:Last])).to eq(@section_1)
    end

    describe "#create_outline_item" do
      it "should create outline items for each section and page" do
        [@section_1, @page_1, @page_2].each { |item| expect(item).not_to be_nil }
      end
    end

    describe "#set_relations, #set_variables_for_block, and #reset_parent" do
      it "should link sibling items" do
        expect(referenced_object(@page_1[:Next])).to eq(@page_2)
        expect(referenced_object(@page_2[:Prev])).to eq(@page_1)
      end

      it "should link child items to parent item" do
        [@page_1, @page_2].each { |page| expect(referenced_object(page[:Parent])).to eq(@section_1) }
      end

      it "should set the first and last child items for parent item" do
        expect(referenced_object(@section_1[:First])).to eq(@page_1)
        expect(referenced_object(@section_1[:Last])).to eq(@page_2)
      end
    end

    describe "#increase_count" do
      it "should add the count of all descendant items" do
        expect(@outline_root[:Count]).to eq(3)
        expect(@section_1[:Count].abs).to eq(2)
        expect(@page_1[:Count]).to eq(0)
        expect(@page_2[:Count]).to eq(0)
      end
    end

    describe "closed option" do
      it "should set the item's integer count to negative" do
        expect(@section_1[:Count]).to eq(-2)
      end
    end
  end

  describe "adding a custom destination" do
    before(:each) do
      @pdf.start_new_page
      @pdf.text "Page 3 with a destination"
      @pdf.add_dest('customdest', @pdf.dest_xyz(200, 200))
      pdf = @pdf
      @pdf.outline.update do
        page :destination => pdf.dest_xyz(200, 200), :title => 'Custom Destination'
      end
      render_and_find_objects
    end

    it "should create an outline item" do
      expect(@custom_dest).not_to be_nil
    end

    it "should reference the custom destination" do
      expect(referenced_object(@custom_dest[:Dest].first)).to eq(referenced_object(@pages.last))
    end
  end

  describe "addding a section later with outline#section" do
    before(:each) do
      @pdf.start_new_page
      @pdf.text "Page 3. An added section "
      @pdf.outline.update do
        section 'Added Section', :destination => 3 do
          page :destination => 3, :title => 'Page 3'
        end
      end
      render_and_find_objects
    end

    it "should add new outline items to document" do
      [@section_2, @page_3].each { |item| expect(item).not_to be_nil }
    end

    it "should reset the last items for root outline dictionary" do
      expect(referenced_object(@outline_root[:First])).to eq(@section_1)
      expect(referenced_object(@outline_root[:Last])).to eq(@section_2)
    end

    it "should reset the next relation for the previous last top level item" do
      expect(referenced_object(@section_1[:Next])).to eq(@section_2)
    end

    it "should set the previous relation of the addded to section" do
      expect(referenced_object(@section_2[:Prev])).to eq(@section_1)
    end

    it "should increase the count of root outline dictionary" do
      expect(@outline_root[:Count]).to eq(5)
    end
  end

  describe "#outline.add_subsection_to" do
    context "positioned last" do
      before(:each) do
        @pdf.start_new_page
        @pdf.text "Page 3. An added subsection "
        @pdf.outline.update do
          add_subsection_to 'Chapter 1' do
            section 'Added SubSection', :destination => 3 do
              page :destination => 3, :title => 'Added Page 3'
            end
          end
        end
        render_and_find_objects
      end

      it "should add new outline items to document" do
        [@subsection, @added_page_3].each { |item| expect(item).not_to be_nil }
      end

      it "should reset the last item for parent item dictionary" do
        expect(referenced_object(@section_1[:First])).to eq(@page_1)
        expect(referenced_object(@section_1[:Last])).to eq(@subsection)
      end

      it "should set the prev relation for the new subsection to its parent's old last item" do
        expect(referenced_object(@subsection[:Prev])).to eq(@page_2)
      end

      it "the subsection should become the next relation for its parent's old last item" do
        expect(referenced_object(@page_2[:Next])).to eq(@subsection)
      end

      it "should set the first relation for the new subsection" do
        expect(referenced_object(@subsection[:First])).to eq(@added_page_3)
      end

      it "should set the correct last relation of the added to section" do
        expect(referenced_object(@subsection[:Last])).to eq(@added_page_3)
      end

      it "should increase the count of root outline dictionary" do
        expect(@outline_root[:Count]).to eq(5)
      end
    end

    context "positioned first" do
      before(:each) do
        @pdf.start_new_page
        @pdf.text "Page 3. An added subsection "
        @pdf.outline.update do
          add_subsection_to 'Chapter 1', :first do
            section 'Added SubSection', :destination => 3 do
              page :destination => 3, :title => 'Added Page 3'
            end
          end
        end
        render_and_find_objects
      end

      it "should add new outline items to document" do
        [@subsection, @added_page_3].each { |item| expect(item).not_to be_nil }
      end

      it "should reset the first item for parent item dictionary" do
        expect(referenced_object(@section_1[:First])).to eq(@subsection)
        expect(referenced_object(@section_1[:Last])).to eq(@page_2)
      end

      it "should set the next relation for the new subsection to its parent's old first item" do
        expect(referenced_object(@subsection[:Next])).to eq(@page_1)
      end

      it "the subsection should become the prev relation for its parent's old first item" do
        expect(referenced_object(@page_1[:Prev])).to eq(@subsection)
      end

      it "should set the first relation for the new subsection" do
        expect(referenced_object(@subsection[:First])).to eq(@added_page_3)
      end

      it "should set the correct last relation of the added to section" do
        expect(referenced_object(@subsection[:Last])).to eq(@added_page_3)
      end

      it "should increase the count of root outline dictionary" do
        expect(@outline_root[:Count]).to eq(5)
      end
    end

    it "should require an existing title" do
      expect do
        @pdf.go_to_page 1
        @pdf.start_new_page
        @pdf.text "Inserted Page"
        @pdf.outline.update do
          add_subsection_to 'Wrong page' do
            page page_number, :title => "Inserted Page"
          end
        end
        render_and_find_objects
      end.to raise_error(Prawn::Errors::UnknownOutlineTitle)
    end
  end

  describe "#outline.insert_section_after" do
    describe "inserting in the middle of another section" do
      before(:each) do
        @pdf.go_to_page 1
        @pdf.start_new_page
        @pdf.text "Inserted Page"
        @pdf.outline.update do
          insert_section_after 'Page 1' do
            page :destination => page_number, :title => "Inserted Page"
          end
        end
      end

      it "should insert new outline items to document" do
        render_and_find_objects
        expect(@inserted_page).not_to be_nil
      end

      it "should adjust the count of all ancestors" do
        render_and_find_objects
        expect(@outline_root[:Count]).to eq(4)
        expect(@section_1[:Count].abs).to eq(3)
      end

      describe "#adjust_relations" do
        it "should reset the sibling relations of adjoining items to inserted item" do
          render_and_find_objects
          expect(referenced_object(@page_1[:Next])).to eq(@inserted_page)
          expect(referenced_object(@page_2[:Prev])).to eq(@inserted_page)
        end

        it "should set the sibling relation of added item to adjoining items" do
          render_and_find_objects
          expect(referenced_object(@inserted_page[:Next])).to eq(@page_2)
          expect(referenced_object(@inserted_page[:Prev])).to eq(@page_1)
        end

        it "should not affect the first and last relations of parent item" do
          render_and_find_objects
          expect(referenced_object(@section_1[:First])).to eq(@page_1)
          expect(referenced_object(@section_1[:Last])).to eq(@page_2)
        end
      end

      context "when adding another section afterwards" do
        it "should have reset the root position so that a new section is added at the end of root sections" do
          @pdf.start_new_page
          @pdf.text "Another Inserted Page"
          @pdf.outline.update do
            section 'Added Section' do
              page :destination => page_number, :title => "Inserted Page"
            end
          end
          render_and_find_objects
          expect(referenced_object(@outline_root[:Last])).to eq(@section_2)
          expect(referenced_object(@section_1[:Next])).to eq(@section_2)
        end
      end
    end

    describe "inserting at the end of another section" do
      before(:each) do
        @pdf.go_to_page 2
        @pdf.start_new_page
        @pdf.text "Inserted Page"
        @pdf.outline.update do
          insert_section_after 'Page 2' do
            page :destination => page_number, :title => "Inserted Page"
          end
        end
        render_and_find_objects
      end

      describe "#adjust_relations" do
        it "should reset the sibling relations of adjoining item to inserted item" do
          expect(referenced_object(@page_2[:Next])).to eq(@inserted_page)
        end

        it "should set the sibling relation of added item to adjoining items" do
          expect(referenced_object(@inserted_page[:Next])).to be_nil
          expect(referenced_object(@inserted_page[:Prev])).to eq(@page_2)
        end

        it "should adjust the last relation of parent item" do
          expect(referenced_object(@section_1[:Last])).to eq(@inserted_page)
        end
      end
    end

    it "should require an existing title" do
      expect do
        @pdf.go_to_page 1
        @pdf.start_new_page
        @pdf.text "Inserted Page"
        @pdf.outline.update do
          insert_section_after 'Wrong page' do
            page :destination => page_number, :title => "Inserted Page"
          end
        end
        render_and_find_objects
      end.to raise_error(Prawn::Errors::UnknownOutlineTitle)
    end
  end

  describe "#page" do
    it "should require a title option to be set" do
      expect do
        @pdf = Prawn::Document.new do
          text "Page 1. This is the first Chapter. "
          outline.define do
            page :destination => 1, :title => nil
          end
        end
      end.to raise_error(Prawn::Errors::RequiredOption)
    end
  end
end

describe "foreign character encoding" do
  before(:each) do
    pdf = Prawn::Document.new do
      outline.define do
        section 'La pomme croquée', :destination => 1, :closed => true
      end
    end
    @hash = PDF::Reader::ObjectHash.new(StringIO.new(pdf.render, 'r+'))
  end

  it "should handle other encodings for the title" do
    object = find_by_title('La pomme croquée')
    expect(object).not_to be_nil
  end
end

def render_and_find_objects
  output = StringIO.new(@pdf.render, 'r+')
  @hash = PDF::Reader::ObjectHash.new(output)
  @outline_root = @hash.values.find { |obj| obj.is_a?(Hash) && obj[:Type] == :Outlines }
  @pages = @hash.values.find { |obj| obj.is_a?(Hash) && obj[:Type] == :Pages }[:Kids]
  @section_1 = find_by_title('Chapter 1')
  @page_1 = find_by_title('Page 1')
  @page_2 = find_by_title('Page 2')
  @section_2 = find_by_title('Added Section')
  @page_3 = find_by_title('Page 3')
  @inserted_page = find_by_title('Inserted Page')
  @subsection = find_by_title('Added SubSection')
  @added_page_3 = find_by_title('Added Page 3')
  @custom_dest = find_by_title('Custom Destination')
end

# Outline titles are stored as UTF-16. This method accepts a UTF-8 outline title
# and returns the PDF Object that contains an outline with that name
def find_by_title(title)
  @hash.values.find {|obj|
    if obj.is_a?(Hash) && obj[:Title]
      title_codepoints = obj[:Title].unpack("n*")
      title_codepoints.shift
      utf8_title = title_codepoints.pack("U*")
      utf8_title == title ? obj : nil
    end
  }
end

def referenced_object(reference)
  @hash[reference]
end
