"""Tests for BookmarksMixin — Bookmark CRUD."""

from __future__ import annotations

from pathlib import Path

import pytest

from docx_mcp.document import DocxDocument
from docx_mcp.document.errors import DocxMcpError, ErrCode

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _open(tmp_path: Path, test_docx: Path) -> DocxDocument:
    """Copy fixture to tmp_path so each test gets a clean copy, then open."""
    import shutil

    dest = tmp_path / "work.docx"
    shutil.copy2(test_docx, dest)
    doc = DocxDocument(str(dest))
    doc.open()
    return doc


# ---------------------------------------------------------------------------
# TestListBookmarks
# ---------------------------------------------------------------------------


class TestListBookmarks:
    def test_empty_doc_has_no_bookmarks(self, tmp_path: Path):
        """A freshly-created blank doc has no user bookmarks."""
        out = str(tmp_path / "blank.docx")
        doc = DocxDocument.create(out)
        bookmarks = doc.list_bookmarks()
        assert bookmarks == []

    def test_lists_added_bookmark(self, tmp_path: Path, test_docx: Path):
        """After add_bookmark, list_bookmarks returns it."""
        doc = _open(tmp_path, test_docx)
        doc.add_bookmark("00000002", "my_ref")
        bms = doc.list_bookmarks()
        names = [b["name"] for b in bms]
        assert "my_ref" in names

    def test_skips_internal_underscore_bookmarks(self, tmp_path: Path):
        """Bookmarks whose names start with '_' are excluded."""
        from lxml import etree

        out = str(tmp_path / "internal.docx")
        doc = DocxDocument.create(out)
        # Inject an underscore bookmark directly into the XML
        tree = doc._require("word/document.xml")
        body = tree.find("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}body")
        W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
        p = body[0]
        bs = etree.SubElement(p, f"{{{W}}}bookmarkStart")
        bs.set(f"{{{W}}}id", "99")
        bs.set(f"{{{W}}}name", "_GoBack")
        be = etree.SubElement(p, f"{{{W}}}bookmarkEnd")
        be.set(f"{{{W}}}id", "99")
        doc._mark("word/document.xml")

        bms = doc.list_bookmarks()
        names = [b["name"] for b in bms]
        assert "_GoBack" not in names

    def test_returns_id_name_para_id(self, tmp_path: Path, test_docx: Path):
        """Each bookmark dict has id (int), name (str), para_id (str|None)."""
        doc = _open(tmp_path, test_docx)
        # fixture has section_bg on para 00000004
        bms = doc.list_bookmarks()
        assert len(bms) >= 1
        bm = next(b for b in bms if b["name"] == "section_bg")
        assert isinstance(bm["id"], int)
        assert isinstance(bm["name"], str)
        assert bm["para_id"] == "00000004"


# ---------------------------------------------------------------------------
# TestAddBookmark
# ---------------------------------------------------------------------------


