# Copyright 2018-2026 Jérôme Dumonteil
# Copyright (c) 2009-2010 Ars Aperta, Itaapy, Pierlis, Talend.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# Authors (odfdo project): jerome.dumonteil@gmail.com
# The odfdo project is a derivative work of the lpod-python project:
# https://github.com/lpod/lpod-python
# Authors: Romain Gauthier <romain@itaapy.com>
#          Hervé Cauwelier <herve@itaapy.com>
#          David Versmisse <david.versmisse@itaapy.com>
#          Jerome Dumonteil <jerome.dumonteil@itaapy.com>
from __future__ import annotations

from collections.abc import Iterable
from unittest.mock import PropertyMock, patch

import pytest

from odfdo.body import Metadata
from odfdo.document import Document
from odfdo.element import Element
from odfdo.mixin_named_range import TableNamedExpressions
from odfdo.named_range import NamedRange
from odfdo.table import Table


@pytest.fixture
def table(samples) -> Iterable[Table]:
    # simpletable :
    #   1	1	1	2	3	3	3
    #   1	1	1	2	3	3	3
    #   1	1	1	2	3	3	3
    #   1   2	3	4	5	6	7
    document = Document(samples("simple_table.ods"))
    yield document.body.get_table(name="Example1")


@pytest.fixture
def table2(samples) -> Iterable[Table]:
    document = Document(samples("simple_table_named_range.ods"))
    yield document.body.get_table(name="Example1")


def test_create_bad_nr():
    with pytest.raises(ValueError):
        NamedRange()


def test_create_bad_nr_2():
    with pytest.raises(ValueError):
        NamedRange(" ", "A1", "tname")


def test_create_bad_nr_3():
    with pytest.raises(ValueError):
        NamedRange("A1", "A1", "tname")


def test_create_bad_nr_4():
    with pytest.raises(ValueError):
        NamedRange("a space", "A1", "tname")


def test_create_bad_nr_5():
    with pytest.raises(ValueError):
        NamedRange("===", "A1", "tname")


def test_create_bad_nr_6():
    with pytest.raises(ValueError):
        NamedRange("ok", "A1", "/ ")


def test_create_bad_nr_7():
    with pytest.raises(ValueError):
        NamedRange("ok", "A1", " ")


def test_create_bad_nr_8():
    with pytest.raises(ValueError):
        NamedRange("ok", "A1", "\\")


def test_create_bad_nr_9():
    with pytest.raises(ValueError):
        NamedRange("ok", "A1", "tname\nsecond line")


def test_create_bad_nr_10():
    with pytest.raises(TypeError):
        NamedRange("ok", "A1", 42)


def test_create_nr():
    nr = NamedRange("nr_name_ù", "A1:C2", "table name é", usage="filter")
    result = (
        """<table:named-range table:name="nr_name_ù" """
        """table:base-cell-address="$'table name é'.$A$1" """
        """table:cell-range-address="$'table name é'.$A$1:.$C$2" """
        """table:range-usable-as="filter"/>"""
    )
    assert nr.serialize() == result


def test_create_nr_minimal():
    minimal = "<table:named-range/>"
    nr = Element.from_tag(minimal)
    assert isinstance(nr, NamedRange)


def test_create_nr_minimal_2():
    minimal = (
        '<table:named-range table:name="nr_name_ù" table:range-usable-as="filter"/>'
    )
    nr = Element.from_tag(minimal)
    assert isinstance(nr, NamedRange)


def test_create_bad_nr_coordinate():
    with pytest.raises(ValueError):
        NamedRange("nr_name_ù", table_name="table name é")


def test_usage_1():
    nr = NamedRange("a123a", "A1:C2", "tablename")
    assert nr.usage is None
    nr.set_usage("blob")
    assert nr.usage is None


def test_usage_2():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_usage("filter")
    assert nr.usage == "filter"
    nr.set_usage("blob")
    assert nr.usage is None


def test_usage_3():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_usage("Print-Range")
    assert nr.usage == "print-range"
    nr.set_usage(None)
    assert nr.usage is None


def test_usage_4():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_usage("repeat-column")
    assert nr.usage == "repeat-column"


def test_usage_5():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_usage("repeat-row")
    assert nr.usage == "repeat-row"


def test_name_1():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    assert nr.name == "nr_name"


def test_name_2():
    NAME = "New_Name_ô"
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.name = f"  {NAME} "
    assert nr.name == NAME


def test_name_3():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    try:
        nr.name = "   "
        ok = True
    except Exception:
        ok = False
    assert ok is False


