# encoding: utf-8

"""Unit-test suite for `pptx.table` module."""

import pytest

from pptx.dml.fill import FillFormat
from pptx.dml.border import BorderFormat
from pptx.enum.text import MSO_ANCHOR
from pptx.oxml.ns import qn
from pptx.oxml.table import CT_Table, CT_TableCell, TcRange
from pptx.shapes.graphfrm import GraphicFrame
from pptx.table import (
    _Cell,
    _CellCollection,
    _Column,
    _ColumnCollection,
    _Row,
    _RowCollection,
    Table,
)
from pptx.text.text import TextFrame
from pptx.util import Inches, Length, Pt

from .unitutil.cxml import element, xml
from .unitutil.mock import call, class_mock, instance_mock, property_mock


class DescribeTable(object):
    """Unit-test suite for `pptx.table.Table` objects."""

    def it_provides_access_to_its_cells(self, tbl_, tc_, _Cell_, cell_):
        row_idx, col_idx = 4, 2
        tbl_.tc.return_value = tc_
        _Cell_.return_value = cell_
        table = Table(tbl_, None)

        cell = table.cell(row_idx, col_idx)

        tbl_.tc.assert_called_once_with(row_idx, col_idx)
        _Cell_.assert_called_once_with(tc_, table)
        assert cell is cell_

    def it_provides_access_to_its_columns(self, request):
        columns_ = instance_mock(request, _ColumnCollection)
        _ColumnCollection_ = class_mock(
            request, "pptx.table._ColumnCollection", return_value=columns_
        )
        tbl = element("a:tbl")
        table = Table(tbl, None)

        columns = table.columns

        _ColumnCollection_.assert_called_once_with(tbl, table)
        assert columns is columns_

    def it_can_iterate_its_grid_cells(self, request, _Cell_):
        tbl = element("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))")
        expected_tcs = tbl.xpath(".//a:tc")
        expected_cells = _Cell_.side_effect = [
            instance_mock(request, _Cell, name="cell%d" % idx) for idx in range(4)
        ]
        table = Table(tbl, None)

        cells = list(table.iter_cells())

        assert cells == expected_cells
        assert _Cell_.call_args_list == [call(tc, table) for tc in expected_tcs]

    def it_provides_access_to_its_rows(self, request):
        rows_ = instance_mock(request, _RowCollection)
        _RowCollection_ = class_mock(
            request, "pptx.table._RowCollection", return_value=rows_
        )
        tbl = element("a:tbl")
        table = Table(tbl, None)

        rows = table.rows

        _RowCollection_.assert_called_once_with(tbl, table)
        assert rows is rows_

    def it_updates_graphic_frame_width_on_width_change(self, dx_fixture):
        table, expected_width = dx_fixture
        table.notify_width_changed()
        assert table._graphic_frame.width == expected_width

    def it_updates_graphic_frame_height_on_height_change(self, dy_fixture):
        table, expected_height = dy_fixture
        table.notify_height_changed()
        assert table._graphic_frame.height == expected_height

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

    @pytest.fixture
    def dx_fixture(self, graphic_frame_):
        tbl_cxml = "a:tbl/a:tblGrid/(a:gridCol{w=111},a:gridCol{w=222})"
        table = Table(element(tbl_cxml), graphic_frame_)
        expected_width = 333
        return table, expected_width

    @pytest.fixture
    def dy_fixture(self, graphic_frame_):
        tbl_cxml = "a:tbl/(a:tr{h=100},a:tr{h=200})"
        table = Table(element(tbl_cxml), graphic_frame_)
        expected_height = 300
        return table, expected_height

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

    @pytest.fixture
    def _Cell_(self, request):
        return class_mock(request, "pptx.table._Cell")

    @pytest.fixture
    def cell_(self, request):
        return instance_mock(request, _Cell)

    @pytest.fixture
    def graphic_frame_(self, request):
        return instance_mock(request, GraphicFrame)

    @pytest.fixture
    def tbl_(self, request):
        return instance_mock(request, CT_Table)

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


