# encoding: utf-8

"""Test suite for the docx.table module"""

from __future__ import absolute_import, print_function, unicode_literals

import pytest

from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import (
    WD_ALIGN_VERTICAL, WD_ROW_HEIGHT, WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION
)
from docx.oxml import parse_xml
from docx.oxml.table import CT_Tc
from docx.parts.document import DocumentPart
from docx.shared import Inches
from docx.table import _Cell, _Column, _Columns, _Row, _Rows, Table
from docx.text.paragraph import Paragraph

from .oxml.unitdata.table import a_gridCol, a_tbl, a_tblGrid, a_tc, a_tr
from .oxml.unitdata.text import a_p
from .unitutil.cxml import element, xml
from .unitutil.file import snippet_seq
from .unitutil.mock import instance_mock, property_mock


class DescribeTable(object):

    def it_can_add_a_row(self, add_row_fixture):
        table, expected_xml = add_row_fixture
        row = table.add_row()
        assert table._tbl.xml == expected_xml
        assert isinstance(row, _Row)
        assert row._tr is table._tbl.tr_lst[-1]
        assert row._parent is table

    def it_can_add_a_column(self, add_column_fixture):
        table, width, expected_xml = add_column_fixture
        column = table.add_column(width)
        assert table._tbl.xml == expected_xml
        assert isinstance(column, _Column)
        assert column._gridCol is table._tbl.tblGrid.gridCol_lst[-1]
        assert column._parent is table

    def it_provides_access_to_a_cell_by_row_and_col_indices(self, table):
        for row_idx in range(2):
            for col_idx in range(2):
                cell = table.cell(row_idx, col_idx)
                assert isinstance(cell, _Cell)
                tr = table._tbl.tr_lst[row_idx]
                tc = tr.tc_lst[col_idx]
                assert tc is cell._tc

    def it_provides_access_to_the_table_rows(self, table):
        rows = table.rows
        assert isinstance(rows, _Rows)

    def it_provides_access_to_the_table_columns(self, table):
        columns = table.columns
        assert isinstance(columns, _Columns)

    def it_provides_access_to_the_cells_in_a_column(self, col_cells_fixture):
        table, column_idx, expected_cells = col_cells_fixture
        column_cells = table.column_cells(column_idx)
        assert column_cells == expected_cells

    def it_provides_access_to_the_cells_in_a_row(self, row_cells_fixture):
        table, row_idx, expected_cells = row_cells_fixture
        row_cells = table.row_cells(row_idx)
        assert row_cells == expected_cells

    def it_knows_its_alignment_setting(self, alignment_get_fixture):
        table, expected_value = alignment_get_fixture
        assert table.alignment == expected_value

    def it_can_change_its_alignment_setting(self, alignment_set_fixture):
        table, new_value, expected_xml = alignment_set_fixture
        table.alignment = new_value
        assert table._tbl.xml == expected_xml

    def it_knows_whether_it_should_autofit(self, autofit_get_fixture):
        table, expected_value = autofit_get_fixture
        assert table.autofit is expected_value

    def it_can_change_its_autofit_setting(self, autofit_set_fixture):
        table, new_value, expected_xml = autofit_set_fixture
        table.autofit = new_value
        assert table._tbl.xml == expected_xml

    def it_knows_it_is_the_table_its_children_belong_to(self, table_fixture):
        table = table_fixture
        assert table.table is table

    def it_knows_its_direction(self, direction_get_fixture):
        table, expected_value = direction_get_fixture
        assert table.table_direction == expected_value

    def it_can_change_its_direction(self, direction_set_fixture):
        table, new_value, expected_xml = direction_set_fixture
        table.table_direction = new_value
        assert table._element.xml == expected_xml

    def it_knows_its_table_style(self, style_get_fixture):
        table, style_id_, style_ = style_get_fixture
        style = table.style
        table.part.get_style.assert_called_once_with(
            style_id_, WD_STYLE_TYPE.TABLE
        )
        assert style is style_

    def it_can_change_its_table_style(self, style_set_fixture):
        table, value, expected_xml = style_set_fixture
        table.style = value
        table.part.get_style_id.assert_called_once_with(
            value, WD_STYLE_TYPE.TABLE
        )
        assert table._tbl.xml == expected_xml

    def it_provides_access_to_its_cells_to_help(self, cells_fixture):
        table, cell_count, unique_count, matches = cells_fixture
        cells = table._cells
        assert len(cells) == cell_count
        assert len(set(cells)) == unique_count
        for matching_idxs in matches:
            comparator_idx = matching_idxs[0]
            for idx in matching_idxs[1:]:
                assert cells[idx] is cells[comparator_idx]

    def it_knows_its_column_count_to_help(self, column_count_fixture):
        table, expected_value = column_count_fixture
        column_count = table._column_count
        assert column_count == expected_value

    # fixtures -------------------------------------------------------

    @pytest.fixture
    def add_column_fixture(self):
        snippets = snippet_seq('add-row-col')
        tbl = parse_xml(snippets[0])
        table = Table(tbl, None)
        width = Inches(1.5)
        expected_xml = snippets[2]
        return table, width, expected_xml

    @pytest.fixture
    def add_row_fixture(self):
        snippets = snippet_seq('add-row-col')
        tbl = parse_xml(snippets[0])
        table = Table(tbl, None)
        expected_xml = snippets[1]
        return table, expected_xml

    @pytest.fixture(params=[
        ('w:tbl/w:tblPr',                    None),
        ('w:tbl/w:tblPr/w:jc{w:val=center}', WD_TABLE_ALIGNMENT.CENTER),
        ('w:tbl/w:tblPr/w:jc{w:val=right}',  WD_TABLE_ALIGNMENT.RIGHT),
        ('w:tbl/w:tblPr/w:jc{w:val=left}',   WD_TABLE_ALIGNMENT.LEFT),
    ])
    def alignment_get_fixture(self, request):
        tbl_cxml, expected_value = request.param
        table = Table(element(tbl_cxml), None)
        return table, expected_value

    @pytest.fixture(params=[
        ('w:tbl/w:tblPr', WD_TABLE_ALIGNMENT.LEFT,
         'w:tbl/w:tblPr/w:jc{w:val=left}'),
        ('w:tbl/w:tblPr/w:jc{w:val=left}', WD_TABLE_ALIGNMENT.RIGHT,
         'w:tbl/w:tblPr/w:jc{w:val=right}'),
        ('w:tbl/w:tblPr/w:jc{w:val=right}', None,
         'w:tbl/w:tblPr'),
    ])
    def alignment_set_fixture(self, request):
        tbl_cxml, new_value, expected_tbl_cxml = request.param
        table = Table(element(tbl_cxml), None)
        expected_xml = xml(expected_tbl_cxml)
        return table, new_value, expected_xml

    @pytest.fixture(params=[
        ('w:tbl/w:tblPr',                             True),
        ('w:tbl/w:tblPr/w:tblLayout',                 True),
        ('w:tbl/w:tblPr/w:tblLayout{w:type=autofit}', True),
        ('w:tbl/w:tblPr/w:tblLayout{w:type=fixed}',   False),
    ])
    def autofit_get_fixture(self, request):
        tbl_cxml, expected_autofit = request.param
        table = Table(element(tbl_cxml), None)
        return table, expected_autofit

    @pytest.fixture(params=[
        ('w:tbl/w:tblPr', True,
         'w:tbl/w:tblPr/w:tblLayout{w:type=autofit}'),
        ('w:tbl/w:tblPr', False,
         'w:tbl/w:tblPr/w:tblLayout{w:type=fixed}'),
        ('w:tbl/w:tblPr', None,
         'w:tbl/w:tblPr/w:tblLayout{w:type=fixed}'),
        ('w:tbl/w:tblPr/w:tblLayout{w:type=fixed}', True,
         'w:tbl/w:tblPr/w:tblLayout{w:type=autofit}'),
        ('w:tbl/w:tblPr/w:tblLayout{w:type=autofit}', False,
         'w:tbl/w:tblPr/w:tblLayout{w:type=fixed}'),
    ])
    def autofit_set_fixture(self, request):
        tbl_cxml, new_value, expected_tbl_cxml = request.param
        table = Table(element(tbl_cxml), None)
        expected_xml = xml(expected_tbl_cxml)
        return table, new_value, expected_xml

    @pytest.fixture(params=[
        (0, 9, 9, ()),
        (1, 9, 8, ((0, 1),)),
        (2, 9, 8, ((1, 4),)),
        (3, 9, 6, ((0, 1, 3, 4),)),
        (4, 9, 4, ((0, 1), (3, 6), (4, 5, 7, 8))),
    ])
    def cells_fixture(self, request):
        snippet_idx, cell_count, unique_count, matches = request.param
        tbl_xml = snippet_seq('tbl-cells')[snippet_idx]
        table = Table(parse_xml(tbl_xml), None)
        return table, cell_count, unique_count, matches

    @pytest.fixture
    def col_cells_fixture(self, _cells_, _column_count_):
        table = Table(None, None)
        _cells_.return_value = [0, 1, 2, 3, 4, 5, 6, 7, 8]
        _column_count_.return_value = 3
        column_idx = 1
        expected_cells = [1, 4, 7]
        return table, column_idx, expected_cells

    @pytest.fixture
    def column_count_fixture(self):
        tbl_cxml = 'w:tbl/w:tblGrid/(w:gridCol,w:gridCol,w:gridCol)'
        expected_value = 3
        table = Table(element(tbl_cxml), None)
        return table, expected_value

    @pytest.fixture(params=[
        ('w:tbl/w:tblPr',                        None),
        ('w:tbl/w:tblPr/w:bidiVisual',           WD_TABLE_DIRECTION.RTL),
        ('w:tbl/w:tblPr/w:bidiVisual{w:val=0}',  WD_TABLE_DIRECTION.LTR),
        ('w:tbl/w:tblPr/w:bidiVisual{w:val=on}', WD_TABLE_DIRECTION.RTL),
    ])
    def direction_get_fixture(self, request):
        tbl_cxml, expected_value = request.param
        table = Table(element(tbl_cxml), None)
        return table, expected_value

    @pytest.fixture(params=[
        ('w:tbl/w:tblPr',                       WD_TABLE_DIRECTION.RTL,
         'w:tbl/w:tblPr/w:bidiVisual'),
        ('w:tbl/w:tblPr/w:bidiVisual',          WD_TABLE_DIRECTION.LTR,
         'w:tbl/w:tblPr/w:bidiVisual{w:val=0}'),
        ('w:tbl/w:tblPr/w:bidiVisual{w:val=0}', WD_TABLE_DIRECTION.RTL,
         'w:tbl/w:tblPr/w:bidiVisual'),
        ('w:tbl/w:tblPr/w:bidiVisual{w:val=1}', None,
         'w:tbl/w:tblPr'),
    ])
    def direction_set_fixture(self, request):
        tbl_cxml, new_value, expected_cxml = request.param
        table = Table(element(tbl_cxml), None)
        expected_xml = xml(expected_cxml)
        return table, new_value, expected_xml

    @pytest.fixture
    def row_cells_fixture(self, _cells_, _column_count_):
        table = Table(None, None)
        _cells_.return_value = [0, 1, 2, 3, 4, 5, 6, 7, 8]
        _column_count_.return_value = 3
        row_idx = 1
        expected_cells = [3, 4, 5]
        return table, row_idx, expected_cells

    @pytest.fixture
    def style_get_fixture(self, part_prop_):
        style_id = 'Barbaz'
        tbl_cxml = 'w:tbl/w:tblPr/w:tblStyle{w:val=%s}' % style_id
        table = Table(element(tbl_cxml), None)
        style_ = part_prop_.return_value.get_style.return_value
        return table, style_id, style_

    @pytest.fixture(params=[
        ('w:tbl/w:tblPr',                         'Tbl A', 'TblA',
         'w:tbl/w:tblPr/w:tblStyle{w:val=TblA}'),
        ('w:tbl/w:tblPr/w:tblStyle{w:val=TblA}',  'Tbl B', 'TblB',
         'w:tbl/w:tblPr/w:tblStyle{w:val=TblB}'),
        ('w:tbl/w:tblPr/w:tblStyle{w:val=TblB}',  None,    None,
         'w:tbl/w:tblPr'),
    ])
    def style_set_fixture(self, request, part_prop_):
        tbl_cxml, value, style_id, expected_cxml = request.param
        table = Table(element(tbl_cxml), None)
        part_prop_.return_value.get_style_id.return_value = style_id
        expected_xml = xml(expected_cxml)
        return table, value, expected_xml

    @pytest.fixture
    def table_fixture(self):
        table = Table(None, None)
        return table

    # fixture components ---------------------------------------------

    @pytest.fixture
    def _cells_(self, request):
        return property_mock(request, Table, '_cells')

    @pytest.fixture
    def _column_count_(self, request):
        return property_mock(request, Table, '_column_count')

    @pytest.fixture
    def document_part_(self, request):
        return instance_mock(request, DocumentPart)

    @pytest.fixture
    def part_prop_(self, request, document_part_):
        return property_mock(
            request, Table, 'part', return_value=document_part_
        )

    @pytest.fixture
    def table(self):
        tbl = _tbl_bldr(rows=2, cols=2).element
        table = Table(tbl, None)
        return table