def test_table_name_1():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    assert nr.table_name == "tablename"


def test_table_name_2():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_table_name("  new name ")
    assert nr.table_name == "new name"


def test_table_name_3():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    with pytest.raises(ValueError):
        nr.set_table_name("   ")


def test_range_1():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    with pytest.raises(ValueError):
        nr.set_range("   ")


def test_range_2():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    assert nr.crange == (0, 0, 2, 1)
    assert nr.start == (0, 0)
    assert nr.end == (2, 1)


def test_range_3():
    nr = NamedRange("nr_name", "A1", "tablename")
    assert nr.crange == (0, 0, 0, 0)
    assert nr.start == (0, 0)
    assert nr.end == (0, 0)


def test_range_4():
    nr = NamedRange("nr_name", (1, 2, 3, 4), "tablename")
    assert nr.crange == (1, 2, 3, 4)
    assert nr.start == (1, 2)
    assert nr.end == (3, 4)


def test_range_5():
    nr = NamedRange("nr_name", (5, 6), "tablename")
    assert nr.crange == (5, 6, 5, 6)
    assert nr.start == (5, 6)
    assert nr.end == (5, 6)


def test_range_6():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_range("B3")
    assert nr.crange == (1, 2, 1, 2)
    assert nr.start == (1, 2)
    assert nr.end == (1, 2)


def test_range_7():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_range("B3:b10")
    assert nr.crange == (1, 2, 1, 9)
    assert nr.start == (1, 2)
    assert nr.end == (1, 9)


def test_range_8():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_range((1, 5, 0, 9))
    assert nr.crange == (1, 5, 0, 9)
    assert nr.start == (1, 5)
    assert nr.end == (0, 9)


def test_range_9():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    nr.set_range((0, 9))
    assert nr.crange == (0, 9, 0, 9)
    assert nr.start == (0, 9)
    assert nr.end == (0, 9)


def test_value_bad_1():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    with pytest.raises(ValueError):
        nr.get_values()


def test_value_bad_2():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    with pytest.raises(ValueError):
        nr.get_value()


def test_value_bad_3():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    with pytest.raises(ValueError):
        nr.set_values([[1, 2]])


def test_value_bad_4():
    nr = NamedRange("nr_name", "A1:C2", "tablename")
    with pytest.raises(ValueError):
        nr.set_value(42)


def test_body_table_get_1(table):
    assert table.get_named_ranges() == []


def test_body_table_get_2(table2):
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["nr_1", "nr_6"]


def test_body_table_get_3(table2):
    assert len(table2.get_named_ranges()) == 2


def test_body_table_get_4(table2):
    back_nr = table2.get_named_range("nr_1")
    assert back_nr.name == "nr_1"


def test_body_table_get_4_1(table2):
    back_nr = table2.get_named_range("nr_1xxx")
    assert back_nr is None


def test_body_table_get_4_2(table2):
    back_nr = table2.get_named_range("nr_6")
    assert back_nr.name == "nr_6"
    assert back_nr.table_name == "Example1"
    assert back_nr.start == (3, 2)
    assert back_nr.end == (5, 3)
    assert back_nr.crange == (3, 2, 5, 3)
    assert back_nr.usage == "print-range"


def test_body_table_get_5(table):
    back_nr = table.get_named_range("nr_1")
    assert back_nr is None


def test_body_table_rename_1(table2):
    back_nr = table2.get_named_range("nr_1")
    back_nr.name = "new_name"
    assert back_nr.name == "new_name"


def test_body_table_rename_2(table2):
    back_nr = table2.get_named_range("nr_1")
    back_nr.name = "new_name"
    back_nr2 = table2.get_named_range("nr_1")
    assert back_nr2 is None


def test_body_table_rename_3(table2):
    back_nr = table2.get_named_range("nr_1")
    back_nr.name = "new_name"
    back_nr2 = table2.get_named_range("new_name")
    assert isinstance(back_nr2, NamedRange)


def test_body_table_rename_4(table2):
    back_nr = table2.get_named_range("nr_1")
    back_nr.name = "new_name"
    back_nr2 = table2.get_named_range("new_name")
    expected = (
        '<table:named-range table:name="new_name" '
        'table:base-cell-address="$Example1.$C$1" '
        'table:cell-range-address="$Example1.$C$1"/>'
    )
    assert back_nr2.serialize() == expected