class DescribeTableBooleanProperties(object):
    def it_knows_its_boolean_property_settings(self, boolprop_get_fixture):
        table, boolprop_name, expected_value = boolprop_get_fixture
        boolprop_value = getattr(table, boolprop_name)
        assert boolprop_value is expected_value

    def it_can_change_its_boolean_property_settings(self, boolprop_set_fixture):
        table, boolprop_name, new_value, expected_xml = boolprop_set_fixture
        setattr(table, boolprop_name, new_value)
        assert table._tbl.xml == expected_xml

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

    @pytest.fixture(
        params=[
            ("a:tbl", "first_row", False),
            ("a:tbl/a:tblPr", "first_row", False),
            ("a:tbl/a:tblPr{firstRow=1}", "first_row", True),
            ("a:tbl/a:tblPr{firstRow=0}", "first_row", False),
            ("a:tbl/a:tblPr{firstRow=true}", "first_row", True),
            ("a:tbl/a:tblPr{firstRow=false}", "first_row", False),
            ("a:tbl/a:tblPr{firstCol=1}", "first_col", True),
            ("a:tbl/a:tblPr{lastRow=0}", "last_row", False),
            ("a:tbl/a:tblPr{lastCol=true}", "last_col", True),
            ("a:tbl/a:tblPr{bandRow=false}", "horz_banding", False),
            ("a:tbl/a:tblPr", "vert_banding", False),
        ]
    )
    def boolprop_get_fixture(self, request):
        tbl_cxml, boolprop_name, expected_value = request.param
        table = Table(element(tbl_cxml), None)
        return table, boolprop_name, expected_value

    @pytest.fixture(
        params=[
            ("a:tbl", "first_row", True, "a:tbl/a:tblPr{firstRow=1}"),
            ("a:tbl", "first_row", False, "a:tbl/a:tblPr"),
            ("a:tbl/a:tblPr", "first_row", True, "a:tbl/a:tblPr{firstRow=1}"),
            ("a:tbl/a:tblPr", "first_row", False, "a:tbl/a:tblPr"),
            (
                "a:tbl/a:tblPr{firstRow=true}",
                "first_row",
                True,
                "a:tbl/a:tblPr{firstRow=1}",
            ),
            ("a:tbl/a:tblPr{firstRow=false}", "first_row", False, "a:tbl/a:tblPr"),
            (
                "a:tbl/a:tblPr{bandRow=1}",
                "first_row",
                True,
                "a:tbl/a:tblPr{bandRow=1,firstRow=1}",
            ),
            ("a:tbl", "first_col", True, "a:tbl/a:tblPr{firstCol=1}"),
            ("a:tbl", "last_row", True, "a:tbl/a:tblPr{lastRow=1}"),
            ("a:tbl", "last_col", True, "a:tbl/a:tblPr{lastCol=1}"),
            ("a:tbl", "horz_banding", True, "a:tbl/a:tblPr{bandRow=1}"),
            ("a:tbl", "vert_banding", True, "a:tbl/a:tblPr{bandCol=1}"),
        ]
    )
    def boolprop_set_fixture(self, request):
        tbl_cxml, boolprop_name, new_value, expected_tbl_cxml = request.param
        table = Table(element(tbl_cxml), None)
        expected_xml = xml(expected_tbl_cxml)
        return table, boolprop_name, new_value, expected_xml