class TestAddBookmark:
    def test_add_bookmark(self, tmp_path: Path, test_docx: Path):
        """add_bookmark returns dict with id, name, para_id."""
        doc = _open(tmp_path, test_docx)
        result = doc.add_bookmark("00000002", "intro_ref")
        assert result["name"] == "intro_ref"
        assert result["para_id"] == "00000002"
        assert isinstance(result["id"], int)

    def test_bookmark_in_document_xml(self, tmp_path: Path, test_docx: Path):
        """bookmarkStart appears in word/document.xml after add_bookmark."""
        doc = _open(tmp_path, test_docx)
        doc.add_bookmark("00000002", "xml_check")
        tree = doc._require("word/document.xml")
        W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
        starts = tree.findall(f".//{{{W}}}bookmarkStart")
        names = [s.get(f"{{{W}}}name") for s in starts]
        assert "xml_check" in names

    def test_add_duplicate_name_raises(self, tmp_path: Path, test_docx: Path):
        """Adding a bookmark with an existing name raises OOXML_INVALID."""
        doc = _open(tmp_path, test_docx)
        doc.add_bookmark("00000002", "dup_test")
        with pytest.raises(DocxMcpError) as exc_info:
            doc.add_bookmark("00000003", "dup_test")
        assert exc_info.value.code == ErrCode.OOXML_INVALID

    def test_add_underscore_name_raises(self, tmp_path: Path, test_docx: Path):
        """Names starting with '_' are reserved; raises OOXML_INVALID."""
        doc = _open(tmp_path, test_docx)
        with pytest.raises(DocxMcpError) as exc_info:
            doc.add_bookmark("00000002", "_internal")
        assert exc_info.value.code == ErrCode.OOXML_INVALID

    def test_para_not_found_raises(self, tmp_path: Path, test_docx: Path):
        """Unknown para_id raises PARA_NOT_FOUND."""
        doc = _open(tmp_path, test_docx)
        with pytest.raises(DocxMcpError) as exc_info:
            doc.add_bookmark("DEADBEEF", "ghost")
        assert exc_info.value.code == ErrCode.PARA_NOT_FOUND

    def test_id_uniqueness_across_additions(self, tmp_path: Path, test_docx: Path):
        """Three successive add_bookmark calls all get distinct IDs."""
        doc = _open(tmp_path, test_docx)
        r1 = doc.add_bookmark("00000002", "bm_one")
        r2 = doc.add_bookmark("00000003", "bm_two")
        r3 = doc.add_bookmark("00000005", "bm_three")
        ids = {r1["id"], r2["id"], r3["id"]}
        assert len(ids) == 3


# ---------------------------------------------------------------------------
# TestRemoveBookmark
# ---------------------------------------------------------------------------


class TestRemoveBookmark:
    def test_remove_bookmark(self, tmp_path: Path, test_docx: Path):
        """remove_bookmark returns {"removed": name}."""
        doc = _open(tmp_path, test_docx)
        result = doc.remove_bookmark("section_bg")
        assert result == {"removed": "section_bg"}

    def test_not_found_raises(self, tmp_path: Path, test_docx: Path):
        """Removing a non-existent bookmark raises BOOKMARK_NOT_FOUND."""
        doc = _open(tmp_path, test_docx)
        with pytest.raises(DocxMcpError) as exc_info:
            doc.remove_bookmark("nonexistent_bm")
        assert exc_info.value.code == ErrCode.BOOKMARK_NOT_FOUND

    def test_both_start_and_end_removed(self, tmp_path: Path, test_docx: Path):
        """Both bookmarkStart and bookmarkEnd are gone after remove."""
        doc = _open(tmp_path, test_docx)
        result = doc.add_bookmark("00000002", "cleanup_test")
        bm_id = str(result["id"])
        doc.remove_bookmark("cleanup_test")
        W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
        tree = doc._require("word/document.xml")
        starts = [
            s
            for s in tree.findall(f".//{{{W}}}bookmarkStart")
            if s.get(f"{{{W}}}name") == "cleanup_test"
        ]
        assert starts == []
        ends = [e for e in tree.findall(f".//{{{W}}}bookmarkEnd") if e.get(f"{{{W}}}id") == bm_id]
        assert ends == []


# ---------------------------------------------------------------------------
# TestGetBookmarkedText
# ---------------------------------------------------------------------------


class TestGetBookmarkedText:
    def test_get_text(self, tmp_path: Path, test_docx: Path):
        """get_bookmarked_text returns the paragraph text for known bookmark."""
        doc = _open(tmp_path, test_docx)
        result = doc.get_bookmarked_text("section_bg")
        assert result["name"] == "section_bg"
        assert "30 days" in result["text"]

    def test_not_found_raises(self, tmp_path: Path, test_docx: Path):
        """Unknown bookmark name raises BOOKMARK_NOT_FOUND."""
        doc = _open(tmp_path, test_docx)
        with pytest.raises(DocxMcpError) as exc_info:
            doc.get_bookmarked_text("no_such_bookmark")
        assert exc_info.value.code == ErrCode.BOOKMARK_NOT_FOUND
