from __future__ import annotations

from datetime import date
from xml.etree import ElementTree

import pytest

import ccma.services.documents as document_module
from ccma.domain.models import ContributionData, Member
from ccma.services.documents import (
    DocumentError,
    DocumentService,
    _replace_xml_placeholders,
    _template_values,
)
from ccma.services.housekeeper import Housekeeper
from ccma.storage.repository import MemberRepository, RepositoryError


def test_placeholder_replacement_crosses_formatted_odf_spans() -> None:
    source = b"""<?xml version="1.0" encoding="UTF-8"?>
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
 xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
 <office:body><office:text><text:p>Hello {{member.<text:span>full</text:span>_name}}!</text:p>
 </office:text></office:body></office:document>"""

    rendered = _replace_xml_placeholders(source, {"member.full_name": "Ada Lovelace"})
    text = "".join(ElementTree.fromstring(rendered).itertext())

    assert "Hello Ada Lovelace!" in text
    assert "{{" not in text


def test_unknown_placeholder_is_reported() -> None:
    source = b"""<document><p>{{member.unknown}}</p></document>"""

    with pytest.raises(DocumentError, match="member.unknown"):
        _replace_xml_placeholders(source, {})


def test_document_and_claim_creation_time_placeholders() -> None:
    member = Member("member-1", "CCMA-1", "Ada", "Lovelace")
    claim = {
        "claim_id": "claim-1",
        "title": "Test",
        "amount": "10.00",
        "created_at": "2026-06-21T14:35:00+02:00",
    }
    data = ContributionData(claims=[claim])

    values, _repeats = _template_values(member, data=data, claim=claim)

    assert values["document.created_date"] == values["current_date"]
    assert values["document.created_at"] == values["current_datetime"]
    assert "document.date" not in values
    assert "document.datetime" not in values
    assert values["claim.created_date"] in {"21.06.2026", "2026-06-21"}
    assert values["claim.created_at"] == "21.06.2026 14:35"


def test_credit_claim_paid_placeholder_is_positive() -> None:
    member = Member("member-1", "CCMA-1", "Ada", "Lovelace")
    claim = {"claim_id": "claim-1", "title": "Kautionsrückzahlung", "amount": "-25.00"}
    data = ContributionData(
        claims=[claim],
        credits=[{"credit_id": "credit-1", "amount": "25.00"}],
        allocations=[
            {
                "allocation_id": "allocation-1",
                "claim_id": "claim-1",
                "credit_id": "credit-1",
                "amount": "25.00",
            }
        ],
    )

    values, _repeats = _template_values(member, data=data, claim=claim)

    assert values["claim.paid"] == "25.00 EUR"
    assert values["claim.balance"] == "0.00 EUR"


def test_claim_item_loop_clones_formatted_table_row() -> None:
    source = b"""<office:document
 xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
 xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
 xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
 <office:body><office:text><table:table>
  <table:table-row table:style-name="item-row">
   <table:table-cell><text:p>{{#claim.items}}{{item.description}}</text:p></table:table-cell>
   <table:table-cell><text:p>{{item.amount}}{{/claim.items}}</text:p></table:table-cell>
  </table:table-row>
 </table:table></office:text></office:body></office:document>"""
    items = [
        {"item.description": "Beitrag", "item.amount": "75.00"},
        {"item.description": "Gebühr", "item.amount": "5.00"},
    ]

    rendered = _replace_xml_placeholders(source, {}, {"claim.items": items})
    root = ElementTree.fromstring(rendered)
    rows = [element for element in root.iter() if element.tag.endswith("table-row")]
    row_text = ["".join("".join(row.itertext()).split()) for row in rows]

    assert row_text == ["Beitrag75.00", "Gebühr5.00"]
    assert all(next(iter(row.attrib.values())) == "item-row" for row in rows)


def test_claim_item_loop_removes_row_for_empty_collection() -> None:
    source = b"""<table:table xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
 xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
 <table:table-row><table:table-cell><text:p>{{#claim.items}}{{item.description}}
 {{/claim.items}}</text:p></table:table-cell></table:table-row></table:table>"""

    rendered = _replace_xml_placeholders(source, {}, {"claim.items": []})

    assert not any(element.tag.endswith("table-row") for element in ElementTree.fromstring(rendered).iter())