def test_body_table_rename_same(table2):
    table2.set_named_range(
        name="nr_1", table_name="Example1", crange="C5", usage="print-range"
    )
    nr = table2.get_named_range("nr_1")
    assert nr.name == "nr_1"
    assert nr.table_name == "Example1"
    assert nr.start == (2, 4)
    assert nr.end == (2, 4)
    assert nr.crange == (2, 4, 2, 4)
    assert nr.usage == "print-range"


def test_body_table_rename_same_2(table2):
    nr = table2.get_named_range("nr_6")
    nr.name = "nr_1"
    assert nr.name == "nr_1"
    assert nr.table_name == "Example1"
    assert nr.start == (3, 2)
    assert nr.end == (5, 3)
    assert nr.crange == (3, 2, 5, 3)
    assert nr.usage == "print-range"


def test_body_table_rename_same_3(table2):
    table2.set_named_range(
        name="nr_1", table_name="Example1", crange="C5", usage="print-range"
    )
    nr = table2.get_named_range("nr_1")
    nr.name = "nr_42"
    assert nr.name == "nr_42"
    assert nr.table_name == "Example1"


def test_body_table_set_0(table2):
    with pytest.raises(ValueError):
        table2.set_named_range("   ", "A1:C2")


def test_body_table_set_1(table2):
    table2.set_named_range("new", "A1:B1")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["nr_1", "nr_6", "new"]


def test_body_table_set_3(table2):
    table2.set_named_range("new", "A1:B1")
    back_nr = table2.get_named_range("new")
    assert back_nr.usage is None
    assert back_nr.crange == (0, 0, 1, 0)
    assert back_nr.start == (0, 0)
    assert back_nr.end == (1, 0)
    assert back_nr.table_name == "Example1"
    # reset
    table2.set_named_range("new", "A1:c2")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["nr_1", "nr_6", "new"]
    back_nr = table2.get_named_range("new")
    assert back_nr.usage is None
    assert back_nr.crange == (0, 0, 2, 1)
    assert back_nr.start == (0, 0)
    assert back_nr.end == (2, 1)
    assert back_nr.table_name == "Example1"


def test_body_table_name_set(table2):
    nr = table2.get_named_range("nr_6")
    nr.table_name = "badname"
    assert nr.table_name == "badname"


def test_body_table_name_set_2(table2):
    nr = table2.get_named_range("nr_6")
    nr.table_name = "badname"
    with pytest.raises(ValueError):
        nr.get_values()


def test_body_table_name_set_3(table2):
    nr = table2.get_named_range("nr_6")
    nr.table_name = "badname"
    with pytest.raises(ValueError):
        nr.get_value()


def test_body_table_name_set_4(table2):
    nr = table2.get_named_range("nr_6")
    nr.table_name = "badname"
    with pytest.raises(ValueError):
        nr.set_values([1, 2, 3, 4])


def test_body_table_name_set_5(table2):
    nr = table2.get_named_range("nr_6")
    nr.table_name = "badname"
    with pytest.raises(ValueError):
        nr.set_value(1)


def test_body_table_delete_1(table2):
    table2.delete_named_range("xxx")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["nr_1", "nr_6"]


def test_body_table_delete_2(table2):
    table2.delete_named_range("nr_1")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["nr_6"]


def test_body_table_delete_3(table2):
    table2.set_named_range("new", "A1:c2")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["nr_1", "nr_6", "new"]
    table2.delete_named_range("nr_1")
    table2.delete_named_range("nr_6")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["new"]
    table2.delete_named_range("new")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == []
    table2.delete_named_range("new")
    table2.delete_named_range("xxx")
    table2.set_named_range("hop", "A1:C2")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["hop"]
    table2.set_named_range("hop", "A2:d8")
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["hop"]
    nr = table2.get_named_range("hop")
    assert nr.crange == (0, 1, 3, 7)


def test_body_table_delete_none():
    table = Table("table")
    doc = Document("ods")
    doc.body.clear()
    doc.body.append(table)
    assert table.delete_named_range("nothing") is None


def test_body_table_get_value_1(table2):
    result = table2.get_named_range("nr_1").get_value()
    assert result == 1


def test_body_table_get_value_2(table2):
    result = table2.get_named_range("nr_1").get_value(get_type=True)
    assert result == (1, "float")


def test_body_table_get_value_3(table2):
    result = table2.get_named_range("nr_1").get_values()
    assert result == [[1]]


def test_body_table_get_value_4(table2):
    result = table2.get_named_range("nr_1").get_values(flat=True)
    assert result == [1]