class Describe_Cell(object):

    def it_knows_what_text_it_contains(self, text_get_fixture):
        cell, expected_text = text_get_fixture
        text = cell.text
        assert text == expected_text

    def it_can_replace_its_content_with_a_string_of_text(
            self, text_set_fixture):
        cell, text, expected_xml = text_set_fixture
        cell.text = text
        assert cell._tc.xml == expected_xml

    def it_knows_its_vertical_alignment(self, alignment_get_fixture):
        cell, expected_value = alignment_get_fixture
        vertical_alignment = cell.vertical_alignment
        assert vertical_alignment == expected_value

    def it_can_change_its_vertical_alignment(self, alignment_set_fixture):
        cell, new_value, expected_xml = alignment_set_fixture
        cell.vertical_alignment = new_value
        assert cell._element.xml == expected_xml

    def it_knows_its_width_in_EMU(self, width_get_fixture):
        cell, expected_width = width_get_fixture
        assert cell.width == expected_width

    def it_can_change_its_width(self, width_set_fixture):
        cell, value, expected_xml = width_set_fixture
        cell.width = value
        assert cell.width == value
        assert cell._tc.xml == expected_xml

    def it_provides_access_to_the_paragraphs_it_contains(
            self, paragraphs_fixture):
        cell = paragraphs_fixture
        paragraphs = cell.paragraphs
        assert len(paragraphs) == 2
        count = 0
        for idx, paragraph in enumerate(paragraphs):
            assert isinstance(paragraph, Paragraph)
            assert paragraph is paragraphs[idx]
            count += 1
        assert count == 2

    def it_provides_access_to_the_tables_it_contains(self, tables_fixture):
        # test len(), iterable, and indexed access
        cell, expected_count = tables_fixture
        tables = cell.tables
        assert len(tables) == expected_count
        count = 0
        for idx, table in enumerate(tables):
            assert isinstance(table, Table)
            assert tables[idx] is table
            count += 1
        assert count == expected_count

    def it_can_add_a_paragraph(self, add_paragraph_fixture):
        cell, expected_xml = add_paragraph_fixture
        p = cell.add_paragraph()
        assert cell._tc.xml == expected_xml
        assert isinstance(p, Paragraph)

    def it_can_add_a_table(self, add_table_fixture):
        cell, expected_xml = add_table_fixture
        table = cell.add_table(rows=2, cols=2)
        assert cell._element.xml == expected_xml
        assert isinstance(table, Table)

    def it_can_merge_itself_with_other_cells(self, merge_fixture):
        cell, other_cell, merged_tc_ = merge_fixture
        merged_cell = cell.merge(other_cell)
        cell._tc.merge.assert_called_once_with(other_cell._tc)
        assert isinstance(merged_cell, _Cell)
        assert merged_cell._tc is merged_tc_
        assert merged_cell._parent is cell._parent

    # fixtures -------------------------------------------------------

    @pytest.fixture(params=[
        ('w:tc',       'w:tc/w:p'),
        ('w:tc/w:p',   'w:tc/(w:p, w:p)'),
        ('w:tc/w:tbl', 'w:tc/(w:tbl, w:p)'),
    ])
    def add_paragraph_fixture(self, request):
        tc_cxml, after_tc_cxml = request.param
        cell = _Cell(element(tc_cxml), None)
        expected_xml = xml(after_tc_cxml)
        return cell, expected_xml

    @pytest.fixture
    def add_table_fixture(self, request):
        cell = _Cell(element('w:tc/w:p'), None)
        expected_xml = snippet_seq('new-tbl')[1]
        return cell, expected_xml

    @pytest.fixture(params=[
        ('w:tc', None),
        ('w:tc/w:tcPr', None),
        ('w:tc/w:tcPr/w:vAlign{w:val=bottom}', WD_ALIGN_VERTICAL.BOTTOM),
        ('w:tc/w:tcPr/w:vAlign{w:val=top}', WD_ALIGN_VERTICAL.TOP),
    ])
    def alignment_get_fixture(self, request):
        tc_cxml, expected_value = request.param
        cell = _Cell(element(tc_cxml), None)
        return cell, expected_value

    @pytest.fixture(params=[
        ('w:tc', WD_ALIGN_VERTICAL.TOP,
         'w:tc/w:tcPr/w:vAlign{w:val=top}'),
        ('w:tc/w:tcPr', WD_ALIGN_VERTICAL.CENTER,
         'w:tc/w:tcPr/w:vAlign{w:val=center}'),
        ('w:tc/w:tcPr/w:vAlign{w:val=center}', WD_ALIGN_VERTICAL.BOTTOM,
         'w:tc/w:tcPr/w:vAlign{w:val=bottom}'),
        ('w:tc/w:tcPr/w:vAlign{w:val=center}', None,
         'w:tc/w:tcPr'),
        ('w:tc', None,
         'w:tc/w:tcPr'),
        ('w:tc/w:tcPr', None,
         'w:tc/w:tcPr'),
    ])
    def alignment_set_fixture(self, request):
        cxml, new_value, expected_cxml = request.param
        cell = _Cell(element(cxml), None)
        expected_xml = xml(expected_cxml)
        return cell, new_value, expected_xml

    @pytest.fixture
    def merge_fixture(self, tc_, tc_2_, parent_, merged_tc_):
        cell, other_cell = _Cell(tc_, parent_), _Cell(tc_2_, parent_)
        tc_.merge.return_value = merged_tc_
        return cell, other_cell, merged_tc_

    @pytest.fixture
    def paragraphs_fixture(self):
        return _Cell(element('w:tc/(w:p, w:p)'), None)

    @pytest.fixture(params=[
        ('w:tc',                   0),
        ('w:tc/w:tbl',             1),
        ('w:tc/(w:tbl,w:tbl)',     2),
        ('w:tc/(w:p,w:tbl)',       1),
        ('w:tc/(w:tbl,w:tbl,w:p)', 2),
    ])
    def tables_fixture(self, request):
        cell_cxml, expected_count = request.param
        cell = _Cell(element(cell_cxml), None)
        return cell, expected_count

    @pytest.fixture(params=[
        ('w:tc',                                     ''),
        ('w:tc/w:p/w:r/w:t"foobar"',                 'foobar'),
        ('w:tc/(w:p/w:r/w:t"foo",w:p/w:r/w:t"bar")', 'foo\nbar'),
        ('w:tc/(w:tcPr,w:p/w:r/w:t"foobar")',        'foobar'),
        ('w:tc/w:p/w:r/(w:t"fo",w:tab,w:t"ob",w:br,w:t"ar",w:br)',
         'fo\tob\nar\n'),
    ])
    def text_get_fixture(self, request):
        tc_cxml, expected_text = request.param
        cell = _Cell(element(tc_cxml), None)
        return cell, expected_text

    @pytest.fixture(params=[
        ('w:tc/w:p', 'foobar',
         'w:tc/w:p/w:r/w:t"foobar"'),
        ('w:tc/w:p', 'fo\tob\rar\n',
         'w:tc/w:p/w:r/(w:t"fo",w:tab,w:t"ob",w:br,w:t"ar",w:br)'),
        ('w:tc/(w:tcPr, w:p, w:tbl, w:p)', 'foobar',
         'w:tc/(w:tcPr, w:p/w:r/w:t"foobar")'),
    ])
    def text_set_fixture(self, request):
        tc_cxml, new_text, expected_cxml = request.param
        cell = _Cell(element(tc_cxml), None)
        expected_xml = xml(expected_cxml)
        return cell, new_text, expected_xml

    @pytest.fixture(params=[
        ('w:tc',                                   None),
        ('w:tc/w:tcPr',                            None),
        ('w:tc/w:tcPr/w:tcW{w:w=25%,w:type=pct}',  None),
        ('w:tc/w:tcPr/w:tcW{w:w=1440,w:type=dxa}', 914400),
    ])
    def width_get_fixture(self, request):
        tc_cxml, expected_width = request.param
        cell = _Cell(element(tc_cxml), None)
        return cell, expected_width

    @pytest.fixture(params=[
        ('w:tc', Inches(1),
         'w:tc/w:tcPr/w:tcW{w:w=1440,w:type=dxa}'),
        ('w:tc/w:tcPr/w:tcW{w:w=25%,w:type=pct}', Inches(2),
         'w:tc/w:tcPr/w:tcW{w:w=2880,w:type=dxa}'),
    ])
    def width_set_fixture(self, request):
        tc_cxml, new_value, expected_cxml = request.param
        cell = _Cell(element(tc_cxml), None)
        expected_xml = xml(expected_cxml)
        return cell, new_value, expected_xml

    # fixture components ---------------------------------------------

    @pytest.fixture
    def merged_tc_(self, request):
        return instance_mock(request, CT_Tc)

    @pytest.fixture
    def parent_(self, request):
        return instance_mock(request, Table)

    @pytest.fixture
    def tc_(self, request):
        return instance_mock(request, CT_Tc)

    @pytest.fixture
    def tc_2_(self, request):
        return instance_mock(request, CT_Tc)