class Describe_Cell(object):
    """Unit-test suite for `pptx.table._Cell` object."""

    def it_is_equal_to_other_instance_having_same_tc(self):
        tc = element("a:tc")
        other_tc = element("a:tc")
        cell = _Cell(tc, None)
        cell_with_same_tc = _Cell(tc, None)
        cell_with_other_tc = _Cell(other_tc, None)

        assert cell == cell_with_same_tc
        assert cell != cell_with_other_tc

    def it_has_a_fill(self, fill_fixture):
        cell = fill_fixture
        assert isinstance(cell.fill, FillFormat)

    def it_knows_whether_it_is_merge_origin_cell(self, origin_fixture):
        tc, expected_value = origin_fixture
        cell = _Cell(tc, None)

        is_merge_origin = cell.is_merge_origin

        assert is_merge_origin is expected_value

    def it_knows_whether_it_is_spanned(self, spanned_fixture):
        tc, expected_value = spanned_fixture
        cell = _Cell(tc, None)

        is_spanned = cell.is_spanned

        assert is_spanned is expected_value

    def it_knows_its_margin_settings(self, margin_get_fixture):
        cell, margin_prop_name, expected_value = margin_get_fixture
        margin_value = getattr(cell, margin_prop_name)
        assert margin_value == expected_value

    def it_can_change_its_margin_settings(self, margin_set_fixture):
        cell, margin_prop_name, new_value, expected_xml = margin_set_fixture
        setattr(cell, margin_prop_name, new_value)
        assert cell._tc.xml == expected_xml

    def it_raises_on_margin_assigned_other_than_int_or_None(
        self, margin_raises_fixture
    ):
        cell, margin_attr_name, val_of_invalid_type = margin_raises_fixture
        with pytest.raises(TypeError):
            setattr(cell, margin_attr_name, val_of_invalid_type)

    def it_can_merge_a_range_of_cells(self, TcRange_, tc_range_):
        tbl = element("a:tbl/(a:tr/(a:tc,a:tc),a:tr/(a:tc,a:tc))")
        tc, other_tc = tbl.tc(0, 0), tbl.tc(1, 1)
        TcRange_.return_value = tc_range_
        tc_range_.contains_merged_cell = False
        tc_range_.dimensions = 2, 2

        def tcs(*rowcols):
            return (tbl.tc(*rowcol) for rowcol in rowcols)

        tc_range_.iter_top_row_tcs.return_value = tcs((0, 0), (0, 1))
        tc_range_.iter_left_col_tcs.return_value = tcs((0, 0), (1, 0))
        tc_range_.iter_except_left_col_tcs.return_value = tcs((0, 1), (1, 1))
        tc_range_.iter_except_top_row_tcs.return_value = tcs((1, 0), (1, 1))
        expected_xml = xml(
            "a:tbl/(a:tr/(a:tc{gridSpan=2,rowSpan=2},a:tc{rowSpan=2,hMerge=1"
            "}),a:tr/(a:tc{gridSpan=2,vMerge=1},a:tc{hMerge=1,vMerge=1}))"
        )
        cell, other_cell = _Cell(tc, None), _Cell(other_tc, None)

        cell.merge(other_cell)

        TcRange_.assert_called_once_with(tc, other_tc)
        tc_range_.move_content_to_origin.assert_called_once_with()
        assert tbl.xml == expected_xml

    def but_it_raises_when_cells_are_from_different_tables(self, TcRange_, tc_range_):
        TcRange_.return_value = tc_range_
        tc_range_.in_same_table = False
        cell, other_cell = _Cell(None, None), _Cell(None, None)

        with pytest.raises(ValueError) as e:
            cell.merge(other_cell)
        assert "different table" in str(e.value)

    def and_it_raises_when_range_contains_merged_cell(self, TcRange_, tc_range_):
        TcRange_.return_value = tc_range_
        tc_range_.contains_merged_cell = True
        cell, other_cell = _Cell(None, None), _Cell(None, None)

        with pytest.raises(ValueError) as e:
            cell.merge(other_cell)
        assert "contains one or more merged cells" in str(e.value)

    def it_knows_how_many_rows_the_merge_spans(self, height_fixture):
        tc, expected_value = height_fixture
        cell = _Cell(tc, None)
        span_height = cell.span_height
        assert span_height == expected_value

    def it_knows_how_many_columns_the_merge_spans(self, width_fixture):
        tc, expected_value = width_fixture
        cell = _Cell(tc, None)
        span_width = cell.span_width
        assert span_width == expected_value

    def it_can_split_a_merged_cell(self, split_fixture):
        origin_tc, range_tcs = split_fixture
        cell = _Cell(origin_tc, None)

        cell.split()

        assert all(tc.gridSpan == 1 for tc in range_tcs)
        assert all(tc.rowSpan == 1 for tc in range_tcs)
        assert all(not tc.hMerge for tc in range_tcs)
        assert all(not tc.vMerge for tc in range_tcs)

    def but_it_raises_when_cell_to_be_split_is_not_merge_origin(self):
        tc = element("a:tbl/a:tr/a:tc").xpath("//a:tc")[0]
        cell = _Cell(tc, None)

        with pytest.raises(ValueError) as e:
            cell.split()
        assert "not a merge-origin cell" in str(e.value)

    def it_knows_what_text_it_contains(self, text_frame_prop_, text_frame_):
        text_frame_prop_.return_value = text_frame_
        text_frame_.text = "foobar"
        cell = _Cell(None, None)

        text = cell.text

        assert text == "foobar"

    def it_can_change_its_text(self, text_frame_prop_, text_frame_):
        text_frame_prop_.return_value = text_frame_
        cell = _Cell(None, None)

        cell.text = "føøbår"

        assert text_frame_.text == "føøbår"

    def it_knows_its_vertical_anchor_setting(self, anchor_get_fixture):
        cell, expected_value = anchor_get_fixture
        assert cell.vertical_anchor == expected_value

    def it_can_change_its_vertical_anchor(self, anchor_set_fixture):
        cell, new_value, expected_xml = anchor_set_fixture
        cell.vertical_anchor = new_value
        assert cell._tc.xml == expected_xml

    def it_knows_it_has_border_settings(self, border_fixture):
        cell = border_fixture
        assert isinstance(cell.border_left, BorderFormat)
        assert isinstance(cell.border_right, BorderFormat)
        assert isinstance(cell.border_top, BorderFormat)
        assert isinstance(cell.border_bottom, BorderFormat)
        assert isinstance(cell.border_tl_br, BorderFormat)
        assert isinstance(cell.border_bl_tr, BorderFormat)
        
    # fixtures -------------------------------------------------------

    @pytest.fixture(
        params=[
            ("a:tc", None),
            ("a:tc/a:tcPr", None),
            ("a:tc/a:tcPr{anchor=t}", MSO_ANCHOR.TOP),
            ("a:tc/a:tcPr{anchor=ctr}", MSO_ANCHOR.MIDDLE),
            ("a:tc/a:tcPr{anchor=b}", MSO_ANCHOR.BOTTOM),
        ]
    )
    def anchor_get_fixture(self, request):
        tc_cxml, expected_value = request.param
        cell = _Cell(element(tc_cxml), None)
        return cell, expected_value

    @pytest.fixture(
        params=[
            ("a:tc", None, "a:tc"),
            ("a:tc", MSO_ANCHOR.TOP, "a:tc/a:tcPr{anchor=t}"),
            ("a:tc", MSO_ANCHOR.MIDDLE, "a:tc/a:tcPr{anchor=ctr}"),
            ("a:tc", MSO_ANCHOR.BOTTOM, "a:tc/a:tcPr{anchor=b}"),
            ("a:tc/a:tcPr{anchor=t}", MSO_ANCHOR.MIDDLE, "a:tc/a:tcPr{anchor=ctr}"),
            ("a:tc/a:tcPr{anchor=ctr}", None, "a:tc/a:tcPr"),
        ]
    )
    def anchor_set_fixture(self, request):
        tc_cxml, new_value, expected_tc_cxml = request.param
        cell = _Cell(element(tc_cxml), None)
        expected_xml = xml(expected_tc_cxml)
        return cell, new_value, expected_xml

    @pytest.fixture
    def fill_fixture(self, cell):
        return cell

    @pytest.fixture
    def border_fixture(self, cell):
        return cell

    @pytest.fixture(
        params=[("a:tc", 1), ("a:tc{gridSpan=2}", 1), ("a:tc{rowSpan=42}", 42)]
    )
    def height_fixture(self, request):
        tc_cxml, expected_value = request.param
        tc = element(tc_cxml)
        return tc, expected_value

    @pytest.fixture(
        params=[
            ("a:tc/a:tcPr{marL=82296}", "margin_left", Inches(0.09)),
            ("a:tc/a:tcPr{marR=73152}", "margin_right", Inches(0.08)),
            ("a:tc/a:tcPr{marT=64008}", "margin_top", Inches(0.07)),
            ("a:tc/a:tcPr{marB=54864}", "margin_bottom", Inches(0.06)),
            ("a:tc", "margin_left", Inches(0.1)),
            ("a:tc/a:tcPr", "margin_right", Inches(0.1)),
            ("a:tc", "margin_top", Inches(0.05)),
            ("a:tc/a:tcPr", "margin_bottom", Inches(0.05)),
        ]
    )
    def margin_get_fixture(self, request):
        tc_cxml, margin_prop_name, expected_value = request.param
        cell = _Cell(element(tc_cxml), None)
        return cell, margin_prop_name, expected_value

    @pytest.fixture(
        params=[
            ("a:tc", "margin_left", Inches(0.08), "a:tc/a:tcPr{marL=73152}"),
            ("a:tc", "margin_right", Inches(0.08), "a:tc/a:tcPr{marR=73152}"),
            ("a:tc", "margin_top", Inches(0.08), "a:tc/a:tcPr{marT=73152}"),
            ("a:tc", "margin_bottom", Inches(0.08), "a:tc/a:tcPr{marB=73152}"),
            ("a:tc", "margin_left", None, "a:tc"),
            ("a:tc/a:tcPr{marL=42}", "margin_left", None, "a:tc/a:tcPr"),
        ]
    )
    def margin_set_fixture(self, request):
        tc_cxml, margin_prop_name, new_value, expected_tc_cxml = request.param
        cell = _Cell(element(tc_cxml), None)
        expected_xml = xml(expected_tc_cxml)
        return cell, margin_prop_name, new_value, expected_xml

    @pytest.fixture(
        params=["margin_left", "margin_right", "margin_top", "margin_bottom"]
    )
    def margin_raises_fixture(self, request):
        margin_prop_name = request.param
        cell = _Cell(element("a:tc"), None)
        val_of_invalid_type = "foobar"
        return cell, margin_prop_name, val_of_invalid_type

    @pytest.fixture(
        params=[
            ("a:tc", False),
            ("a:tc{gridSpan=1}", False),
            ("a:tc{hMerge=1}", False),
            ("a:tc{gridSpan=2,vMerge=1}", False),
            ("a:tc{gridSpan=2}", True),
            ("a:tc{rowSpan=2}", True),
            ("a:tc{gridSpan=2,rowSpan=3}", True),
        ]
    )
    def origin_fixture(self, request):
        tc_cxml, expected_value = request.param
        tc = element(tc_cxml)
        return tc, expected_value

    @pytest.fixture(
        params=[
            ("a:tc", False),
            ("a:tc{gridSpan=2}", False),
            ("a:tc{hMerge=1}", True),
            ("a:tc{gridSpan=2,vMerge=1}", True),
            ("a:tc{rowSpan=2,hMerge=true}", True),
            ("a:tc{gridSpan=2,rowSpan=3}", False),
        ]
    )
    def spanned_fixture(self, request):
        tc_cxml, expected_value = request.param
        tc = element(tc_cxml)
        return tc, expected_value

    @pytest.fixture(
        params=[
            (
                "a:tbl/(a:tr/(a:tc{gridSpan=2},a:tc{hMerge=1}),a:tr/(a:tc,a:tc))",
                0,
                [0, 1],
            ),
            (
                "a:tbl/(a:tr/(a:tc{rowSpan=2},a:tc),a:tr/(a:tc{vMerge=1},a:tc))",
                0,
                [0, 2],
            ),
            (
                "a:tbl/(a:tr/(a:tc{gridSpan=2,rowSpan=2},a:tc{hMerge=1,rowSpan=2}),"
                "a:tr/(a:tc{gridSpan=2,vMerge=1},a:tc{hMerge=1,vMerge=1}))",
                0,
                [0, 1, 2, 3],
            ),
        ]
    )
    def split_fixture(self, request):
        tbl_cxml, origin_tc_idx, range_tc_idxs = request.param
        tcs = element(tbl_cxml).xpath("//a:tc")
        origin_tc = tcs[origin_tc_idx]
        range_tcs = tuple(tcs[idx] for idx in range_tc_idxs)
        return origin_tc, range_tcs

    @pytest.fixture(
        params=[("a:tc", 1), ("a:tc{rowSpan=2}", 1), ("a:tc{gridSpan=24}", 24)]
    )
    def width_fixture(self, request):
        tc_cxml, expected_value = request.param
        tc = element(tc_cxml)
        return tc, expected_value

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

    @pytest.fixture
    def cell(self):
        return _Cell(element("a:tc"), None)

    @pytest.fixture
    def TcRange_(self, request):
        return class_mock(request, "pptx.table.TcRange")

    @pytest.fixture
    def tc_range_(self, request):
        return instance_mock(request, TcRange)

    @pytest.fixture
    def text_frame_(self, request):
        return instance_mock(request, TextFrame)

    @pytest.fixture
    def text_frame_prop_(self, request):
        return property_mock(request, _Cell, "text_frame")


