# encoding: utf-8

require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")  
require 'set'

describe "Prawn::Table" do

  describe "converting data to Cell objects" do
    before(:each) do
      @pdf = Prawn::Document.new
      @table = @pdf.table([%w[R0C0 R0C1], %w[R1C0 R1C1]])
    end

    it "should return a Prawn::Table" do
      @table.should.be.an.instance_of Prawn::Table
    end

    it "should flatten the data into the @cells array in row-major order" do
      @table.cells.map { |c| c.content }.should == %w[R0C0 R0C1 R1C0 R1C1]
    end

    it "should add row and column numbers to each cell" do
      c = @table.cells.to_a.first
      c.row.should == 0
      c.column.should == 0
    end

    it "should allow empty fields" do
      lambda {
        data = [["foo","bar"],["baz",""]]
        @pdf.table(data)
      }.should.not.raise
    end   

    it "should allow a table with a header but no body" do
      lambda { @pdf.table([["Header"]], :header => true) }.should.not.raise
    end

    # TODO: pending colspan
    xit "should accurately count columns from data" do
      # First data row may contain colspan which would hide true column count
      data = [["Name:",{:text => "Some very long name", :colspan => 5}]]
      pdf = Prawn::Document.new
      table = Prawn::Table.new data, pdf
      table.column_widths.length.should == 6
    end
  end

  describe "#initialize" do
    before(:each) do
      @pdf = Prawn::Document.new
    end

    it "should instance_eval a 0-arg block" do
      initializer = mock()
      initializer.expects(:kick).once

      @pdf.table([["a"]]){
        self.should.be.an.instance_of(Prawn::Table); initializer.kick }
    end

    it "should call a 1-arg block with the document as the argument" do
      initializer = mock()
      initializer.expects(:kick).once

      @pdf.table([["a"]]){ |doc|
        doc.should.be.an.instance_of(Prawn::Table); initializer.kick }
    end

    it "should proxy cell methods to #cells" do
      table = @pdf.table([["a"]], :cell_style => { :padding => 11 })
      table.cells[0, 0].padding.should == [11, 11, 11, 11]
    end

    it "should set row and column length" do
      table = @pdf.table([["a", "b", "c"], ["d", "e", "f"]])
      table.row_length.should == 2
      table.column_length.should == 3
    end

    it "should generate a text cell based on a String" do
      t = @pdf.table([["foo"]])
      t.cells[0,0].should.be.a.kind_of(Prawn::Table::Cell::Text)
    end

    it "should pass through a text cell" do
      c = Prawn::Table::Cell::Text.new(@pdf, [0,0], :content => "foo")
      t = @pdf.table([[c]])
      t.cells[0,0].should == c
    end
  end

  describe "cell accessors" do
    before(:each) do
      @pdf = Prawn::Document.new
      @table = @pdf.table([%w[R0C0 R0C1], %w[R1C0 R1C1]])
    end

    it "should select rows by number or range" do
      Set.new(@table.row(0).map { |c| c.content }).should == 
        Set.new(%w[R0C0 R0C1])
      Set.new(@table.rows(0..1).map { |c| c.content }).should == 
        Set.new(%w[R0C0 R0C1 R1C0 R1C1])
    end

    it "should allow negative row selectors" do
      Set.new(@table.row(-1).map { |c| c.content }).should ==
        Set.new(%w[R1C0 R1C1])
      Set.new(@table.rows(-2..-1).map { |c| c.content }).should ==
        Set.new(%w[R0C0 R0C1 R1C0 R1C1])
      Set.new(@table.rows(0..-1).map { |c| c.content }).should ==
        Set.new(%w[R0C0 R0C1 R1C0 R1C1])
    end

    it "should select columns by number or range" do
      Set.new(@table.column(0).map { |c| c.content }).should == 
        Set.new(%w[R0C0 R1C0])
      Set.new(@table.columns(0..1).map { |c| c.content }).should == 
        Set.new(%w[R0C0 R0C1 R1C0 R1C1])
    end

    it "should allow negative column selectors" do
      Set.new(@table.column(-1).map { |c| c.content }).should ==
        Set.new(%w[R0C1 R1C1])
      Set.new(@table.columns(-2..-1).map { |c| c.content }).should ==
        Set.new(%w[R0C0 R0C1 R1C0 R1C1])
      Set.new(@table.columns(0..-1).map { |c| c.content }).should ==
        Set.new(%w[R0C0 R0C1 R1C0 R1C1])
    end

    it "should allow rows and columns to be combined" do
      @table.row(0).column(1).map { |c| c.content }.should == ["R0C1"]
    end

    it "should accept a filter block, returning a cell proxy" do
      @table.cells.filter { |c| c.content =~ /R0/ }.column(1).map{ |c| 
        c.content }.should == ["R0C1"]
    end

    it "should accept the [] method, returning a Cell or nil" do
      @table.cells[0, 0].content.should == "R0C0"
      @table.cells[12, 12].should.be.nil
    end

    it "should proxy unknown methods to the cells" do
      @table.cells.height = 200
      @table.row(1).height = 100

      @table.cells[0, 0].height.should == 200
      @table.cells[1, 0].height.should == 100
    end

    it "should accept the style method, proxying its calls to the cells" do
      @table.cells.style(:height => 200, :width => 200)
      @table.column(0).style(:width => 100)

      @table.cells[0, 1].width.should == 200
      @table.cells[1, 0].height.should == 200
      @table.cells[1, 0].width.should == 100
    end

    it "style method should accept a block, passing each cell to be styled" do
      @table.cells.style { |c| c.height = 200 }
      @table.cells[0, 1].height.should == 200
    end

    it "should return the width of selected columns for #width" do
      c0_width = @table.column(0).map{ |c| c.width }.max
      c1_width = @table.column(1).map{ |c| c.width }.max

      @table.column(0).width.should == c0_width
      @table.column(1).width.should == c1_width

      @table.columns(0..1).width.should == c0_width + c1_width
      @table.cells.width.should == c0_width + c1_width
    end

    it "should return the height of selected rows for #height" do
      r0_height = @table.row(0).map{ |c| c.height }.max
      r1_height = @table.row(1).map{ |c| c.height }.max

      @table.row(0).height.should == r0_height
      @table.row(1).height.should == r1_height

      @table.rows(0..1).height.should == r0_height + r1_height
      @table.cells.height.should == r0_height + r1_height
    end
  end

  describe "layout" do
    before(:each) do
      @pdf = Prawn::Document.new
      @long_text = "The quick brown fox jumped over the lazy dogs. " * 5
    end

    describe "width" do
      it "should raise an error if the given width is outside of range" do
        lambda do
          @pdf.table([["foo"]], :width => 1)
        end.should.raise(Prawn::Errors::CannotFit)

        lambda do
          @pdf.table([[@long_text]], :width => @pdf.bounds.width + 100)
        end.should.raise(Prawn::Errors::CannotFit)
      end

      it "should accept the natural width for small tables" do
        pad = 10 # default padding
        @table = @pdf.table([["a"]])
        @table.width.should == @table.cells[0, 0].natural_content_width + pad
      end

      it "width should equal sum(column_widths)" do
        table = Prawn::Table.new([%w[ a b c ], %w[d e f]], @pdf) do
          column(0).width = 50
          column(1).width = 100
          column(2).width = 150
        end
        table.width.should == 300
      end

      it "should accept Numeric for column_widths" do
        table = Prawn::Table.new([%w[ a b c ], %w[d e f]], @pdf) do |t|
          t.column_widths = 50
        end
        table.width.should == 150
      end

      it "should calculate unspecified column widths as "+
         "(max(string_width) + 2*horizontal_padding)" do
        hpad, fs = 3, 12
        columns = 2
        table = Prawn::Table.new( [%w[ foo b ], %w[d foobar]], @pdf,
          :cell_style => { :padding => hpad, :size => fs } )

        col0_width = @pdf.width_of("foo", :size => fs)
        col1_width = @pdf.width_of("foobar", :size => fs)

        table.width.should == col0_width + col1_width + 2*columns*hpad
      end

      it "should allow mixing autocalculated and preset"+
         "column widths within a single table" do
        hpad, fs = 10, 6
        stretchy_columns = 2
        
        col0_width = 50
        col1_width = @pdf.width_of("foo", :size => fs)
        col2_width = @pdf.width_of("foobar", :size => fs)
        col3_width = 150

        table = Prawn::Table.new( [%w[snake foo b apple], 
                                   %w[kitten d foobar banana]], @pdf,
          :cell_style => { :padding => hpad, :size => fs }) do

          column(0).width = col0_width
          column(3).width = col3_width
        end

        table.width.should == col1_width + col2_width + 
                              2*stretchy_columns*hpad + 
                              col0_width + col3_width
      end

      it "should not exceed the maximum width of the margin_box" do
        expected_width = @pdf.margin_box.width
        data = [
          ['This is a column with a lot of text that should comfortably exceed '+
          'the width of a normal document margin_box width', 'Some more text', 
          'and then some more', 'Just a bit more to be extra sure']
        ]
        table = Prawn::Table.new(data, @pdf)

        table.width.should == expected_width
      end

      it "should not exceed the maximum width of the margin_box even with" +
        "manual widths specified" do
        expected_width = @pdf.margin_box.width
        data = [
          ['This is a column with a lot of text that should comfortably exceed '+
          'the width of a normal document margin_box width', 'Some more text', 
          'and then some more', 'Just a bit more to be extra sure']
        ]
        table = Prawn::Table.new(data, @pdf) { column(1).width = 100 }

        table.width.should == expected_width
      end
      
      it "scales down only the non-preset column widths when the natural width" +
        "exceeds the maximum width of the margin_box" do
        expected_width = @pdf.margin_box.width
        data = [
          ['This is a column with a lot of text that should comfortably exceed '+
          'the width of a normal document margin_box width', 'Some more text', 
          'and then some more', 'Just a bit more to be extra sure']
        ]
        table = Prawn::Table.new(data, @pdf) { column(1).width = 100; column(3).width = 50 }

        table.width.should == expected_width
        table.column_widths[1].should == 100
        table.column_widths[3].should == 50
      end

      it "should allow width to be reset even after it has been calculated" do
        @table = @pdf.table([[@long_text]])
        @table.width
        @table.width = 100
        @table.width.should == 100
      end

      it "should shrink columns evenly when two equal columns compete" do
        @table = @pdf.table([["foo", @long_text], [@long_text, "foo"]])
        @table.cells[0, 0].width.should == @table.cells[0, 1].width
      end

      it "should grow columns evenly when equal deficient columns compete" do
        @table = @pdf.table([["foo", "foobar"], ["foobar", "foo"]], :width => 500)
        @table.cells[0, 0].width.should == @table.cells[0, 1].width
      end

      it "should respect manual widths" do
        @table = @pdf.table([%w[foo bar baz], %w[baz bar foo]], :width => 500) do
          column(1).width = 60
        end
        @table.column(1).width.should == 60
        @table.column(0).width.should == @table.column(2).width
      end

      it "should allow table cells to be resized in block" do
        lambda do
          @pdf.table([%w[1 2 3 4 5]]) do |t|
            t.width = 40
            t.cells.size = 8
            t.cells.padding = 0
          end
        end.should.not.raise(Prawn::Errors::CannotFit)
      end

      it "should be the width of the :width parameter" do
        expected_width = 300
        table = Prawn::Table.new( [%w[snake foo b apple], 
                                   %w[kitten d foobar banana]], @pdf,
                                 :width => expected_width)

        table.width.should == expected_width
      end

      it "should not exceed the :width option" do
        expected_width = 400
        data = [
          ['This is a column with a lot of text that should comfortably exceed '+
          'the width of a normal document margin_box width', 'Some more text', 
          'and then some more', 'Just a bit more to be extra sure']
        ]
        table = Prawn::Table.new(data, @pdf, :width => expected_width)

        table.width.should == expected_width
      end

      it "should not exceed the :width option even with manual widths specified" do
        expected_width = 400
        data = [
          ['This is a column with a lot of text that should comfortably exceed '+
          'the width of a normal document margin_box width', 'Some more text', 
          'and then some more', 'Just a bit more to be extra sure']
        ]
        table = Prawn::Table.new(data, @pdf, :width => expected_width) do
          column(1).width = 100
        end

        table.width.should == expected_width
      end

      # TODO: pending colspan
      xit "should calculate unspecified column widths even " +
         "with colspan cells declared" do
        pdf = Prawn::Document.new
        hpad, fs = 3, 5
        columns  = 3

        data = [ [ { :text => 'foo', :colspan => 2 }, "foobar" ],
                 [ "foo", "foo", "foo" ] ]
        table = Prawn::Table.new( data, pdf,
          :horizontal_padding => hpad,
          :font_size => fs )

        col0_width = pdf.width_of("foo",    :size => fs) # cell 1, 0
        col1_width = pdf.width_of("foo",    :size => fs) # cell 1, 1
        col2_width = pdf.width_of("foobar", :size => fs) # cell 0, 1 (at col 2)

        table.width.should == col0_width.ceil + col1_width.ceil +
                              col2_width.ceil + 2*columns*hpad
      end
    end

    describe "height" do
      it "should set all cells in a row to the same height" do
        @table = @pdf.table([["foo", @long_text]])
        @table.cells[0, 0].height.should == @table.cells[0, 1].height
      end

      it "should move y-position to the bottom of the table after drawing" do
        old_y = @pdf.y
        table = @pdf.table([["foo"]])
        @pdf.y.should == old_y - table.height
      end

      it "should not wrap unnecessarily" do
        # Test for FP errors and glitches
        t = @pdf.table([["Bender Bending Rodriguez"]])
        h = @pdf.height_of("one line")
        (t.height - 10).should.be < h*1.5
      end

      it "should have a height of n rows" do  
        data = [["foo"],["bar"],["baaaz"]]
           
        vpad = 4
        origin = @pdf.y
        @pdf.table data, :cell_style => { :padding => vpad }

        table_height = origin - @pdf.y
        font_height = @pdf.font.height
        line_gap = @pdf.font.line_gap

        num_rows = data.length
        table_height.should.be.close(
          num_rows * font_height + 2*vpad*num_rows, 0.001 )
      end

    end

  end

  describe "Multi-page tables" do
    it "should flow to the next page when hitting the bottom of the bounds" do
      Prawn::Document.new { table([["foo"]] * 30) }.page_count.should == 1
      Prawn::Document.new { table([["foo"]] * 31) }.page_count.should == 2
      Prawn::Document.new { table([["foo"]] * 31); table([["foo"]] * 35) }.
        page_count.should == 3
    end

    it "should respect the containing bounds" do
      Prawn::Document.new do
        bounding_box([0, cursor], :width => bounds.width, :height => 72) do
          table([["foo"]] * 4)
        end
      end.page_count.should == 2
    end

    it "should not start a new page before finishing out a row" do
      Prawn::Document.new do
        table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
      end.page_count.should == 1
    end

    it "should only start new page on long cells if it would gain us height" do
      Prawn::Document.new do
        text "Hello"
        table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
      end.page_count.should == 2
    end

    it "should not start a new page to gain height when at the top of " +
       "a bounding box, even if stretchy" do
      Prawn::Document.new do
        bounding_box([bounds.left, bounds.top - 20], :width => 400) do
          table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
        end
      end.page_count.should == 1
    end

    it "should still break to the next page if in a stretchy bounding box " +
       "but not at the top" do
      Prawn::Document.new do
        bounding_box([bounds.left, bounds.top - 20], :width => 400) do
          text "Hello"
          table([[ (1..80).map{ |i| "Line #{i}" }.join("\n"), "Column 2" ]])
        end
      end.page_count.should == 2
    end

    it "should only draw first-page header if the first body row fits" do
      pdf = Prawn::Document.new

      pdf.y = 60 # not enough room for a table row
      pdf.table [["Header"], ["Body"]], :header => true

      output = PDF::Inspector::Page.analyze(pdf.render)
      # Ensure we only drew the header once, on the second page
      output.pages[0][:strings].should.be.empty
      output.pages[1][:strings].should == ["Header", "Body"]
    end

    it "should draw background before borders, but only within pages" do
      seq = sequence("drawing_order")

      @pdf = Prawn::Document.new

      # give enough room for only the first row
      @pdf.y = @pdf.bounds.absolute_bottom + 30
      t = @pdf.make_table([["A", "B"],
                           ["C", "D"]],
            :cell_style => {:background_color => 'ff0000'})

      ca = t.cells[0, 0]
      cb = t.cells[0, 1]
      cc = t.cells[1, 0]
      cd = t.cells[1, 1]

      # All backgrounds should draw before any borders on page 1...
      ca.expects(:draw_background).in_sequence(seq)
      cb.expects(:draw_background).in_sequence(seq)
      ca.expects(:draw_borders).in_sequence(seq)
      cb.expects(:draw_borders).in_sequence(seq)
      # ...and page 2
      @pdf.expects(:start_new_page).in_sequence(seq)
      cc.expects(:draw_background).in_sequence(seq)
      cd.expects(:draw_background).in_sequence(seq)
      cc.expects(:draw_borders).in_sequence(seq)
      cd.expects(:draw_borders).in_sequence(seq)

      t.draw
    end
  end

  describe "#style" do
    it "should send #style to its first argument, passing the style hash and" +
        " block" do

      stylable = stub()
      stylable.expects(:style).with(:foo => :bar).once.yields

      block = stub()
      block.expects(:kick).once

      Prawn::Document.new do
        table([["x"]]) { style(stylable, :foo => :bar) { block.kick } }
      end
    end

    it "should default to {} for the hash argument" do
      stylable = stub()
      stylable.expects(:style).with({}).once
      
      Prawn::Document.new do
        table([["x"]]) { style(stylable) }
      end
    end
  end

  describe "row_colors" do
    it "should allow array syntax for :row_colors" do
      data = [["foo"], ["bar"], ["baz"]]
      pdf = Prawn::Document.new
      t = pdf.table(data, :row_colors => ['cccccc', 'ffffff'])
      t.cells.map{|x| x.background_color}.should == %w[cccccc ffffff cccccc]
    end

    it "should ignore headers" do
      data = [["header"], ["foo"], ["bar"], ["baz"]]
      pdf = Prawn::Document.new
      t = pdf.table(data, :header => true, 
                    :row_colors => ['cccccc', 'ffffff']) do
        row(0).background_color = '333333'
      end

      t.cells.map{|x| x.background_color}.should == 
        %w[333333 cccccc ffffff cccccc]
    end
  end

  describe "inking" do
    before(:each) do
      @pdf = Prawn::Document.new
    end

    it "should set the x-position of each cell based on widths" do
      @table = @pdf.table([["foo", "bar", "baz"]])
      
      x = 0
      (0..2).each do |col|
        cell = @table.cells[0, col]
        cell.x.should == x
        x += cell.width
      end
    end

    it "should set the y-position of each cell based on heights" do
      y = 0
      @table = @pdf.make_table([["foo"], ["bar"], ["baz"]])

      (0..2).each do |row|
        cell = @table.cells[row, 0]
        cell.y.should.be.close(y, 0.01)
        y -= cell.height
      end
    end

    it "should output content cell by cell, row by row" do
      data = [["foo","bar"],["baz","bang"]]
      @pdf = Prawn::Document.new
      @pdf.table(data)
      output = PDF::Inspector::Text.analyze(@pdf.render)
      output.strings.should == data.flatten
    end

    it "should not cause an error if rendering the very first row causes a " +
      "page break" do
      Prawn::Document.new do
        arr = Array(1..5).collect{|i| ["cell #{i}"] }

        move_down( y - (bounds.absolute_bottom + 3) )

        lambda {
          table(arr)
        }.should.not.raise
      end
    end

    it "should draw all backgrounds before any borders" do
      # lest backgrounds overlap borders:
      # https://github.com/sandal/prawn/pull/226

      seq = sequence("drawing_order")

      t = @pdf.make_table([["A", "B"]],
            :cell_style => {:background_color => 'ff0000'})
      ca = t.cells[0, 0]
      cb = t.cells[0, 1]

      # XXX Not a perfectly general test, because it would still be acceptable
      # if we drew B then A
      ca.expects(:draw_background).in_sequence(seq)
      cb.expects(:draw_background).in_sequence(seq)
      ca.expects(:draw_borders).in_sequence(seq)
      cb.expects(:draw_borders).in_sequence(seq)

      t.draw
    end

    it "should allow multiple inkings of the same table" do
      pdf = Prawn::Document.new
      t = Prawn::Table.new([["foo"]], pdf)

      pdf.expects(:bounding_box).with{|(x, y), options| y.to_i == 495}.yields
      pdf.expects(:bounding_box).with{|(x, y), options| y.to_i == 395}.yields
      pdf.expects(:draw_text!).with{ |text, options| text == 'foo' }.twice

      pdf.move_cursor_to(500)
      t.draw

      pdf.move_cursor_to(400)
      t.draw
    end

    describe "in stretchy bounding boxes" do
      it "should draw all cells on a row at the same y-position" do
        pdf = Prawn::Document.new
        
        text_y = pdf.y.to_i - 5 # text starts 5pt below current y pos (padding)

        pdf.bounding_box([0, pdf.cursor], :width => pdf.bounds.width) do
          pdf.expects(:draw_text!).checking { |text, options|
            pdf.bounds.absolute_top.should == text_y
          }.times(3)

          pdf.table([%w[a b c]])
        end
      end
    end
  end

  describe "headers" do
    it "should add headers to output when specified" do
      data = [["a", "b"], ["foo","bar"],["baz","bang"]]
      @pdf = Prawn::Document.new
      @pdf.table(data, :header => true)
      output = PDF::Inspector::Text.analyze(@pdf.render)   
      output.strings.should == data.flatten
    end

    it "should repeat headers across pages" do
      data = [["foo","bar"]] * 30
      headers = ["baz","foobar"]
      @pdf = Prawn::Document.new
      @pdf.table([headers] + data, :header => true)
      output = PDF::Inspector::Text.analyze(@pdf.render)   
      output.strings.should == headers + data.flatten[0..-3] + headers +
        data.flatten[-2..-1]
    end

    it "should not draw header twice when starting new page" do
      @pdf = Prawn::Document.new
      @pdf.y = 0
      @pdf.table([["Header"], ["Body"]], :header => true)
      output = PDF::Inspector::Text.analyze(@pdf.render)   
      output.strings.should == ["Header", "Body"]
    end
  end

  describe "nested tables" do
    before(:each) do
      @pdf = Prawn::Document.new
      @subtable = Prawn::Table.new([["foo"]], @pdf)
      @table = @pdf.table([[@subtable, "bar"]])
    end

    it "can be created from an Array" do
      cell = Prawn::Table::Cell.make(@pdf, [["foo"]])
      cell.should.be.an.instance_of(Prawn::Table::Cell::Subtable)
      cell.subtable.should.be.an.instance_of(Prawn::Table)
    end

    it "defaults its padding to zero" do
      @table.cells[0, 0].padding.should == [0, 0, 0, 0]
    end

    it "has a subtable accessor" do
      @table.cells[0, 0].subtable.should == @subtable
    end
    
    it "determines its dimensions from the subtable" do
      @table.cells[0, 0].width.should == @subtable.width
      @table.cells[0, 0].height.should == @subtable.height
    end

  end

  describe "An invalid table" do
    
    before(:each) do
      @pdf = Prawn::Document.new
      @bad_data = ["Single Nested Array"]
    end
    
    it "should raise error when invalid table data is given" do
      assert_raises(Prawn::Errors::InvalidTableData) do
        @pdf.table(@bad_data)
      end
    end

    it "should raise an EmptyTableError with empty table data" do
      lambda {
        data = []
        @pdf = Prawn::Document.new
        @pdf.table(data)
      }.should.raise( Prawn::Errors::EmptyTable )
    end   

    it "should raise an EmptyTableError with nil table data" do
      lambda {
        data = nil
        @pdf = Prawn::Document.new
        @pdf.table(data)
      }.should.raise( Prawn::Errors::EmptyTable )
    end   

  end

end

