from __future__ import annotations

import datetime as dt
from io import StringIO
from pathlib import Path

import pytest

from xml_to_xlsx import generate_workbook_from_xml, write_workbook_from_xml
from xml_to_xlsx._parser import (
    coerce_value,
    compute_span_bounds,
    load_xml_root,
    parse_bool,
    parse_column,
    parse_column_range,
    parse_span,
)
from xml_to_xlsx._styling import normalize_color


def test_generate_workbook_with_core_sheet_features() -> None:
    xml = """
    <workbook>
      <sheet name="Summary" freeze="B2" gridlines="false" tab-color="green"
             default-column-width="20" default-row-height="24"
             style="font-weight:bold;color:#112233">
        <column range="A:C" width="11" />
        <row index="2" start-column="2" style="text-align:center">
          <cell>West</cell>
          <cell>East</cell>
        </row>
        <cell ref="E2" formula="SUM(B2:C2)" />
      </sheet>
    </workbook>
    """

    workbook = generate_workbook_from_xml(xml)
    ws = workbook["Summary"]

    assert ws.freeze_panes == "B2"
    assert ws.sheet_view.showGridLines is False
    assert ws.sheet_properties.tabColor is not None
    assert ws.sheet_properties.tabColor.rgb == "FF008000"
    assert ws.sheet_format.defaultColWidth == 20
    assert ws.sheet_format.defaultRowHeight == 24
    assert ws.column_dimensions["A"].width == 11
    assert ws.column_dimensions["B"].width == 11
    assert ws.column_dimensions["C"].width == 11

    assert ws["B2"].value == "West"
    assert ws["C2"].value == "East"
    assert ws["E2"].value == "=SUM(B2:C2)"
    assert ws["B2"].font.bold is True
    assert ws["B2"].alignment.horizontal == "center"


def test_merge_comment_hyperlink_and_style_propagation() -> None:
    xml = """
    <workbook>
      <sheet name="Merged">
        <row index="1">
          <cell column="A" rowspan="2" colspan="2"
                style="border:2px solid #111111;background-color:#ffeeaa"
                comment="Merged" comment-author="QA" hyperlink="https://example.com">
            Anchor
          </cell>
        </row>
      </sheet>
    </workbook>
    """

    workbook = generate_workbook_from_xml(xml)
    ws = workbook["Merged"]

    assert "A1:B2" in {str(rng) for rng in ws.merged_cells.ranges}
    assert ws["A1"].value == "Anchor"
    assert ws["A1"].hyperlink is not None
    assert ws["A1"].hyperlink.target == "https://example.com"
    assert ws["A1"].comment is not None
    assert ws["A1"].comment.author == "QA"

    assert ws["A1"].fill.fgColor.rgb == "FFFFEEAA"
    assert ws["B2"].fill.fgColor.rgb == "FFFFEEAA"
    assert ws["A1"].border.left.style == "medium"
    assert ws["B2"].border.left.style == "medium"


def test_write_workbook_from_xml_saves_file(tmp_path: Path) -> None:
    xml = """
    <workbook>
      <sheet name="Export">
        <row index="1"><cell column="A">ok</cell></row>
      </sheet>
    </workbook>
    """
    output = tmp_path / "export.xlsx"

    workbook = write_workbook_from_xml(xml, save_to=output)

    assert output.exists()
    assert output.stat().st_size > 0
    assert workbook["Export"]["A1"].value == "ok"


def test_invalid_root_raises_value_error() -> None:
    with pytest.raises(ValueError, match="Root element must be <workbook>"):
        generate_workbook_from_xml("<sheet name='x' />")


def test_row_without_index_raises_value_error() -> None:
    xml = """
    <workbook>
      <sheet>
        <row><cell column="A">x</cell></row>
      </sheet>
    </workbook>
    """
    with pytest.raises(ValueError, match="<row> requires index attribute"):
        generate_workbook_from_xml(xml)


def test_load_xml_root_supports_path_and_stream(tmp_path: Path) -> None:
    xml_file = tmp_path / "sheet.xml"
    xml_file.write_text("<workbook><sheet name='One' /></workbook>")

    root_from_path = load_xml_root(str(xml_file))
    root_from_stream = load_xml_root(StringIO("<workbook><sheet /></workbook>"))

    assert root_from_path.tag == "workbook"
    assert root_from_stream.tag == "workbook"


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        ("A", 1),
        ("$b", 2),
        ("27", 27),
    ],
)
def test_parse_column(value: str, expected: int) -> None:
    assert parse_column(value) == expected


def test_parse_column_range_reorders_reverse_input() -> None:
    assert parse_column_range("D:B") == (2, 4)


@pytest.mark.parametrize(
    ("value", "expected"),
    [
        (None, 1),
        ("0", 1),
        ("3", 3),
        ("-2", -2),
    ],
)
def test_parse_span(value: str | None, expected: int) -> None:
    assert parse_span(value) == expected


def test_parse_span_rejects_invalid_values() -> None:
    with pytest.raises(ValueError, match="Span must be an integer"):
        parse_span("bad")


def test_compute_span_bounds_and_boundary_error() -> None:
    assert compute_span_bounds(5, 2) == (5, 6)
    assert compute_span_bounds(5, -2) == (4, 5)
    with pytest.raises(ValueError, match="worksheet boundary"):
        compute_span_bounds(1, -3)


def test_parse_bool_and_coerce_value() -> None:
    assert parse_bool("true", default=False) is True
    assert parse_bool("off", default=True) is False
    assert parse_bool("unknown", default=True) is True

    assert coerce_value("42", "int") == 42
    assert coerce_value("1.25", "float") == pytest.approx(1.25)
    assert coerce_value("yes", "bool") is True
    assert coerce_value("2025-01-02", "date") == dt.date(2025, 1, 2)
    assert coerce_value("2025-01-02T09:30:00", "datetime") == dt.datetime(
        2025,
        1,
        2,
        9,
        30,
    )
    assert coerce_value(" raw ", None) == "raw"


def test_normalize_color_variants() -> None:
    assert normalize_color("#abc") == "FFAABBCC"
    assert normalize_color("orange") == "FFFFA500"
    assert normalize_color("rgb(17, 34, 51)") == "FF112233"
    assert normalize_color("transparent") is None