class Describe_CellCollection(object):
    def it_knows_how_many_cells_it_contains(self, len_fixture):
        cells, expected_count = len_fixture
        assert len(cells) == expected_count

    def it_can_iterate_over_the_cells_it_contains(self, iter_fixture):
        cell_collection, _Cell_, calls, expected_cells = iter_fixture

        cells = list(cell_collection)

        assert _Cell_.call_args_list == calls
        assert cells == expected_cells

    def it_supports_indexed_access(self, _Cell_, cell_):
        tr = element("a:tr/(a:tc, a:tc, a:tc)")
        tcs = tr.xpath("//a:tc")
        _Cell_.return_value = cell_
        cell_collection = _CellCollection(tr, None)

        cell = cell_collection[1]

        _Cell_.assert_called_once_with(tcs[1], cell_collection)
        assert cell is cell_

    def it_raises_on_indexed_access_out_of_range(self):
        cells = _CellCollection(element("a:tr/a:tc"), None)
        with pytest.raises(IndexError):
            cells[-1]
        with pytest.raises(IndexError):
            cells[9]

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

    @pytest.fixture(params=["a:tr", "a:tr/a:tc", "a:tr/(a:tc, a:tc, a:tc)"])
    def iter_fixture(self, request, _Cell_):
        tr_cxml = request.param
        tr = element(tr_cxml)
        tcs = tr.xpath("//a:tc")
        cell_collection = _CellCollection(tr, None)

        expected_cells = [
            instance_mock(request, _Cell, name="cell%d" % idx)
            for idx in range(len(tcs))
        ]
        _Cell_.side_effect = expected_cells
        calls = [call(tc, cell_collection) for tc in tcs]

        return cell_collection, _Cell_, calls, expected_cells

    @pytest.fixture(params=[("a:tr", 0), ("a:tr/a:tc", 1), ("a:tr/(a:tc, a:tc)", 2)])
    def len_fixture(self, request):
        tr_cxml, expected_len = request.param
        cells = _CellCollection(element(tr_cxml), None)
        return cells, expected_len

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

    @pytest.fixture
    def _Cell_(self, request):
        return class_mock(request, "pptx.table._Cell")

    @pytest.fixture
    def cell_(self, request):
        return instance_mock(request, _Cell)