def test_body_table_get_value_5(table2):
    result = table2.get_named_range("nr_6").get_values(flat=True)
    assert result == [2, 3, 3, 4, 5, 6]


def test_body_table_get_value_6(table2):
    result = table2.get_named_range("nr_6").get_value()
    assert result == 2


def test_body_table_set_value_1(table2):
    table2.get_named_range("nr_6").set_value("AAA")
    assert table2.get_value("D3") == "AAA"
    assert table2.get_value("E3") == 3


def test_body_table_set_value_2(table2):
    table2.get_named_range("nr_6").set_values([[10, 11, 12], [13, 14, 15]])
    assert table2.get_values() == [
        [1, 1, 1, 2, 3, 3, 3],
        [1, 1, 1, 2, 3, 3, 3],
        [1, 1, 1, 10, 11, 12, 3],
        [1, 2, 3, 13, 14, 15, 7],
    ]


def test_body_change_name_table(table2):
    table2.name = "new table"
    result = [nr.name for nr in table2.get_named_ranges()]
    assert result == ["nr_1", "nr_6"]
    back_nr = table2.get_named_range("nr_6")
    assert back_nr.name == "nr_6"
    assert back_nr.table_name == "new table"
    assert back_nr.start == (3, 2)
    assert back_nr.end == (5, 3)
    assert back_nr.crange == (3, 2, 5, 3)
    assert back_nr.usage == "print-range"


def test_body_set_named_range_bad_name(samples):
    document = Document(samples("simple_table_named_range.ods"))
    with pytest.raises(ValueError):
        document.body.set_named_range("", "", "")


def test_body_set_named_range_bad_table_name(samples):
    document = Document(samples("simple_table_named_range.ods"))
    with pytest.raises(ValueError):
        document.body.set_named_range("name", "", "")


def test_get_named_ranges_filter():
    table = Table("Test")
    table._local_set_named_range("local_range_one", "A1:B2")
    res = table.get_named_ranges(table_name=["Test"], global_scope=False)
    assert len(res) == 1
    with pytest.raises(ValueError, match="table_name must be string or Iterable"):
        table.get_named_ranges(table_name=True, global_scope=False)


def test_named_range_no_body_errors():
    table = Table("Test")
    with pytest.raises(ValueError, match="Table is not inside a document"):
        table.get_named_range("some", global_scope=True)
    nr = NamedRange("name_nr", crange="A1:B2", table_name="Test")
    with pytest.raises(ValueError, match="Table is not inside a document"):
        table.append_named_range(nr, global_scope=True)
    with pytest.raises(ValueError, match="Table is not inside a document"):
        table.set_named_range("name_nr", "A1:B2", global_scope=True)
    with pytest.raises(ValueError, match="Table is not inside a document"):
        table.delete_named_range("name_nr", global_scope=True)
    with pytest.raises(ValueError, match="Name required"):
        table.delete_named_range("", global_scope=False)


def test_named_range_with_body():
    doc = Document("ods")
    table = doc.body.get_table(0)
    # global append
    nr = NamedRange("global_nr", crange="A1:B2", table_name=table.name)
    table.append_named_range(nr, global_scope=True)

    # global get
    got = table.get_named_range("global_nr", global_scope=True)
    assert got.name == "global_nr"

    # global delete
    table.delete_named_range("global_nr", global_scope=True)
    assert table.get_named_range("global_nr", global_scope=True) is None


def test_named_range_with_body_error():
    doc = Document("ods")
    # replace body child by something that don't allow named range
    body_node = doc.content.root.get_element("office:body")
    old_body_child = body_node.children[0]
    table = old_body_child.get_table(0)
    new_child = Metadata()
    body_node.delete(old_body_child)
    body_node.append(new_child)
    new_child.append(table)

    nr = NamedRange("nr_err", crange="A1", table_name="T")
    # Metadata doesn't allow named ranges
    with pytest.raises(TypeError, match="Document must be of type"):
        table.append_named_range(nr, global_scope=True)

    # set_named_range
    with pytest.raises(TypeError, match="Document must be of type"):
        table.set_named_range("nr_err", "A1", global_scope=True)

    # delete_named_range
    with pytest.raises(TypeError, match="Document must be of type"):
        table.delete_named_range("nr_err", global_scope=True)


def test_get_named_range_none_internal():
    table = Table("Test")
    doc = Document("ods")
    doc.body.append(table)
    with patch.object(Table, "document_body", new_callable=PropertyMock) as mock_body:
        mock_body.return_value = doc.body
        with patch.object(doc.body, "get_named_range", return_value=None):
            assert table.get_named_range("none") is None
    assert table.get_named_range("none", global_scope=False) is None