class Describe_Column(object):

    def it_provides_access_to_its_cells(self, cells_fixture):
        column, column_idx, expected_cells = cells_fixture
        cells = column.cells
        column.table.column_cells.assert_called_once_with(column_idx)
        assert cells == expected_cells

    def it_provides_access_to_the_table_it_belongs_to(self, table_fixture):
        column, table_ = table_fixture
        assert column.table is table_

    def it_knows_its_width_in_EMU(self, width_get_fixture):
        column, expected_width = width_get_fixture
        assert column.width == expected_width

    def it_can_change_its_width(self, width_set_fixture):
        column, value, expected_xml = width_set_fixture
        column.width = value
        assert column.width == value
        assert column._gridCol.xml == expected_xml

    def it_knows_its_index_in_table_to_help(self, index_fixture):
        column, expected_idx = index_fixture
        assert column._index == expected_idx

    # fixtures -------------------------------------------------------

    @pytest.fixture
    def cells_fixture(self, _index_, table_prop_, table_):
        column = _Column(None, None)
        _index_.return_value = column_idx = 4
        expected_cells = (3, 2, 1)
        table_.column_cells.return_value = list(expected_cells)
        return column, column_idx, expected_cells

    @pytest.fixture
    def index_fixture(self):
        tbl = element('w:tbl/w:tblGrid/(w:gridCol,w:gridCol,w:gridCol)')
        gridCol, expected_idx = tbl.tblGrid[1], 1
        column = _Column(gridCol, None)
        return column, expected_idx

    @pytest.fixture
    def table_fixture(self, parent_, table_):
        column = _Column(None, parent_)
        parent_.table = table_
        return column, table_

    @pytest.fixture(params=[
        ('w:gridCol{w:w=4242}',   2693670),
        ('w:gridCol{w:w=1440}',    914400),
        ('w:gridCol{w:w=2.54cm}',  914400),
        ('w:gridCol{w:w=54mm}',   1944000),
        ('w:gridCol{w:w=12.5pt}',  158750),
        ('w:gridCol',                None),
    ])
    def width_get_fixture(self, request):
        gridCol_cxml, expected_width = request.param
        column = _Column(element(gridCol_cxml), None)
        return column, expected_width

    @pytest.fixture(params=[
        ('w:gridCol',           914400, 'w:gridCol{w:w=1440}'),
        ('w:gridCol{w:w=4242}', 457200, 'w:gridCol{w:w=720}'),
        ('w:gridCol{w:w=4242}',   None, 'w:gridCol'),
        ('w:gridCol',             None, 'w:gridCol'),
    ])
    def width_set_fixture(self, request):
        gridCol_cxml, new_value, expected_cxml = request.param
        column = _Column(element(gridCol_cxml), None)
        expected_xml = xml(expected_cxml)
        return column, new_value, expected_xml

    # fixture components ---------------------------------------------

    @pytest.fixture
    def _index_(self, request):
        return property_mock(request, _Column, '_index')

    @pytest.fixture
    def parent_(self, request):
        return instance_mock(request, Table)

    @pytest.fixture
    def table_(self, request):
        return instance_mock(request, Table)

    @pytest.fixture
    def table_prop_(self, request, table_):
        return property_mock(request, _Column, 'table', return_value=table_)