class Describe_Column(object):
    def it_knows_its_width(self, width_get_fixture):
        column, expected_value = width_get_fixture
        width = column.width
        assert width == expected_value
        assert isinstance(width, Length)

    def it_can_change_its_width(self, width_set_fixture):
        column, new_width, expected_xml, parent_ = width_set_fixture
        column.width = new_width
        assert column._gridCol.xml == expected_xml
        parent_.notify_width_changed.assert_called_once_with()

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

    @pytest.fixture(
        params=[("a:gridCol{w=914400}", Inches(1)), ("a:gridCol{w=10pt}", Pt(10))]
    )
    def width_get_fixture(self, request):
        gridCol_cxml, expected_value = request.param
        column = _Column(element(gridCol_cxml), None)
        return column, expected_value

    @pytest.fixture(
        params=[
            ("a:gridCol{w=12pt}", Inches(1), "a:gridCol{w=914400}"),
            ("a:gridCol{w=1234}", Inches(1), "a:gridCol{w=914400}"),
        ]
    )
    def width_set_fixture(self, request, parent_):
        gridCol_cxml, new_width, expected_gridCol_cxml = request.param
        column = _Column(element(gridCol_cxml), parent_)
        expected_xml = xml(expected_gridCol_cxml)
        return column, new_width, expected_xml, parent_

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

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


class Describe_ColumnCollection(object):
    def it_knows_how_many_columns_it_contains(self, len_fixture):
        columns, expected_count = len_fixture
        assert len(columns) == expected_count

    def it_can_iterate_over_the_columns_it_contains(self, iter_fixture):
        columns, expected_gridCol_lst = iter_fixture
        count = 0
        for idx, column in enumerate(columns):
            assert isinstance(column, _Column)
            assert column._gridCol is expected_gridCol_lst[idx]
            count += 1
        assert count == len(expected_gridCol_lst)

    def it_supports_indexed_access(self, getitem_fixture):
        columns, expected_gridCol_lst = getitem_fixture
        for idx, gridCol in enumerate(expected_gridCol_lst):
            column = columns[idx]
            assert isinstance(column, _Column)
            assert column._gridCol is gridCol

    def it_raises_on_indexed_access_out_of_range(self):
        columns = _ColumnCollection(element("a:tbl/a:tblGrid/a:gridCol"), None)
        with pytest.raises(IndexError):
            columns[-1]
        with pytest.raises(IndexError):
            columns[9]

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

    @pytest.fixture(
        params=[
            "a:tbl/a:tblGrid",
            "a:tbl/a:tblGrid/a:gridCol",
            "a:tbl/a:tblGrid/(a:gridCol, a:gridCol, a:gridCol)",
        ]
    )
    def getitem_fixture(self, request):
        tbl_cxml = request.param
        tbl = element(tbl_cxml)
        columns = _ColumnCollection(tbl, None)
        expected_column_lst = tbl.xpath("//a:gridCol")
        return columns, expected_column_lst

    @pytest.fixture(
        params=[
            "a:tbl/a:tblGrid",
            "a:tbl/a:tblGrid/a:gridCol",
            "a:tbl/a:tblGrid/(a:gridCol, a:gridCol, a:gridCol)",
        ]
    )
    def iter_fixture(self, request):
        tbl_cxml = request.param
        tbl = element(tbl_cxml)
        columns = _ColumnCollection(tbl, None)
        expected_column_lst = tbl.xpath("//a:gridCol")
        return columns, expected_column_lst

    @pytest.fixture(
        params=[
            ("a:tbl/a:tblGrid", 0),
            ("a:tbl/a:tblGrid/a:gridCol", 1),
            ("a:tbl/a:tblGrid/(a:gridCol,a:gridCol)", 2),
        ]
    )
    def len_fixture(self, request):
        tbl_cxml, expected_len = request.param
        columns = _ColumnCollection(element(tbl_cxml), None)
        return columns, expected_len