def test_append_named_range_local():
    table = Table("Test")
    nr = NamedRange("local_nr", crange="A1", table_name="Test")
    table.append_named_range(nr, global_scope=False)
    assert table.get_named_range("local_nr", global_scope=False) is not None


def test_set_named_range_local():
    table = Table("Test")
    table.set_named_range("local_nr", "A1", global_scope=False)
    assert table.get_named_range("local_nr", global_scope=False) is not None


def test_delete_named_range_local():
    table = Table("Test")
    table.set_named_range("local_nr", "A1", global_scope=False)
    table.delete_named_range("local_nr", global_scope=False)
    assert table.get_named_range("local_nr", global_scope=False) is None


def test_get_named_range_no_body_try_valueError():
    table = Table("T")
    with pytest.raises(ValueError, match="Table is not inside a document"):
        table.get_named_range("nr", global_scope=True)


def test_get_named_range_unsupported_body_branch():
    doc = Document("ods")
    table = doc.body.get_table(0)
    # We mock allow_named_range to False
    with patch.object(Element, "tag", new_callable=PropertyMock) as mock_tag:
        mock_tag.return_value = "office:other"
        assert table.get_named_range("nr", global_scope=True) is None


def test_local_named_ranges():
    table = Table("Test")
    table._local_set_named_range("nr_one", "A1:B2")
    table._local_set_named_range("nr_two", "C1:D2")
    res = table._local_named_ranges()
    assert len(res) == 2


def test_local_delete_named_range_exit():
    table = Table("T")
    table._local_set_named_range("nr_one", "A1")
    table._local_set_named_range("nr_two", "B2")
    # Delete nr_one. named_expressions will still have nr_two.
    table._local_delete_named_range("nr_one")

    container = table.get_element(TableNamedExpressions._tag)
    assert container is not None
    assert not container.is_empty()


def test_local_delete_named_range_no_container_at_all():
    table = Table("T")
    table._local_set_named_range("nr_one", "A1")
    with patch.object(Table, "get_element", return_value=None):
        table._local_delete_named_range("nr_one")


def test_local_named_range_more():
    table = Table("Test")
    table._local_set_named_range("local_range_more", "A1:B2")
    got = table._local_named_range("local_range_more")
    assert got.crange == (0, 0, 1, 1)
    table._local_delete_named_range("local_range_more")
    assert table._local_named_range("local_range_more") is None
    table._local_delete_named_range("none_nr")
    table._local_set_named_range("local_range_2", "A1:B2")
    table._local_delete_named_range("local_range_2")

    assert table.get_element(TableNamedExpressions._tag) is None


def test_local_named_range_delete_empty_container():
    table = Table("Test")
    table._local_set_named_range("nr_one", "A1")

    assert table.get_element(TableNamedExpressions._tag) is not None
    # delete the only NR
    table._local_delete_named_range("nr_one")
    assert table.get_element(TableNamedExpressions._tag) is None


def test_local_named_range_delete_no_container():
    table = Table("Test")
    table._local_set_named_range("nr_one", "A1")

    container = table.get_element(TableNamedExpressions._tag)
    # move nr_one out of container
    nr_one = container.get_element("table:named-range")
    table.append(nr_one)
    table.delete(container)
    table._local_delete_named_range("nr_one")
    assert table._local_named_range("nr_one") is None


def test_local_append_named_range_replace_internal():
    table = Table("Test")
    table._local_set_named_range("nr_one", "A1")
    nr2 = NamedRange("nr_one", crange="B2", table_name="Test")
    table._local_append_named_range(nr2)
    assert table._local_named_range("nr_one").crange == (1, 1, 1, 1)


def test_named_range_set_table_name_same():
    nr = NamedRange("nr", crange="A1", table_name="T")
    # Coverage for set_table_name if name == self.table_name
    nr.set_table_name("T")
    assert nr.table_name == "T"


def test_local_delete_named_range_no_container_after_delete():
    table = Table("T")
    table._local_set_named_range("nr_one", "A1")

    container = table.get_element(TableNamedExpressions._tag)
    nr = container.get_element("table:named-range")
    # put nr outside
    table.append(nr)
    table.delete(container)
    table._local_delete_named_range("nr_one")
    # Check it still exists because _local_delete_named_range failed
    # before delete
    assert table.get_element("table:named-range") is not None