class Describe_Columns(object):

    def it_knows_how_many_columns_it_contains(self, columns_fixture):
        columns, column_count = columns_fixture
        assert len(columns) == column_count

    def it_can_interate_over_its__Column_instances(self, columns_fixture):
        columns, column_count = columns_fixture
        actual_count = 0
        for column in columns:
            assert isinstance(column, _Column)
            actual_count += 1
        assert actual_count == column_count

    def it_provides_indexed_access_to_columns(self, columns_fixture):
        columns, column_count = columns_fixture
        for idx in range(-column_count, column_count):
            column = columns[idx]
            assert isinstance(column, _Column)

    def it_raises_on_indexed_access_out_of_range(self, columns_fixture):
        columns, column_count = columns_fixture
        too_low = -1 - column_count
        too_high = column_count
        with pytest.raises(IndexError):
            columns[too_low]
        with pytest.raises(IndexError):
            columns[too_high]

    def it_provides_access_to_the_table_it_belongs_to(self, table_fixture):
        columns, table_ = table_fixture
        assert columns.table is table_

    # fixtures -------------------------------------------------------

    @pytest.fixture
    def columns_fixture(self):
        column_count = 2
        tbl = _tbl_bldr(rows=2, cols=column_count).element
        columns = _Columns(tbl, None)
        return columns, column_count

    @pytest.fixture
    def table_fixture(self, table_):
        columns = _Columns(None, table_)
        table_.table = table_
        return columns, table_

    # fixture components ---------------------------------------------

    @pytest.fixture
    def table_(self, request):
        return instance_mock(request, Table)