def test_default_templates_are_seeded_without_overwriting_store_version(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    template = repository.root / "templates" / "Mitglied.fodt"
    template.write_text("custom", encoding="utf-8")

    repository.initialize()

    assert template.read_text(encoding="utf-8") == "custom"
    assert {path.name for path in (repository.root / "templates").iterdir()} == {
        "Forderung mit Positionen.fodt",
        "Mahnung.fodt",
        "Mitglied.fodt",
        "mail",
    }


def test_templates_are_filtered_by_available_context(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    service = DocumentService(repository)

    member_templates = {
        item.path.name for item in service.compatible_templates(has_claim=False, has_reminder=False)
    }
    claim_templates = {
        item.path.name for item in service.compatible_templates(has_claim=True, has_reminder=False)
    }
    reminder_templates = {
        item.path.name for item in service.compatible_templates(has_claim=True, has_reminder=True)
    }

    assert member_templates == {"Mitglied.fodt"}
    assert claim_templates == {"Forderung mit Positionen.fodt", "Mitglied.fodt"}
    assert reminder_templates == {
        "Forderung mit Positionen.fodt",
        "Mahnung.fodt",
        "Mitglied.fodt",
    }


def test_generated_reminder_pdf_is_stored_audited_and_linked(tmp_path, monkeypatch) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Ada", last_name="Lovelace")
    member.status = "active"
    member.accepted_at = "2026-01-01"
    member.membership_started_at = "2026-01-01"
    repository.save_member(member)
    Housekeeper(repository).run(today=date(2026, 1, 1))
    claim = repository.get_contributions(member.member_id).claims[0]
    claim_id = str(claim["claim_id"])
    reminder = repository.create_reminder_draft(
        member.member_id,
        claim_id,
        level=1,
        name="Zahlungserinnerung",
        payment_deadline_days=14,
    )

    def fake_convert(source, output_directory):
        destination = output_directory / f"{source.stem}.pdf"
        destination.write_bytes(b"%PDF-1.7\ntest")
        return destination

    monkeypatch.setattr(document_module, "_convert_to_pdf", fake_convert)
    service = DocumentService(repository)
    template = next(item for item in service.list_templates() if item.path.name == "Mahnung.fodt")

    generated = service.generate(
        template,
        member.member_id,
        output_name="Mahnung.pdf",
        claim_id=claim_id,
        reminder_id=str(reminder["reminder_id"]),
    )

    assert generated.path.read_bytes().startswith(b"%PDF-")
    assert generated.relative_path == "documents/Mahnung.pdf"
    stored = repository.get_contributions(member.member_id)
    updated = next(item for item in stored.reminders if item["reminder_id"] == reminder["reminder_id"])
    assert updated["status"] == "generated"
    assert updated["document"]["path"] == generated.relative_path
    assert updated["document"]["sha256"] == generated.sha256
    assert repository.get_events(member.member_id)[-1].event_type == "reminder_document_generated"

    with pytest.raises(RepositoryError, match="innerhalb"):
        repository.register_reminder_document(
            member.member_id,
            claim_id,
            str(reminder["reminder_id"]),
            relative_path="../outside.pdf",
            sha256="invalid",
            template="Mahnung.fodt",
        )


def test_office_executable_prefers_path_lookup(monkeypatch) -> None:
    monkeypatch.setattr(document_module.shutil, "which", lambda name: "C:/tools/soffice.exe")

    assert document_module._office_executable() == "C:/tools/soffice.exe"


def test_office_executable_falls_back_to_windows_default_path(tmp_path, monkeypatch) -> None:
    libreoffice = tmp_path / "LibreOffice" / "program"
    libreoffice.mkdir(parents=True)
    executable = libreoffice / "soffice.exe"
    executable.write_text("", encoding="utf-8")

    monkeypatch.setattr(document_module.shutil, "which", lambda name: None)
    monkeypatch.setattr(document_module.os, "name", "nt")
    monkeypatch.setenv("PROGRAMFILES", str(tmp_path))

    assert document_module._office_executable() == str(executable)