class Describe_Row(object):
    def it_knows_its_height(self, height_get_fixture):
        row, expected_value = height_get_fixture
        height = row.height
        assert height == expected_value
        assert isinstance(height, Length)

    def it_can_change_its_height(self, height_set_fixture):
        row, new_height, expected_xml, parent_ = height_set_fixture
        row.height = new_height
        assert row._tr.xml == expected_xml
        parent_.notify_height_changed.assert_called_once_with()

    def it_provides_access_to_its_cells(self, cells_fixture):
        row, _CellCollection_, cells_ = cells_fixture
        cells = row.cells
        _CellCollection_.assert_called_once_with(row._tr, row)
        assert cells is cells_

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

    @pytest.fixture
    def cells_fixture(self, _CellCollection_, cells_):
        row = _Row(element("a:tr"), None)
        return row, _CellCollection_, cells_

    @pytest.fixture(params=[("a:tr{h=914400}", Inches(1)), ("a:tr{h=10pt}", Pt(10))])
    def height_get_fixture(self, request):
        tr_cxml, expected_value = request.param
        row = _Row(element(tr_cxml), None)
        return row, expected_value

    @pytest.fixture(
        params=[
            ("a:tr{h=12pt}", Inches(1), "a:tr{h=914400}"),
            ("a:tr{h=1234}", Inches(1), "a:tr{h=914400}"),
        ]
    )
    def height_set_fixture(self, request, parent_):
        tr_cxml, new_height, expected_tr_cxml = request.param
        row = _Row(element(tr_cxml), parent_)
        expected_xml = xml(expected_tr_cxml)
        return row, new_height, expected_xml, parent_

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

    @pytest.fixture
    def _CellCollection_(self, request, cells_):
        return class_mock(request, "pptx.table._CellCollection", return_value=cells_)

    @pytest.fixture
    def cells_(self, request):
        return instance_mock(request, _CellCollection)

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


class Describe_RowCollection(object):
    def it_knows_how_many_rows_it_contains(self, len_fixture):
        rows, expected_count = len_fixture
        assert len(rows) == expected_count

    def it_can_iterate_over_the_rows_it_contains(self, iter_fixture):
        rows, expected_tr_lst = iter_fixture
        count = 0
        for idx, row in enumerate(rows):
            assert isinstance(row, _Row)
            assert row._tr is expected_tr_lst[idx]
            count += 1
        assert count == len(expected_tr_lst)

    def it_supports_indexed_access(self, getitem_fixture):
        rows, expected_tr_lst = getitem_fixture
        for idx, tr in enumerate(expected_tr_lst):
            row = rows[idx]
            assert isinstance(row, _Row)
            assert row._tr is tr

    def it_raises_on_indexed_access_out_of_range(self):
        rows = _RowCollection(element("a:tbl/a:tr"), None)
        with pytest.raises(IndexError):
            rows[-1]
        with pytest.raises(IndexError):
            rows[9]

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

    @pytest.fixture(params=["a:tbl", "a:tbl/a:tr", "a:tbl/(a:tr, a:tr, a:tr)"])
    def getitem_fixture(self, request):
        tbl_cxml = request.param
        tbl = element(tbl_cxml)
        rows = _RowCollection(tbl, None)
        expected_row_lst = tbl.findall(qn("a:tr"))
        return rows, expected_row_lst

    @pytest.fixture(params=["a:tbl", "a:tbl/a:tr", "a:tbl/(a:tr, a:tr, a:tr)"])
    def iter_fixture(self, request):
        tbl_cxml = request.param
        tbl = element(tbl_cxml)
        rows = _RowCollection(tbl, None)
        expected_row_lst = tbl.findall(qn("a:tr"))
        return rows, expected_row_lst

    @pytest.fixture(params=[("a:tbl", 0), ("a:tbl/a:tr", 1), ("a:tbl/(a:tr, a:tr)", 2)])
    def len_fixture(self, request):
        tbl_cxml, expected_len = request.param
        rows = _RowCollection(element(tbl_cxml), None)
        return rows, expected_len