class Describe_Row(object):

    def it_knows_its_height(self, height_get_fixture):
        row, expected_height = height_get_fixture
        assert row.height == expected_height

    def it_can_change_its_height(self, height_set_fixture):
        row, value, expected_xml = height_set_fixture
        row.height = value
        assert row._tr.xml == expected_xml

    def it_knows_its_height_rule(self, height_rule_get_fixture):
        row, expected_rule = height_rule_get_fixture
        assert row.height_rule == expected_rule

    def it_can_change_its_height_rule(self, height_rule_set_fixture):
        row, rule, expected_xml = height_rule_set_fixture
        row.height_rule = rule
        assert row._tr.xml == expected_xml

    def it_provides_access_to_its_cells(self, cells_fixture):
        row, row_idx, expected_cells = cells_fixture
        cells = row.cells
        row.table.row_cells.assert_called_once_with(row_idx)
        assert cells == expected_cells

    def it_provides_access_to_the_table_it_belongs_to(self, table_fixture):
        row, table_ = table_fixture
        assert row.table is table_

    def it_knows_its_index_in_table_to_help(self, idx_fixture):
        row, expected_idx = idx_fixture
        assert row._index == expected_idx

    # fixtures -------------------------------------------------------

    @pytest.fixture
    def cells_fixture(self, _index_, table_prop_, table_):
        row = _Row(None, None)
        _index_.return_value = row_idx = 6
        expected_cells = (1, 2, 3)
        table_.row_cells.return_value = list(expected_cells)
        return row, row_idx, expected_cells

    @pytest.fixture(params=[
        ('w:tr',                               None),
        ('w:tr/w:trPr',                        None),
        ('w:tr/w:trPr/w:trHeight',             None),
        ('w:tr/w:trPr/w:trHeight{w:val=0}',    0),
        ('w:tr/w:trPr/w:trHeight{w:val=1440}', 914400),
    ])
    def height_get_fixture(self, request):
        tr_cxml, expected_height = request.param
        row = _Row(element(tr_cxml), None)
        return row, expected_height

    @pytest.fixture(params=[
        ('w:tr',                               Inches(1),
         'w:tr/w:trPr/w:trHeight{w:val=1440}'),
        ('w:tr/w:trPr',                        Inches(1),
         'w:tr/w:trPr/w:trHeight{w:val=1440}'),
        ('w:tr/w:trPr/w:trHeight',             Inches(1),
         'w:tr/w:trPr/w:trHeight{w:val=1440}'),
        ('w:tr/w:trPr/w:trHeight{w:val=1440}', Inches(2),
         'w:tr/w:trPr/w:trHeight{w:val=2880}'),
        ('w:tr/w:trPr/w:trHeight{w:val=2880}', None,
         'w:tr/w:trPr/w:trHeight'),
        ('w:tr',                   None, 'w:tr/w:trPr'),
        ('w:tr/w:trPr',            None, 'w:tr/w:trPr'),
        ('w:tr/w:trPr/w:trHeight', None, 'w:tr/w:trPr/w:trHeight'),
    ])
    def height_set_fixture(self, request):
        tr_cxml, new_value, expected_cxml = request.param
        row = _Row(element(tr_cxml), None)
        expected_xml = xml(expected_cxml)
        return row, new_value, expected_xml

    @pytest.fixture(params=[
        ('w:tr',        None),
        ('w:tr/w:trPr', None),
        ('w:tr/w:trPr/w:trHeight{w:val=0, w:hRule=auto}',
         WD_ROW_HEIGHT.AUTO),
        ('w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=atLeast}',
         WD_ROW_HEIGHT.AT_LEAST),
        ('w:tr/w:trPr/w:trHeight{w:val=2880, w:hRule=exact}',
         WD_ROW_HEIGHT.EXACTLY),
    ])
    def height_rule_get_fixture(self, request):
        tr_cxml, expected_rule = request.param
        row = _Row(element(tr_cxml), None)
        return row, expected_rule

    @pytest.fixture(params=[
        ('w:tr',
         WD_ROW_HEIGHT.AUTO,
         'w:tr/w:trPr/w:trHeight{w:hRule=auto}'),
        ('w:tr/w:trPr',
         WD_ROW_HEIGHT.AT_LEAST,
         'w:tr/w:trPr/w:trHeight{w:hRule=atLeast}'),
        ('w:tr/w:trPr/w:trHeight',
         WD_ROW_HEIGHT.EXACTLY,
         'w:tr/w:trPr/w:trHeight{w:hRule=exact}'),
        ('w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=exact}',
         WD_ROW_HEIGHT.AUTO,
         'w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=auto}'),
        ('w:tr/w:trPr/w:trHeight{w:val=1440, w:hRule=auto}',
         None,
         'w:tr/w:trPr/w:trHeight{w:val=1440}'),
        ('w:tr',                   None, 'w:tr/w:trPr'),
        ('w:tr/w:trPr',            None, 'w:tr/w:trPr'),
        ('w:tr/w:trPr/w:trHeight', None, 'w:tr/w:trPr/w:trHeight'),
    ])
    def height_rule_set_fixture(self, request):
        tr_cxml, new_rule, expected_cxml = request.param
        row = _Row(element(tr_cxml), None)
        expected_xml = xml(expected_cxml)
        return row, new_rule, expected_xml

    @pytest.fixture
    def idx_fixture(self):
        tbl = element('w:tbl/(w:tr,w:tr,w:tr)')
        tr, expected_idx = tbl[1], 1
        row = _Row(tr, None)
        return row, expected_idx

    @pytest.fixture
    def table_fixture(self, parent_, table_):
        row = _Row(None, parent_)
        parent_.table = table_
        return row, table_

    # fixture components ---------------------------------------------

    @pytest.fixture
    def _index_(self, request):
        return property_mock(request, _Row, '_index')

    @pytest.fixture
    def parent_(self, request):
        return instance_mock(request, Table)

    @pytest.fixture
    def table_(self, request):
        return instance_mock(request, Table)

    @pytest.fixture
    def table_prop_(self, request, table_):
        return property_mock(request, _Row, 'table', return_value=table_)


class Describe_Rows(object):

    def it_knows_how_many_rows_it_contains(self, rows_fixture):
        rows, row_count = rows_fixture
        assert len(rows) == row_count

    def it_can_iterate_over_its__Row_instances(self, rows_fixture):
        rows, row_count = rows_fixture
        actual_count = 0
        for row in rows:
            assert isinstance(row, _Row)
            actual_count += 1
        assert actual_count == row_count

    def it_provides_indexed_access_to_rows(self, rows_fixture):
        rows, row_count = rows_fixture
        for idx in range(-row_count, row_count):
            row = rows[idx]
            assert isinstance(row, _Row)

    def it_provides_sliced_access_to_rows(self, slice_fixture):
        rows, start, end, expected_count = slice_fixture
        slice_of_rows = rows[start:end]
        assert len(slice_of_rows) == expected_count
        tr_lst = rows._tbl.tr_lst
        for idx, row in enumerate(slice_of_rows):
            assert tr_lst.index(row._tr) == start + idx
            assert isinstance(row, _Row)

    def it_raises_on_indexed_access_out_of_range(self, rows_fixture):
        rows, row_count = rows_fixture
        with pytest.raises(IndexError):
            too_low = -1 - row_count
            rows[too_low]
        with pytest.raises(IndexError):
            too_high = row_count
            rows[too_high]

    def it_provides_access_to_the_table_it_belongs_to(self, table_fixture):
        rows, table_ = table_fixture
        assert rows.table is table_

    # fixtures -------------------------------------------------------

    @pytest.fixture
    def rows_fixture(self):
        row_count = 2
        tbl = _tbl_bldr(rows=row_count, cols=2).element
        rows = _Rows(tbl, None)
        return rows, row_count

    @pytest.fixture(params=[
        (3, 1,  3, 2),
        (3, 0, -1, 2),
    ])
    def slice_fixture(self, request):
        row_count, start, end, expected_count = request.param
        tbl = _tbl_bldr(rows=row_count, cols=2).element
        rows = _Rows(tbl, None)
        return rows, start, end, expected_count

    @pytest.fixture
    def table_fixture(self, table_):
        rows = _Rows(None, table_)
        table_.table = table_
        return rows, table_

    # fixture components ---------------------------------------------

    @pytest.fixture
    def table_(self, request):
        return instance_mock(request, Table)


# fixtures -----------------------------------------------------------

def _tbl_bldr(rows, cols):
    tblGrid_bldr = a_tblGrid()
    for i in range(cols):
        tblGrid_bldr.with_child(a_gridCol())
    tbl_bldr = a_tbl().with_nsdecls().with_child(tblGrid_bldr)
    for i in range(rows):
        tr_bldr = _tr_bldr(cols)
        tbl_bldr.with_child(tr_bldr)
    return tbl_bldr


def _tc_bldr():
    return a_tc().with_child(a_p())


def _tr_bldr(cols):
    tr_bldr = a_tr()
    for i in range(cols):
        tc_bldr = _tc_bldr()
        tr_bldr.with_child(tc_bldr)
    return tr_bldr