[((145, 5, 159, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((165, 5, 190, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((360, 5, 368, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((374, 5, 383, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((398, 5, 400, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((439, 5, 441, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((448, 5, 458, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((464, 5, 473, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((479, 5, 498, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((506, 5, 508, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((570, 5, 570, 76), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((586, 5, 586, 85), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((675, 5, 681, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((689, 5, 695, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((703, 5, 709, 5), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((805, 5, 805, 79), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((813, 5, 813, 79), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((821, 5, 821, 88), 'pytest.fixture', 'pytest.fixture', (), '', False, 'import pytest\n'), ((36, 16, 36, 33), 'pptx.table.Table', 'Table', ({(36, 22, 36, 26): 'tbl_', (36, 28, 36, 32): 'None'}, {}), '(tbl_, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((50, 16, 50, 32), 'pptx.table.Table', 'Table', ({(50, 22, 50, 25): 'tbl', (50, 27, 50, 31): 'None'}, {}), '(tbl, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((63, 16, 63, 32), 'pptx.table.Table', 'Table', ({(63, 22, 63, 25): 'tbl', (63, 27, 63, 31): 'None'}, {}), '(tbl, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((76, 16, 76, 32), 'pptx.table.Table', 'Table', ({(76, 22, 76, 25): 'tbl', (76, 27, 76, 31): 'None'}, {}), '(tbl, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((204, 15, 204, 30), 'pptx.table._Cell', '_Cell', ({(204, 21, 204, 23): 'tc', (204, 25, 204, 29): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((205, 28, 205, 43), 'pptx.table._Cell', '_Cell', ({(205, 34, 205, 36): 'tc', (205, 38, 205, 42): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((206, 29, 206, 50), 'pptx.table._Cell', '_Cell', ({(206, 35, 206, 43): 'other_tc', (206, 45, 206, 49): 'None'}, {}), '(other_tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((217, 15, 217, 30), 'pptx.table._Cell', '_Cell', ({(217, 21, 217, 23): 'tc', (217, 25, 217, 29): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((225, 15, 225, 30), 'pptx.table._Cell', '_Cell', ({(225, 21, 225, 23): 'tc', (225, 25, 225, 29): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((294, 15, 294, 30), 'pptx.table._Cell', '_Cell', ({(294, 21, 294, 23): 'tc', (294, 25, 294, 29): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((300, 15, 300, 30), 'pptx.table._Cell', '_Cell', ({(300, 21, 300, 23): 'tc', (300, 25, 300, 29): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((306, 15, 306, 37), 'pptx.table._Cell', '_Cell', ({(306, 21, 306, 30): 'origin_tc', (306, 32, 306, 36): 'None'}, {}), '(origin_tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((317, 15, 317, 30), 'pptx.table._Cell', '_Cell', ({(317, 21, 317, 23): 'tc', (317, 25, 317, 29): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((326, 15, 326, 32), 'pptx.table._Cell', '_Cell', ({(326, 21, 326, 25): 'None', (326, 27, 326, 31): 'None'}, {}), '(None, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((334, 15, 334, 32), 'pptx.table._Cell', '_Cell', ({(334, 21, 334, 25): 'None', (334, 27, 334, 31): 'None'}, {}), '(None, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((554, 26, 554, 51), 'pptx.table._CellCollection', '_CellCollection', ({(554, 42, 554, 44): 'tr', (554, 46, 554, 50): 'None'}, {}), '(tr, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((575, 26, 575, 51), 'pptx.table._CellCollection', '_CellCollection', ({(575, 42, 575, 44): 'tr', (575, 46, 575, 50): 'None'}, {}), '(tr, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((685, 18, 685, 46), 'pptx.table._ColumnCollection', '_ColumnCollection', ({(685, 36, 685, 39): 'tbl', (685, 41, 685, 45): 'None'}, {}), '(tbl, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((699, 18, 699, 46), 'pptx.table._ColumnCollection', '_ColumnCollection', ({(699, 36, 699, 39): 'tbl', (699, 41, 699, 45): 'None'}, {}), '(tbl, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((809, 15, 809, 40), 'pptx.table._RowCollection', '_RowCollection', ({(809, 30, 809, 33): 'tbl', (809, 35, 809, 39): 'None'}, {}), '(tbl, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((817, 15, 817, 40), 'pptx.table._RowCollection', '_RowCollection', ({(817, 30, 817, 33): 'tbl', (817, 35, 817, 39): 'None'}, {}), '(tbl, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((245, 13, 245, 37), 'pytest.raises', 'pytest.raises', ({(245, 27, 245, 36): 'TypeError'}, {}), '(TypeError)', False, 'import pytest\n'), ((266, 27, 266, 42), 'pptx.table._Cell', '_Cell', ({(266, 33, 266, 35): 'tc', (266, 37, 266, 41): 'None'}, {}), '(tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((266, 44, 266, 65), 'pptx.table._Cell', '_Cell', ({(266, 50, 266, 58): 'other_tc', (266, 60, 266, 64): 'None'}, {}), '(other_tc, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((277, 27, 277, 44), 'pptx.table._Cell', '_Cell', ({(277, 33, 277, 37): 'None', (277, 39, 277, 43): 'None'}, {}), '(None, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((277, 46, 277, 63), 'pptx.table._Cell', '_Cell', ({(277, 52, 277, 56): 'None', (277, 58, 277, 62): 'None'}, {}), '(None, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((279, 13, 279, 38), 'pytest.raises', 'pytest.raises', ({(279, 27, 279, 37): 'ValueError'}, {}), '(ValueError)', False, 'import pytest\n'), ((286, 27, 286, 44), 'pptx.table._Cell', '_Cell', ({(286, 33, 286, 37): 'None', (286, 39, 286, 43): 'None'}, {}), '(None, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((286, 46, 286, 63), 'pptx.table._Cell', '_Cell', ({(286, 52, 286, 56): 'None', (286, 58, 286, 62): 'None'}, {}), '(None, None)', False, 'from pptx.table import _Cell, _CellCollection, _Column, _ColumnCollection, _Row, _RowCollection, Table\n'), ((288, 13, 288, 38), 'pytest.raises', 'pytest.raises', ({(288, 27, 288, 37): 'ValueError'}, {}), '(ValueError)', False, 'import pytest\n'), ((319, 13, 319, 38), 'pytest.raises', 'pytest.raises', ({(319, 27, 319, 37): 'ValueError'}, {}), '(ValueError)', False, 'import pytest\n'), ((563, 13, 563, 38), 'pytest.raises', 'pytest.raises', ({(563, 27, 563, 37): 'IndexError'}, {}), '(IndexError)', False, 'import pytest\n'), ((565, 13, 565, 38), 'pytest.raises', 'pytest.raises', ({(565, 27, 565, 37): 'IndexError'}, {}), '(IndexError)', False, 'import pytest\n'), ((668, 13, 668, 38), 'pytest.raises', 'pytest.raises', ({(668, 27, 668, 37): 'IndexError'}, {}), '(IndexError)', False, 'import pytest\n'), ((670, 13, 670, 38), 'pytest.raises', 'pytest.raises', ({(670, 27, 670, 37): 'IndexError'}, {}), '(IndexError)', False, 'import pytest\n'), ((798, 13, 798, 38), 'pytest.raises', 'pytest.raises', ({(798, 27, 798, 37): 'IndexError'}, {}), '(IndexError)', False, 'import pytest\n'), ((800, 13, 800, 38), 'pytest.raises', 'pytest.raises', ({(800, 27, 800, 37): 'IndexError'}, {}), '(IndexError)', False, 'import pytest\n'), ((810, 39, 810, 49), 'pptx.oxml.ns.qn', 'qn', ({(810, 42, 810, 48): '"""a:tr"""'}, {}), "('a:tr')", False, 'from pptx.oxml.ns import qn\n'), ((818, 39, 818, 49), 'pptx.oxml.ns.qn', 'qn', ({(818, 42, 818, 48): '"""a:tr"""'}, {}), "('a:tr')", False, 'from pptx.oxml.ns import qn\n'), ((408, 55, 408, 67), 'pptx.util.Inches', 'Inches', ({(408, 62, 408, 66): '(0.09)'}, {}), '(0.09)', False, 'from pptx.util import Inches, Length, Pt\n'), ((409, 56, 409, 68), 'pptx.util.Inches', 'Inches', ({(409, 63, 409, 67): '(0.08)'}, {}), '(0.08)', False, 'from pptx.util import Inches, Length, Pt\n'), ((410, 54, 410, 66), 'pptx.util.Inches', 'Inches', ({(410, 61, 410, 65): '(0.07)'}, {}), '(0.07)', False, 'from pptx.util import Inches, Length, Pt\n'), ((411, 57, 411, 69), 'pptx.util.Inches', 'Inches', ({(411, 64, 411, 68): '(0.06)'}, {}), '(0.06)', False, 'from pptx.util import Inches, Length, Pt\n'), ((412, 36, 412, 47), 'pptx.util.Inches', 'Inches', ({(412, 43, 412, 46): '(0.1)'}, {}), '(0.1)', False, 'from pptx.util import Inches, Length, Pt\n'), ((413, 44, 413, 55), 'pptx.util.Inches', 'Inches', ({(413, 51, 413, 54): '(0.1)'}, {}), '(0.1)', False, 'from pptx.util import Inches, Length, Pt\n'), ((414, 35, 414, 47), 'pptx.util.Inches', 'Inches', ({(414, 42, 414, 46): '(0.05)'}, {}), '(0.05)', False, 'from pptx.util import Inches, Length, Pt\n'), ((415, 45, 415, 57), 'pptx.util.Inches', 'Inches', ({(415, 52, 415, 56): '(0.05)'}, {}), '(0.05)', False, 'from pptx.util import Inches, Length, Pt\n'), ((425, 36, 425, 48), 'pptx.util.Inches', 'Inches', ({(425, 43, 425, 47): '(0.08)'}, {}), '(0.08)', False, 'from pptx.util import Inches, Length, Pt\n'), ((426, 37, 426, 49), 'pptx.util.Inches', 'Inches', ({(426, 44, 426, 48): '(0.08)'}, {}), '(0.08)', False, 'from pptx.util import Inches, Length, Pt\n'), ((427, 35, 427, 47), 'pptx.util.Inches', 'Inches', ({(427, 42, 427, 46): '(0.08)'}, {}), '(0.08)', False, 'from pptx.util import Inches, Length, Pt\n'), ((428, 38, 428, 50), 'pptx.util.Inches', 'Inches', ({(428, 45, 428, 49): '(0.08)'}, {}), '(0.08)', False, 'from pptx.util import Inches, Length, Pt\n'), ((619, 40, 619, 49), 'pptx.util.Inches', 'Inches', ({(619, 47, 619, 48): '(1)'}, {}), '(1)', False, 'from pptx.util import Inches, Length, Pt\n'), ((619, 74, 619, 80), 'pptx.util.Pt', 'Pt', ({(619, 77, 619, 79): '(10)'}, {}), '(10)', False, 'from pptx.util import Inches, Length, Pt\n'), ((628, 34, 628, 43), 'pptx.util.Inches', 'Inches', ({(628, 41, 628, 42): '(1)'}, {}), '(1)', False, 'from pptx.util import Inches, Length, Pt\n'), ((629, 34, 629, 43), 'pptx.util.Inches', 'Inches', ({(629, 41, 629, 42): '(1)'}, {}), '(1)', False, 'from pptx.util import Inches, Length, Pt\n'), ((742, 47, 742, 56), 'pptx.util.Inches', 'Inches', ({(742, 54, 742, 55): '(1)'}, {}), '(1)', False, 'from pptx.util import Inches, Length, Pt\n'), ((742, 76, 742, 82), 'pptx.util.Pt', 'Pt', ({(742, 79, 742, 81): '(10)'}, {}), '(10)', False, 'from pptx.util import Inches, Length, Pt\n'), ((750, 29, 750, 38), 'pptx.util.Inches', 'Inches', ({(750, 36, 750, 37): '(1)'}, {}), '(1)', False, 'from pptx.util import Inches, Length, Pt\n'), ((751, 29, 751, 38), 'pptx.util.Inches', 'Inches', ({(751, 36, 751, 37): '(1)'}, {}), '(1)', False, 'from pptx.util import Inches, Length, Pt\n')]