# Copyright (c) 2026 Lark Technologies Pte. Ltd.
# SPDX-License-Identifier: MIT
from __future__ import annotations

import itertools
import json
import subprocess
import sys
import tempfile
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
from unittest import mock

import sxsd_validator
import xml_text_overlap_lint


class XmlTextOverlapLintGeometryTest(unittest.TestCase):
    def assertNoXmlTextOverlapLintErrors(self, result: dict, sample_name: str) -> None:
        issue_summaries = []
        for slide in result.get("slides", []):
            for issue in slide.get("issues", []):
                issue_summaries.append(
                    f"slide {slide['slide_number']}: {issue['level']} {issue['code']} {issue['message']}"
                )
        if result.get("issues"):
            for issue in result["issues"]:
                issue_summaries.append(f"{issue['level']} {issue['code']} {issue['message']}")
        self.assertEqual(
            result["summary"]["error_count"],
            0,
            f"{sample_name} has XML text overlap lint errors:\n" + "\n".join(issue_summaries),
        )

    def test_cli_suggests_input_flag_for_positional_argument(self) -> None:
        script_path = Path(xml_text_overlap_lint.__file__).resolve()
        input_path = "/sandboxdata/workspace/file/full_presentation.xml"

        completed = subprocess.run(
            [sys.executable, str(script_path), input_path],
            capture_output=True,
            check=False,
            text=True,
        )

        self.assertEqual(completed.returncode, 1)
        self.assertEqual(completed.stdout, "")
        self.assertEqual(
            completed.stderr,
            f"xml-text-overlap-lint error: unexpected argument: {input_path}, need --input\n",
        )

    def test_cli_preserves_requested_symlink_path_in_result(self) -> None:
        script_path = Path(xml_text_overlap_lint.__file__).resolve()
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_path = Path(temp_dir)
            resolved_path = temp_path / "resolved.xml"
            requested_path = temp_path / "requested.xml"
            resolved_path.write_text(
                '<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">'
                '<slide xmlns="https://www.larkoffice.com/sml/2.0"><data>'
                '<shape type="text" topLeftX="10" topLeftY="10" width="100" height="30">'
                '<content><p><span>Test</span></p></content>'
                '</shape></data></slide>'
                '</presentation>',
                encoding="utf-8",
            )
            requested_path.symlink_to(resolved_path)

            completed = subprocess.run(
                [sys.executable, str(script_path), "--input", str(requested_path)],
                capture_output=True,
                check=False,
                text=True,
            )

        self.assertEqual(completed.returncode, 0, completed.stderr)
        result = json.loads(completed.stdout)
        self.assertEqual(result["file"], str(requested_path))

    def test_cli_reports_structured_slide_sxsd_error_outside_skill_directory(self) -> None:
        script_path = Path(xml_text_overlap_lint.__file__).resolve()
        with tempfile.TemporaryDirectory() as temp_dir:
            input_path = Path(temp_dir) / "invalid-slide.xml"
            input_path.write_text(
                """
                <slide xmlns="https://www.larkoffice.com/sml/2.0">
                  <data><shape type="text" topLeftX="10" topLeftY="20" width="300"/></data>
                </slide>
                """,
                encoding="utf-8",
            )

            completed = subprocess.run(
                [sys.executable, str(script_path), "--input", str(input_path)],
                cwd=temp_dir,
                capture_output=True,
                check=False,
                text=True,
            )

        result = json.loads(completed.stdout)
        issue = result["slides"][0]["errors"][0]
        self.assertEqual(completed.returncode, 1)
        self.assertEqual(completed.stderr, "")
        self.assertEqual(issue["code"], "sxsd_missing_required_attr")
        self.assertEqual(issue["path"], "slide/data/shape")
        self.assertEqual(issue["attr"], "height")
        self.assertEqual(issue["target"]["slide_number"], 1)
        self.assertTrue(issue["hint"])

    def test_xml_text_overlap_lint_accepts_inline_fixture_xml_samples(self) -> None:
        samples = {
            "image-led-cover": """
                <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
                  <slide xmlns="https://www.larkoffice.com/sml/2.0">
                    <style><fill><fillColor color="rgb(15,23,42)"/></fill></style>
                    <data>
                      <img src="tok" topLeftX="560" topLeftY="0" width="400" height="540"/>
                      <shape type="text" topLeftX="64" topLeftY="150" width="420" height="70">
                        <content textType="title"><p><span fontSize="42">Quarterly Review</span></p></content>
                      </shape>
                      <shape type="text" topLeftX="64" topLeftY="235" width="420" height="36">
                        <content textType="sub-headline"><p><span fontSize="20">Focus, progress, and next steps</span></p></content>
                      </shape>
                    </data>
                  </slide>
                </presentation>
            """,
            "content-grid": """
                <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
                  <slide xmlns="https://www.larkoffice.com/sml/2.0">
                    <data>
                      <shape type="text" topLeftX="60" topLeftY="44" width="620" height="46">
                        <content textType="title"><p><span fontSize="30">Execution Snapshot</span></p></content>
                      </shape>
                      <shape type="rect" topLeftX="60" topLeftY="126" width="250" height="150"/>
                      <shape type="text" topLeftX="84" topLeftY="152" width="200" height="36">
                        <content textType="headline"><p><span fontSize="22">Plan</span></p></content>
                      </shape>
                      <shape type="rect" topLeftX="355" topLeftY="126" width="250" height="150"/>
                      <shape type="text" topLeftX="379" topLeftY="152" width="200" height="36">
                        <content textType="headline"><p><span fontSize="22">Build</span></p></content>
                      </shape>
                      <shape type="rect" topLeftX="650" topLeftY="126" width="250" height="150"/>
                      <shape type="text" topLeftX="674" topLeftY="152" width="200" height="36">
                        <content textType="headline"><p><span fontSize="22">Launch</span></p></content>
                      </shape>
                    </data>
                  </slide>
                </presentation>
            """,
        }
        self.assertTrue(samples)
        for sample_name, sample_xml in samples.items():
            with self.subTest(sample=sample_name):
                result = xml_text_overlap_lint.lint_xml(
                    sample_xml,
                    sample_name,
                )
                self.assertNoXmlTextOverlapLintErrors(result, sample_name)

    def test_lint_xml_reports_unescaped_ampersand_in_text(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <content textType="body"><p>Q&A</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = result["issues"][0]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issue["code"], "xml_not_well_formed")
        self.assertIsInstance(issue["line"], int)
        self.assertIsInstance(issue["column"], int)
        self.assertIn("Q&A", issue["context"])
        self.assertIn("&amp;", issue["hint"])

    def test_lint_xml_reports_unescaped_ampersand_in_attribute(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <content textType="body"><p><a href="https://example.com/?a=1&b=2">link</a></p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = result["issues"][0]
        self.assertEqual(issue["code"], "xml_not_well_formed")
        self.assertIn("attribute", issue["hint"])
        self.assertIn("a=1&amp;b=2", issue["hint"])

    def test_lint_xml_reports_mismatched_xml_tag(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <content textType="body"><p>Broken XML</content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = result["issues"][0]
        self.assertEqual(result["summary"]["slide_count"], 0)
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issue["code"], "xml_not_well_formed")
        self.assertIsInstance(issue["line"], int)
        self.assertIsInstance(issue["column"], int)
        self.assertIn("Broken XML", issue["context"])
        self.assertEqual(issue["related_objects"], [])
        self.assertNotIn("Locate via related_objects[].xml_path.", issue["hint"])

    def test_lint_xml_rejects_prefixed_sml_tags(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <ns0:slide xmlns:ns0="https://www.larkoffice.com/sml/2.0">
              <ns0:data>
                <sml:shape xmlns:sml="https://www.larkoffice.com/sml/2.0" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <sml:content textType="body"><sml:p>Prefixed SML</sml:p></sml:content>
                </sml:shape>
              </ns0:data>
            </ns0:slide>
            """
        )
        issues = result["issues"]
        self.assertEqual(result["summary"]["error_count"], 5)
        self.assertEqual([issue["code"] for issue in issues], ["sml_prefixed_tag"] * 5)
        self.assertEqual(issues[0]["tag"], "ns0:slide")
        self.assertIn("default namespace", issues[0]["hint"])
        self.assertEqual({issue["namespace"] for issue in issues}, {SML_NAMESPACE})

    def test_lint_xml_reports_bound_namespace_for_prefixed_sml_tags(self) -> None:
        for namespace in (
            "http://www.larkoffice.com/sml/2.0",
            "/sml/2.0",
            SML_NAMESPACE,
        ):
            with self.subTest(namespace=namespace):
                result = xml_text_overlap_lint.lint_xml(
                    f"""
                    <sml:slide xmlns:sml="{namespace}">
                      <sml:data>
                        <sml:shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                          <sml:content textType="body"><sml:p>Prefixed SML</sml:p></sml:content>
                        </sml:shape>
                      </sml:data>
                    </sml:slide>
                    """
                )
                issues = result["issues"]
                self.assertEqual([issue["code"] for issue in issues], ["sml_prefixed_tag"] * 5)
                self.assertEqual({issue["namespace"] for issue in issues}, {namespace})

    def test_lint_xml_accepts_server_readback_slide_without_namespace(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide id="server-slide-id">
              <data>
                <shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <content textType="body"><p>Unprefixed SML</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_accepts_server_readback_presentation_short_namespace(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="/sml/2.0" width="960" height="540" id="server-presentation-id">
              <slide id="server-slide-id">
                <data>
                  <shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>Server readback presentation</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 1)
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_accepts_server_readback_presentation_https_namespace(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540" id="server-presentation-id">
              <slide id="server-slide-id">
                <data>
                  <shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>Server readback presentation</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 1)
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_accepts_legacy_http_namespace(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540" id="legacy-http-id">
              <slide id="server-slide-id">
                <data>
                  <shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>Legacy HTTP namespace</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 1)
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_rejects_server_readback_presentation_wrong_namespace(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://example.com/not-sml" width="960" height="540">
              <slide/>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["issues"][0]["code"], "sxsd_invalid_namespace")

    def test_lint_xml_rejects_xml_declaration(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            '<?xml version="1.0"?><slide xmlns="https://www.larkoffice.com/sml/2.0"><data/></slide>'
        )

        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["issues"][0]["code"], "sxsd_unsupported_declaration")

    def test_lint_xml_reports_missing_required_sxsd_attribute(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data><shape type="text" topLeftX="80" topLeftY="80" width="300"/></data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertNotIn("issues", result)
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["code"], "sxsd_missing_required_attr")
        self.assertEqual(issue["attr"], "height")

    def test_lint_xml_rejects_child_order_that_violates_xsd_sequence(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide/>
              <title>Late title</title>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["issues"][0]["code"], "sxsd_invalid_child_order")

    def test_lint_xml_accepts_escaped_entities_without_suspicious_entity_warning(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <content textType="body"><p>Q&amp;A</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertNotIn("issues", result)

    def test_lint_xml_accepts_chinese_full_width_punctuation(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftX="80" topLeftY="80" width="620" height="90">
                  <content textType="body"><p>承诺：按期交付；持续复盘｜风险透明</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_single_slide_reports_out_of_canvas_and_blank_slide_errors(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftX="1000" topLeftY="500" width="120" height="80">
                  <content textType="body"><p>Body text outside the canvas</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["slide_size"], {"width": 960, "height": 540})
        self.assertEqual(result["summary"]["slide_count"], 1)
        self.assertEqual(result["summary"]["error_count"], 2)
        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["errors"]],
            ["shape_out_of_canvas", "blank_slide"],
        )

    def test_lint_xml_preserves_presentation_canvas_and_slide_order(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="1280" height="720">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape id="first" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>First slide</p></content>
                  </shape>
                </data>
              </slide>
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <img id="second" src="tok" topLeftX="100" topLeftY="120" width="240" height="160"/>
                  <shape id="second-shape" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>Second slide</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["slide_size"], {"width": 1280, "height": 720})
        self.assertEqual(result["summary"]["slide_count"], 2)
        self.assertEqual([slide["slide_number"] for slide in result["slides"]], [1, 2])
        self.assertEqual([slide["element_count"] for slide in result["slides"]], [1, 2])
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(result["summary"]["warning_count"], 0)

    def test_lint_xml_handles_self_closing_slide_before_normal_slide(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide/>
              <slide>
                <data>
                  <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>Second slide</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 2)
        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["errors"]],
            ["blank_slide"],
        )
        self.assertEqual(result["slides"][1]["status"], "passed")

    def test_lint_xml_keeps_trailing_self_closing_slide(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide>
                <data>
                  <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>First slide</p></content>
                  </shape>
                </data>
              </slide>
              <slide/>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 2)
        self.assertEqual(
            [issue["code"] for issue in result["slides"][1]["errors"]],
            ["blank_slide"],
        )

    def test_lint_xml_ignores_slide_markup_inside_xml_comments(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide>
                <data>
                  <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="body"><p>Real slide</p></content>
                  </shape>
                </data>
              </slide>
              <!-- Old draft: <slide id="ghost"/> -->
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 1)
        self.assertEqual(result["slides"][0]["status"], "passed")

    def test_lint_xml_ignores_invalid_slide_markup_inside_xml_comments(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide><data/></slide>
              <!-- Old draft: <slide bogus="x"/> -->
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 1)
        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["errors"]],
            ["blank_slide"],
        )

    def test_lint_xml_skips_only_invalid_slide_and_continues_geometry_checks(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide>
                <data>
                  <shape id="invalid" type="text" topLeftX="1000" topLeftY="80" width="300">
                    <content textType="body"><p>Missing height</p></content>
                  </shape>
                </data>
              </slide>
              <slide>
                <data>
                  <shape id="outside" type="text" topLeftX="1000" topLeftY="80" width="120" height="60">
                    <content textType="body"><p>Outside canvas</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 2)
        self.assertEqual(result["slides"][0]["element_count"], 0)
        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["errors"]],
            ["sxsd_missing_required_attr"],
        )
        self.assertNotIn(
            "shape_out_of_canvas",
            [issue["code"] for issue in result["slides"][0]["issues"]],
        )
        self.assertIn(
            "shape_out_of_canvas",
            [issue["code"] for issue in result["slides"][1]["errors"]],
        )

    def test_lint_xml_scopes_invalid_slide_root_attribute_to_that_slide(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide bogusAttr="oops">
                <data><shape type="rect" topLeftX="80" topLeftY="80" width="100" height="100"/></data>
              </slide>
              <slide>
                <data><shape type="rect" topLeftX="1000" topLeftY="80" width="100" height="100"/></data>
              </slide>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 2)
        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["errors"]],
            ["sxsd_unsupported_attr"],
        )
        self.assertIn(
            "shape_out_of_canvas",
            [issue["code"] for issue in result["slides"][1]["errors"]],
        )

    def test_lint_xml_allows_svg_subtree_inside_embed(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="http://www.larkoffice.com/sml/2.0">
              <data>
                <embed topLeftX="80" topLeftY="120" width="240" height="140">
                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140">
                    <rect x="10" y="10" width="220" height="120" rx="12" fill="#EFF6FF"/>
                    <circle cx="70" cy="70" r="34" fill="#2563EB"/>
                    <text x="130" y="76" font-size="18" fill="#1E3A8A">SVG OK</text>
                    <foreignObject x="0" y="0" width="1" height="1">
                      <embed xmlns="http://www.w3.org/1999/xhtml">
                        <rect xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>
                      </embed>
                    </foreignObject>
                  </svg>
                </embed>
              </data>
            </slide>
            """
        )
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertNotIn("sxsd_unsupported_tag", codes)
        self.assertNotIn("sxsd_unsupported_attr", codes)
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_enforces_embed_svg_contract(self) -> None:
        cases = [
            ("missing SVG", "", "sxsd_missing_required_child"),
            ("wrong namespace", '<svg viewBox="0 0 20 20"/>', "sxsd_unsupported_tag"),
            (
                "multiple SVG roots",
                '<svg xmlns="http://www.w3.org/2000/svg"/><svg xmlns="http://www.w3.org/2000/svg"/>',
                "sxsd_too_many_children",
            ),
            (
                "non-SVG root element",
                '<rect xmlns="http://www.w3.org/2000/svg" width="20" height="20"/>',
                "sxsd_unexpected_child",
            ),
            (
                "SVG after reflection",
                '<reflection/><svg xmlns="http://www.w3.org/2000/svg"/>',
                "sxsd_invalid_child_order",
            ),
        ]
        for name, children, expected_code in cases:
            with self.subTest(name=name):
                result = xml_text_overlap_lint.lint_xml(
                    f"""
                    <slide xmlns="http://www.larkoffice.com/sml/2.0">
                      <data>
                        <embed topLeftX="80" topLeftY="120" width="240" height="140">
                          {children}
                        </embed>
                      </data>
                    </slide>
                    """
                )
                codes = [issue["code"] for issue in result["slides"][0]["issues"]]
                self.assertIn(expected_code, codes)

    def test_lint_xml_enforces_embed_svg_root_for_readback_namespaces(self) -> None:
        document_templates = [
            ("bare slide", "<slide>{content}</slide>"),
            (
                "short namespace",
                '<presentation xmlns="/sml/2.0" width="960" height="540">'
                "<slide>{content}</slide>"
                "</presentation>",
            ),
            (
                "HTTPS namespace",
                '<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">'
                "<slide>{content}</slide>"
                "</presentation>",
            ),
        ]
        embed_template = """
            <data>
              <embed topLeftX="80" topLeftY="120" width="240" height="140">
                {svg_root}
              </embed>
            </data>
        """
        roots = [
            ("valid SVG root", '<svg xmlns="http://www.w3.org/2000/svg"/>', False),
            (
                "non-SVG root element",
                '<rect xmlns="http://www.w3.org/2000/svg" width="20" height="20"/>',
                True,
            ),
        ]

        for document_name, document_template in document_templates:
            for root_name, svg_root, should_error in roots:
                with self.subTest(document=document_name, root=root_name):
                    content = embed_template.format(svg_root=svg_root)
                    result = xml_text_overlap_lint.lint_xml(
                        document_template.format(content=content)
                    )
                    codes = [
                        issue["code"]
                        for slide in result["slides"]
                        for issue in slide["issues"]
                    ]
                    if should_error:
                        self.assertIn("sxsd_unexpected_child", codes)
                    else:
                        self.assertNotIn("sxsd_unexpected_child", codes)
                        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_reports_sxsd_unsupported_tag_with_alias_hint(self) -> None:
        cases = [
            ("textbox", '<textbox topLeftX="80" topLeftY="80" width="300" height="60">Text</textbox>', '<shape type="text">'),
            ("image", '<image src="tok" topLeftX="80" topLeftY="80" width="300" height="180"/>', "<img>"),
        ]
        for tag_name, element_xml, expected_hint in cases:
            with self.subTest(tag=tag_name):
                result = xml_text_overlap_lint.lint_xml(
                    f"""
                    <slide xmlns="https://www.larkoffice.com/sml/2.0">
                      <data>{element_xml}</data>
                    </slide>
                    """
                )
                slide_issues = result["slides"][0]["issues"]
                issue = next(
                    issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_tag"
                )
                self.assertEqual(result["summary"]["error_count"], 1)
                self.assertEqual(
                    [reported["code"] for reported in slide_issues],
                    ["sxsd_unsupported_tag"],
                )
                self.assertEqual(issue["code"], "sxsd_unsupported_tag")
                self.assertEqual(issue["tag"], tag_name)
                self.assertIn(expected_hint, issue["hint"])

    def test_lint_xml_reports_sxsd_unsupported_attr_with_alias_hint(self) -> None:
        cases = [
            ("shape", "x", "topLeftX", '<shape type="text" x="80" topLeftY="80" width="300" height="60"><content><p>Text</p></content></shape>'),
            ("shape", "heigth", "height", '<shape type="text" topLeftX="80" topLeftY="80" width="300" heigth="60"><content><p>Text</p></content></shape>'),
            ("content", "fontColor", "color", '<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60"><content fontColor="rgba(0, 0, 0, 1)"><p>Text</p></content></shape>'),
        ]
        for tag_name, attr_name, expected_attr, element_xml in cases:
            with self.subTest(attr=attr_name):
                result = xml_text_overlap_lint.lint_xml(
                    f"""
                    <slide xmlns="https://www.larkoffice.com/sml/2.0">
                      <data>{element_xml}</data>
                    </slide>
                    """
                )
                slide_issues = result["slides"][0]["issues"]
                issue = next(
                    issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_attr"
                )
                self.assertEqual(result["summary"]["error_count"], 1)
                self.assertEqual(
                    [reported["code"] for reported in slide_issues],
                    ["sxsd_unsupported_attr"],
                )
                self.assertEqual(issue["code"], "sxsd_unsupported_attr")
                self.assertEqual(issue["tag"], tag_name)
                self.assertEqual(issue["attr"], attr_name)
                self.assertIn(expected_attr, issue["hint"])

    def test_lint_xml_keeps_unrelated_unsupported_and_missing_attrs(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" bogus="x" topLeftX="80" topLeftY="80" width="300"/>
              </data>
            </slide>
            """
        )

        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["issues"]],
            ["sxsd_unsupported_attr", "sxsd_missing_required_attr"],
        )

    def test_lint_xml_keeps_missing_attrs_when_suggestion_is_ambiguous(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeft="80" width="300" height="60"/>
              </data>
            </slide>
            """
        )

        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["issues"]],
            [
                "sxsd_unsupported_attr",
                "sxsd_missing_required_attr",
                "sxsd_missing_required_attr",
            ],
        )

    def test_lint_xml_suppresses_only_missing_attr_that_resolves_ambiguous_suggestion(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftXX="80" topLeftY="80" width="300" height="60"/>
              </data>
            </slide>
            """
        )

        self.assertEqual(
            [
                (issue["code"], issue.get("attr"))
                for issue in result["slides"][0]["issues"]
            ],
            [("sxsd_unsupported_attr", "topLeftXX")],
        )

    def test_lint_xml_ignores_server_filled_id_attrs(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide id="server-slide-id" xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="server-shape-id" type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
                  <fill id="server-fill-id" unexpected="value">
                    <fillColor color="rgba(255, 255, 255, 1)"/>
                  </fill>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(
            [(issue["tag"], issue["attr"]) for issue in result["slides"][0]["issues"]],
            [("fill", "unexpected")],
        )

    def test_lint_xml_ignores_chart_roundtrip_attrs(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <chart updated="true" topLeftX="80" topLeftY="80" width="300" height="160">
                  <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                  <chartData isStaticData="true">
                    <dim1><chartField name="category" valueType="string">A</chartField></dim1>
                    <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                  </chartData>
                </chart>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_chart_parsed_values_roundtrip_tags(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <chart topLeftX="80" topLeftY="80" width="300" height="160">
                  <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                  <chartData isStaticData="true">
                    <dim1>
                      <chartField name="category" valueType="string">
                        A<chartParsedValues>A</chartParsedValues>
                      </chartField>
                    </dim1>
                    <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                  </chartData>
                </chart>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_chart_parsed_values_roundtrip_tag(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <chart topLeftX="80" topLeftY="80" width="300" height="160">
                  <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                  <chartData>
                    <dim1>
                      <chartField name="category" valueType="string">
                        Africa<chartParsedValues>Africa</chartParsedValues>
                      </chartField>
                    </dim1>
                    <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                  </chartData>
                </chart>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertNotIn("issues", result)

    def test_lint_xml_rejects_chart_parsed_values_outside_chart_field(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
                  <chartParsedValues>unexpected</chartParsedValues>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["issues"]],
            ["sxsd_unsupported_tag"],
        )
        self.assertEqual(
            result["slides"][0]["issues"][0]["path"],
            "slide/data/shape/chartParsedValues",
        )

    def test_lint_xml_limits_chart_roundtrip_attrs_to_matching_tags(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <chart isStaticData="true" topLeftX="80" topLeftY="80" width="300" height="160">
                  <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                  <chartData updated="true">
                    <dim1><chartField name="category" valueType="string">A</chartField></dim1>
                    <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                  </chartData>
                </chart>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 2)
        slide_issues = result["slides"][0]["issues"]
        self.assertEqual(
            {(issue["tag"], issue["attr"]) for issue in slide_issues},
            {("chart", "isStaticData"), ("chartData", "updated")},
        )
        self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in slide_issues))

    def test_lint_xml_reports_gradient_shorthand_attrs_on_fill_color(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
                  <fill>
                    <fillColor
                      type="gradient"
                      color1="rgba(255, 0, 0, 1)"
                      color2="rgba(0, 0, 255, 1)"
                      angle="45"
                      stop1="0%"
                      stop2="100%"/>
                  </fill>
                </shape>
              </data>
            </slide>
            """
        )
        slide_issues = result["slides"][0]["issues"]
        unsupported_attrs = {issue["attr"] for issue in slide_issues}
        self.assertEqual(result["summary"]["error_count"], 6)
        self.assertEqual(
            unsupported_attrs,
            {"type", "color1", "color2", "angle", "stop1", "stop2"},
        )
        self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in slide_issues))
        self.assertTrue(all(issue["tag"] == "fillColor" for issue in slide_issues))

    def test_lint_xml_accepts_chart_field_simple_content_attrs(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <chart topLeftX="80" topLeftY="80" width="520" height="320">
                  <chartPlotArea>
                    <chartPlot type="line"/>
                  </chartPlotArea>
                  <chartData>
                    <dim1>
                      <chartField name="month" valueType="string">Jan, Feb</chartField>
                    </dim1>
                    <dim2>
                      <chartField name="value" valueType="number">1, 2</chartField>
                    </dim2>
                  </chartData>
                </chart>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertNotIn("issues", result)

    def test_lint_xml_does_not_load_iconpark_index_without_icons(self) -> None:
        original_loader = xml_text_overlap_lint.load_iconpark_icon_types

        def fail_if_loaded() -> set[str]:
            raise AssertionError("iconpark index should not be loaded without <icon iconType>")

        xml_text_overlap_lint.load_iconpark_icon_types = fail_if_loaded
        try:
            result = xml_text_overlap_lint.lint_xml(
                """
                <slide xmlns="https://www.larkoffice.com/sml/2.0">
                  <data>
                    <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                      <content><p>No icons here</p></content>
                    </shape>
                  </data>
                </slide>
                """
            )
        finally:
            xml_text_overlap_lint.load_iconpark_icon_types = original_loader

        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertNotIn("issues", result)

    def test_lint_xml_accepts_iconpark_icon_type_from_index(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48">
                  <fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertNotIn("issues", result)

    def test_lint_xml_reports_icon_missing_fill_color(self) -> None:
        cases = [
            '<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48"/>',
            '<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48"><fill/></icon>',
            (
                '<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48">'
                "<fill><fillColor/></fill></icon>"
            ),
        ]
        for icon_xml in cases:
            with self.subTest(icon=icon_xml):
                result = xml_text_overlap_lint.lint_xml(
                    f"""
                    <slide xmlns="https://www.larkoffice.com/sml/2.0">
                      <data>{icon_xml}</data>
                    </slide>
                    """
                )

                issue = result["issues"][0]
                self.assertEqual(result["summary"]["error_count"], 1)
                self.assertEqual(issue["code"], "icon_missing_fill_color")
                self.assertEqual(issue["tag"], "icon")

    def test_lint_xml_reports_icon_transparent_fill_color(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48">
                  <fill><fillColor color="rgba(37, 99, 235, 0)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )
        issue = result["issues"][0]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issue["code"], "icon_transparent_fill_color")
        self.assertEqual(issue["tag"], "icon")
        self.assertEqual(issue["attr"], "fillColor")

    def test_lint_xml_reports_iconpark_icon_type_outside_index(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <icon iconType="iconpark/Base/settng.svg" topLeftX="80" topLeftY="80" width="48" height="48">
                  <fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )
        issue = result["issues"][0]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issue["code"], "iconpark_unsupported_icon_type")
        self.assertEqual(issue["tag"], "icon")
        self.assertEqual(issue["attr"], "iconType")
        self.assertEqual(issue["iconType"], "iconpark/Base/settng.svg")
        self.assertIn("iconpark-index.json", issue["hint"])
        self.assertIn("iconpark/Base/setting.svg", issue["hint"])

    def test_lint_xml_skips_iconpark_validation_inside_embedded_svg(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="http://www.larkoffice.com/sml/2.0">
              <data>
                <embed topLeftX="80" topLeftY="120" width="240" height="140">
                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140">
                    <rect x="10" y="10" width="220" height="120" fill="#EFF6FF"/>
                    <icon iconType="not-an-iconpark-name"/>
                    <foreignObject x="0" y="0" width="60" height="60">
                      <icon xmlns="http://www.w3.org/1999/xhtml" iconType="not-an-iconpark-name"/>
                    </foreignObject>
                  </svg>
                </embed>
              </data>
            </slide>
            """
        )
        codes = [issue["code"] for issue in result["document"]["errors"]]
        self.assertNotIn("iconpark_unsupported_icon_type", codes)
        self.assertNotIn("icon_missing_fill_color", codes)
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_detects_overlapping_text_boxes(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                    <content textType="title"><p>Title</p></content>
                  </shape>
                  <shape type="text" topLeftX="80" topLeftY="80" width="300" height="80">
                    <content textType="body"><p>Body</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["summary"]["warning_count"], 0)
        self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")

    def test_lint_xml_detects_current_itinerary_cjk_caption_occlusion(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide id="pQO" xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape width="190" height="80" topLeftX="580" topLeftY="170" presetHandlers="0" type="rect" id="blI">
                  <fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
                  <border color="rgba(220, 205, 185, 1)" width="1"/>
                  <content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
                </shape>
                <shape width="160" height="25" topLeftX="595" topLeftY="180" type="text" id="blX">
                  <content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>日照金山</p></content>
                </shape>
                <shape width="160" height="40" topLeftX="595" topLeftY="205" type="text" id="blY">
                  <content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>清晨躺在床上看玉龙雪山日照金山奇观</p></content>
                </shape>
                <shape width="180" height="80" topLeftX="730" topLeftY="170" presetHandlers="0" type="rect" id="blH">
                  <fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
                  <border color="rgba(220, 205, 185, 1)" width="1"/>
                  <content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
                </shape>
                <shape width="150" height="25" topLeftX="745" topLeftY="180" type="text" id="blp">
                  <content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>午餐返程</p></content>
                </shape>
                <shape width="150" height="40" topLeftX="745" topLeftY="205" type="text" id="blV">
                  <content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>享用特色午餐，带着美好回忆返程</p></content>
                </shape>
                <shape width="190" height="80" topLeftX="580" topLeftY="310" presetHandlers="0" type="rect" id="blP">
                  <fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
                  <border color="rgba(220, 205, 185, 1)" width="1"/>
                  <content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
                </shape>
                <shape width="160" height="25" topLeftX="595" topLeftY="320" type="text" id="blG">
                  <content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>高路徒步</p></content>
                </shape>
                <shape width="160" height="40" topLeftX="595" topLeftY="345" type="text" id="blQ">
                  <content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>经典高路徒步，28道拐，龙洞瀑布，中虎跳峡</p></content>
                </shape>
                <shape width="180" height="80" topLeftX="730" topLeftY="310" presetHandlers="0" type="rect" id="blw">
                  <fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
                  <border color="rgba(220, 205, 185, 1)" width="1"/>
                  <content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
                </shape>
                <shape width="150" height="25" topLeftX="745" topLeftY="320" type="text" id="blZ">
                  <content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>伴手礼</p></content>
                </shape>
                <shape width="150" height="40" topLeftX="745" topLeftY="345" type="text" id="blS">
                  <content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>酒店精心准备的归途伴手礼，留下难忘纪念</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        overlap_pairs = {tuple(issue["elements"]) for issue in result["slides"][0]["issues"]}
        self.assertEqual(result["summary"]["error_count"], 2)
        self.assertIn(("blY", "blV"), overlap_pairs)
        self.assertIn(("blQ", "blS"), overlap_pairs)

    def test_lint_xml_detects_horizontal_text_overflow_across_declared_box_gap(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="source" type="text" topLeftX="80" topLeftY="100" width="160" height="40">
                  <content fontSize="18" wrap="false"><p>这是一个足够长的中文文本用于检测跨越间隙的横向溢出</p></content>
                </shape>
                <shape id="target" type="text" topLeftX="260" topLeftY="100" width="160" height="40">
                  <content fontSize="18"><p>目标</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["summary"]["warning_count"], 0)
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["code"], "bbox_overlap")
        self.assertEqual(issue["elements"], ["source", "target"])
        self.assertGreater(issue["measurement"]["intersection_area"], 0)
        self.assertIsNotNone(issue.get("hint"))

    def test_lint_xml_allows_horizontal_text_with_default_wrap(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="source" type="text" topLeftX="80" topLeftY="100" width="160" height="40">
                  <content fontSize="18"><p>这是一个足够长的中文文本用于检测默认自动换行</p></content>
                </shape>
                <shape id="target" type="text" topLeftX="260" topLeftY="100" width="160" height="40">
                  <content fontSize="18"><p>目标</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["summary"]["warning_count"], 0)
        self.assertEqual(result["slides"][0]["issues"][0]["code"], "text_may_overflow_shape")
        self.assertEqual(result["slides"][0]["issues"][0]["level"], "error")
        self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["source"])

    def test_lint_xml_reports_text_out_of_canvas_and_warns_for_text_height(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape type="text" topLeftX="80" topLeftY="80" width="180" height="20">
                    <content textType="body" fontSize="18"><p>This paragraph is intentionally much longer than the box can safely contain.</p></content>
                  </shape>
                  <shape type="text" topLeftX="1000" topLeftY="500" width="120" height="80">
                    <content textType="body"><p>Body text outside the canvas</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(result["summary"]["error_count"], 2)
        self.assertEqual(result["summary"]["warning_count"], 0)
        self.assertEqual(issue["code"], "shape_out_of_canvas")
        self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 160, "bottom": 40})

    def test_lint_xml_warns_when_text_may_overflow_its_own_shape(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="overflowing" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
                  <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
                    <p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
                  </content>
                </shape>
                <shape id="fitting" type="text" topLeftX="480" topLeftY="80" width="360" height="120">
                  <content fontSize="20" lineSpacing="multiple:1.5">
                    <p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
                  </content>
                </shape>
                <shape id="auto-fit" type="text" topLeftX="80" topLeftY="240" width="360" height="80">
                  <content fontSize="20" lineSpacing="multiple:1.5" autoFit="normal-auto-fit">
                    <p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
                  </content>
                </shape>
                <shape id="shape-auto-fit" type="text" topLeftX="480" topLeftY="240" width="360" height="30">
                  <content fontSize="20" lineSpacing="multiple:1.5" autoFit="shape-auto-fit">
                    <p>第一段</p><p>第二段</p><p>第三段</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        issues = result["slides"][0]["issues"]
        overflow_issues = [issue for issue in issues if issue["code"] == "text_may_overflow_shape"]
        self.assertEqual(result["summary"]["error_count"], 1)
        overflow_ids = {issue["elements"][0] for issue in overflow_issues}
        self.assertIn("overflowing", overflow_ids)
        self.assertNotIn("auto-fit", overflow_ids)
        self.assertNotIn("shape-auto-fit", overflow_ids)
        self.assertNotIn("fitting", overflow_ids)
        overflowing_issue = next(issue for issue in overflow_issues if issue["elements"] == ["overflowing"])
        self.assertEqual(overflowing_issue["line_count"], 4)
        self.assertEqual(overflowing_issue["estimated_height"], 110)
        self.assertEqual(overflowing_issue["available_height"], 80)
        self.assertEqual(overflowing_issue["overflow"], 30)
        self.assertIn('wrap="true" autoFit="normal-auto-fit"', overflowing_issue["message"])

    def test_lint_xml_uses_fixed_line_spacing_for_text_height_warning(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="fixed-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
                  <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
                    <p>第一段</p><p>第二段</p><p>第三段</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(result["summary"]["warning_count"], 1)
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(issue["level"], "warning")
        self.assertEqual(issue["line_height"], 20)
        self.assertEqual(issue["estimated_height"], 60)
        self.assertEqual(issue["overflow"], 10)

    def test_lint_xml_ignores_subpixel_text_height_overflow_tolerance(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="minor-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="39.8">
                  <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
                    <p>第一段</p><p>第二段</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(overflow_issues, [])

    def test_lint_xml_allows_single_line_width_estimation_jitter(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="metric" type="text" topLeftX="80" topLeftY="80" width="152" height="54">
                  <content fontSize="36" lineSpacing="multiple:1.2" autoFit="no-auto-fit"><p>4.16万亿</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(overflow_issues, [])

    def test_lint_xml_allows_short_metric_text_with_separators_as_single_line(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="metric" type="text" topLeftX="80" topLeftY="80" width="150" height="50">
                  <content textType="title" fontSize="36" autoFit="no-auto-fit"><p>4.16万亿</p></content>
                </shape>
                <shape id="table-number" type="text" topLeftX="80" topLeftY="160" width="25" height="20">
                  <content fontSize="10" textAlign="center" autoFit="no-auto-fit"><p>1,380</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(overflow_issues, [])

    def test_lint_xml_reports_plain_short_metric_when_it_wraps(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="plain-age" type="text" topLeftX="80" topLeftY="80" width="50" height="80">
                  <content textType="title" fontSize="36" bold="true" autoFit="no-auto-fit"><p>82岁</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(len(overflow_issues), 1)
        self.assertEqual(overflow_issues[0]["elements"], ["plain-age"])

    def test_lint_xml_allows_centered_short_label_near_fit_as_single_line(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="centered-label" type="text" topLeftX="80" topLeftY="80" width="200" height="30">
                  <content fontSize="14" bold="true" textAlign="center" autoFit="no-auto-fit">
                    <p>参数服务器 (Parameter Server)</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(overflow_issues, [])

    def test_lint_xml_allows_headline_near_fit_as_single_line(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="headline" type="text" topLeftX="80" topLeftY="80" width="700" height="50">
                  <content textType="headline" fontSize="26" bold="true" lineSpacing="multiple:1.3" autoFit="no-auto-fit">
                    <p>全球半导体市场规模持续高速增长，AI驱动新一轮景气周期</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(overflow_issues, [])

    def test_lint_xml_allows_dense_body_line_spacing_estimation_slack(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="140">
                  <content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
                    <p>总体目标：</p>
                    <p>建立深度神经网络高效训练的统一理论框架，实现训练效率与模型性能的协同优化。</p>
                    <p>具体目标：</p>
                    <p>提出自适应优化算法，收敛速度提升 2-3 倍</p>
                    <p>实现结构化压缩方法，模型体积减少 10 倍以上</p>
                    <p>构建分布式训练策略，64 GPU 加速比 &gt; 50x</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(overflow_issues, [])

    def test_lint_xml_reports_dense_body_when_adjusted_height_still_overflows(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="100">
                  <content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
                    <p>总体目标：</p>
                    <p>建立深度神经网络高效训练的统一理论框架，实现训练效率与模型性能的协同优化。</p>
                    <p>具体目标：</p>
                    <p>提出自适应优化算法，收敛速度提升 2-3 倍</p>
                    <p>实现结构化压缩方法，模型体积减少 10 倍以上</p>
                    <p>构建分布式训练策略，64 GPU 加速比 &gt; 50x</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(len(overflow_issues), 1)
        self.assertEqual(overflow_issues[0]["elements"], ["dense-body"])

    def test_lint_xml_reports_letter_spaced_caption_near_fit(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="caption" type="text" topLeftX="80" topLeftY="80" width="120" height="20">
                  <content textType="caption" fontSize="11" letterSpacing="1" autoFit="no-auto-fit">
                    <p>RISKS &amp; CHALLENGES</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(len(overflow_issues), 1)
        self.assertEqual(overflow_issues[0]["elements"], ["caption"])

    def test_lint_xml_reports_micro_caption_when_wrapping_overflows(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="micro-caption" type="text" topLeftX="80" topLeftY="60" width="200" height="16">
                  <content textType="caption" fontSize="3" lineSpacing="multiple:1.3" letterSpacing="160" autoFit="no-auto-fit">
                    <p>MARKET INSIGHT · 市场洞察</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        overflow_issues = [
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        ]
        self.assertEqual(len(overflow_issues), 1)
        self.assertEqual(overflow_issues[0]["elements"], ["micro-caption"])

    def test_lint_xml_text_may_overflow_shape_upgrades_to_error_above_threshold(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="just-warning" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
                  <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
                    <p>第一段</p><p>第二段</p><p>第三段</p>
                  </content>
                </shape>
                <shape id="error-overflow" type="text" topLeftX="80" topLeftY="200" width="360" height="30">
                  <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
                    <p>第一段</p><p>第二段</p><p>第三段</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        issues = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
        self.assertEqual(issues["just-warning"]["level"], "warning")
        self.assertEqual(issues["just-warning"]["overflow"], 10)
        self.assertEqual(issues["error-overflow"]["level"], "error")
        self.assertEqual(issues["error-overflow"]["overflow"], 30)
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["summary"]["warning_count"], 1)

    def test_lint_xml_text_may_overflow_shape_downgrades_background_decoration_to_info(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
                  <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
                </shape>
                <shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
                  <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issues = {
            issue["elements"][0]: issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape"
        }
        self.assertEqual(issues["bg-deco"]["level"], "info")
        self.assertEqual(result["summary"]["warning_count"], 0)
        self.assertEqual(result["summary"]["info_count"], 1)
        self.assertEqual(result["slides"][0]["infos"], [issues["bg-deco"]])
        self.assertIn("background decoration", issues["bg-deco"]["message"])

    def test_lint_xml_reports_shape_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="ghost-number" type="text" topLeftX="-60" topLeftY="30" width="360" height="180" alpha="0.2">
                  <content fontSize="160" lineSpacing="fixed:160" wrap="false"><p>01</p></content>
                </shape>
                <shape id="title" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
                  <content fontSize="30" lineSpacing="fixed:36"><p>Annual Review</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertIn("shape_out_of_canvas", codes)
        self.assertNotIn("bbox_overlap", codes)

    def test_lint_xml_reports_content_color_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="ghost-year" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
                  <content fontSize="140" color="rgba(0,0,0,0.2)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
                </shape>
                <shape id="headline" type="text" topLeftX="700" topLeftY="70" width="220" height="80">
                  <content fontSize="28" lineSpacing="fixed:34"><p>Forecast</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertIn("shape_out_of_canvas", codes)
        self.assertNotIn("bbox_overlap", codes)

    def test_lint_xml_reports_faint_medium_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="medium-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.32">
                  <content fontSize="40" lineSpacing="fixed:40" wrap="false"><p>OFF EDGE</p></content>
                </shape>
                <shape id="caption" type="text" topLeftX="760" topLeftY="315" width="180" height="36">
                  <content fontSize="16" lineSpacing="fixed:20"><p>Readable caption</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertIn("shape_out_of_canvas", codes)
        self.assertNotIn("bbox_overlap", codes)

    def test_lint_xml_allows_ghost_text_image_overlap(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
                  <content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
                </shape>
                <img id="photo" src="token" topLeftX="160" topLeftY="70" width="260" height="160"/>
                <shape id="title" type="text" topLeftX="610" topLeftY="95" width="320" height="60">
                  <content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertNotIn("image_covers_text", codes)
        self.assertNotIn("bbox_overlap", codes)

    def test_lint_slide_allows_ghost_text_whiteboard_overlap(self) -> None:
        result = xml_text_overlap_lint.lint_slide(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <whiteboard id="board" topLeftX="180" topLeftY="70" width="420" height="300"/>
                <shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
                  <content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
                </shape>
                <shape id="title" type="text" topLeftX="610" topLeftY="95" width="220" height="60">
                  <content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
                </shape>
              </data>
            </slide>
            """,
            1,
        )
        codes = [issue["code"] for issue in result["issues"]]
        self.assertNotIn("whiteboard_external_overlap", codes)

    def test_lint_xml_reports_faint_ghost_text_out_of_canvas_without_area_threshold(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="small-ghost" type="text" topLeftX="940" topLeftY="300" width="40" height="40" alpha="0.32">
                  <content fontSize="36" lineSpacing="fixed:36" wrap="false"><p>土</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")

    def test_lint_xml_keeps_out_of_canvas_error_for_medium_text_without_faint_alpha(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="medium-not-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.36">
                  <content fontSize="54" lineSpacing="fixed:54" wrap="false"><p>OFF EDGE</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
        self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["medium-not-ghost"])

    def test_lint_xml_keeps_out_of_canvas_error_for_half_alpha_large_text(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="half-alpha" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
                  <content fontSize="140" color="rgba(0,0,0,0.5)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
        self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["half-alpha"])

    def test_lint_xml_text_may_overflow_shape_keeps_error_when_alpha_not_low(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="opaque-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.9">
                  <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
                </shape>
                <shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
                  <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["opaque-big"]
        )
        self.assertEqual(issue["level"], "error")

    def test_lint_xml_text_may_overflow_shape_keeps_error_when_no_foreground_text(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="lonely-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
                  <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["lonely-big"]
        )
        self.assertEqual(issue["level"], "error")

    def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_alpha_zero(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
                  <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
                </shape>
                <shape id="transparent-foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60" alpha="0">
                  <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["bg-deco"]
        )
        self.assertEqual(issue["level"], "error")

    def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_is_below_in_order(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
                  <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
                </shape>
                <shape id="top-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
                  <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["top-big"]
        )
        self.assertEqual(issue["level"], "error")

    def test_lint_xml_uses_paragraph_spacing_overrides_for_text_height_warning(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="paragraph-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="30">
                  <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
                    <p lineSpacing="fixed:10" beforeLineSpacing="fixed:5" afterLineSpacing="fixed:5">第一行<br/>第二行</p>
                  </content>
                </shape>
                <shape id="paragraph-fitting" type="text" topLeftX="480" topLeftY="80" width="360" height="40">
                  <content fontSize="20" lineSpacing="multiple:1.5">
                    <p lineSpacing="fixed:10">第一行<br/>第二行<br/>第三行</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )
        issues = result["slides"][0]["issues"]
        self.assertEqual(result["summary"]["warning_count"], 1)
        self.assertEqual(issues[0]["elements"], ["paragraph-overflow"])
        self.assertEqual(issues[0]["line_count"], 2)
        self.assertEqual(issues[0]["line_height"], 10)
        self.assertEqual(issues[0]["estimated_height"], 40)
        self.assertEqual(issues[0]["overflow"], 10)

    def test_lint_xml_uses_letter_spacing_for_text_overflow_warning(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="baseline" type="text" topLeftX="0" topLeftY="0" width="120" height="30">
                  <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p>一二三四五六</p></content>
                </shape>
                <shape id="content-spaced" type="text" topLeftX="200" topLeftY="0" width="120" height="30">
                  <content fontSize="20" lineSpacing="multiple:1.5" letterSpacing="2" autoFit="no-auto-fit"><p>一二三四五六</p></content>
                </shape>
                <shape id="paragraph-spaced" type="text" topLeftX="400" topLeftY="0" width="120" height="30">
                  <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p letterSpacing="2">一二三四五六</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        issues = result["slides"][0]["issues"]
        overflow_ids = [issue["elements"][0] for issue in issues if issue["code"] == "text_may_overflow_shape"]
        self.assertNotIn("baseline", overflow_ids)
        self.assertIn("content-spaced", overflow_ids)
        self.assertIn("paragraph-spaced", overflow_ids)
        by_id = {issue["elements"][0]: issue for issue in issues if issue["code"] == "text_may_overflow_shape"}
        self.assertEqual(by_id["content-spaced"]["line_count"], 2)
        self.assertEqual(by_id["content-spaced"]["estimated_height"], 50)
        self.assertEqual(by_id["content-spaced"]["overflow"], 20)
        self.assertEqual(by_id["paragraph-spaced"]["line_count"], 2)

    def test_strip_xml_paragraphs_preserves_br_as_hard_line_break(self) -> None:
        self.assertEqual(
            xml_text_overlap_lint.strip_xml_paragraphs("<p>第一行<br/>第二行<br />第三行</p>"),
            "第一行\n第二行\n第三行",
        )

    def test_lint_xml_allows_template_style_images_outside_canvas(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <img src="tok" topLeftX="-120" topLeftY="20" width="360" height="360"/>
                  <shape type="text" topLeftX="40" topLeftY="80" width="180" height="80">
                    <content textType="title" fontSize="44"><p>Title</p></content>
                  </shape>
                  <shape type="text" topLeftX="40" topLeftY="120" width="180" height="40">
                    <content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
                  </shape>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(result["summary"]["warning_count"], 0)

    def test_extract_elements_preserves_supported_element_geometry_order_and_text_metadata(self) -> None:
        elements = xml_text_overlap_lint.extract_elements(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <img id="photo" src="tok" topLeftX="10" topLeftY="20" width="100" height="80"/>
                <shape id="headline" type="text" topLeftX="40" topLeftY="60" width="320" height="90">
                  <content textType="headline" textAlign="center" autoFit="normal-auto-fit" fontSize="28">
                    <p><![CDATA[Growth & scale]]></p>
                    <p>Focused execution</p>
                  </content>
                </shape>
                <table id="table" topLeftX="400" topLeftY="60" width="220" height="120"></table>
                <chart id="chart" topLeftX="640" topLeftY="60" width="220" height="120"/>
                <whiteboard id="wb" topLeftX="80" topLeftY="220" width="760" height="240"/>
                <embed id="emb" topLeftX="600" topLeftY="320" width="240" height="140">
                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140"><rect x="0" y="0" width="240" height="140"/></svg>
                </embed>
                <shape id="missing-height" type="text" topLeftX="80" topLeftY="480" width="320">
                  <content><p>Skipped</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        self.assertEqual([element["id"] for element in elements], ["photo", "headline", "table", "chart", "wb", "emb"])
        self.assertEqual([element["kind"] for element in elements], ["img", "shape", "table", "chart", "whiteboard", "embed"])
        self.assertEqual([element["order"] for element in elements], [0, 1, 2, 3, 4, 5])
        self.assertEqual(elements[1]["type"], "text")
        self.assertEqual(elements[1]["textType"], "headline")
        self.assertEqual(elements[1]["textAlign"], "center")
        self.assertEqual(elements[1]["autoFit"], "normal-auto-fit")
        self.assertEqual(elements[1]["fontSize"], 28)
        self.assertEqual(elements[1]["text"], "Growth & scale\nFocused execution")

    def test_lint_xml_ignores_small_out_of_bounds_images(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <img src="tok" topLeftX="-20" topLeftY="20" width="120" height="120"/>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_out_of_canvas_images(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <img src="right" topLeftX="780" topLeftY="0" width="500" height="540"/>
                  <img src="bottom" topLeftX="0" topLeftY="430" width="900" height="280"/>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_full_bleed_images_outside_canvas(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <img src="tok" topLeftX="-80" topLeftY="-20" width="1080" height="600"/>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_reports_text_and_chart_but_not_image_out_of_canvas(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape id="outside-shape" type="text" topLeftX="-10" topLeftY="40" width="50" height="50"/>
                  <img id="outside-img" src="token" topLeftX="120" topLeftY="-20" width="50" height="50"/>
                  <chart id="outside-chart" topLeftX="900" topLeftY="100" width="100" height="100">
                    <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                    <chartData>
                      <dim1><chartField name="category" valueType="string">A</chartField></dim1>
                      <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                    </chartData>
                  </chart>
                </data>
              </slide>
            </presentation>
            """
        )
        issues = result["slides"][0]["issues"]
        self.assertEqual(result["summary"]["error_count"], 2)
        self.assertEqual(
            [(issue["code"], issue["elements"], issue["overflow"]) for issue in issues],
            [
                ("shape_out_of_canvas", ["outside-shape"], {"left": 10, "top": 0, "right": 0, "bottom": 0}),
                ("chart_out_of_canvas", ["outside-chart"], {"left": 0, "top": 0, "right": 40, "bottom": 0}),
            ],
        )

    def test_lint_xml_ignores_line_out_of_canvas(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="body" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <content fontSize="18"><p>Visible content</p></content>
                </shape>
                <line id="connector" startX="80" startY="120" endX="980" endY="120"><border/></line>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(result["slides"][0]["issues"], [])

    def test_lint_xml_reports_horizontal_line_crossing_headline_glyphs(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
                  <content fontSize="60"><p>测试文字 ABC</p></content>
                </shape>
                <line id="strike" startX="80" startY="245" endX="560" endY="245">
                  <border color="rgb(255, 0, 0)" width="4"/>
                </line>
              </data>
            </slide>
            """
        )
        crossing = [
            issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"strike", "title"}
        ]
        self.assertEqual(len(crossing), 1)
        self.assertEqual(crossing[0]["code"], "bbox_overlap")

    def test_lint_xml_reports_vertical_line_crossing_multiline_text(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="col" type="text" topLeftX="700" topLeftY="180" width="240" height="180">
                  <content fontSize="20"><p>第一行文字内容</p><p>第二行文字内容</p><p>第三行文字内容</p></content>
                </shape>
                <line id="vbar" startX="740" startY="170" endX="740" endY="360">
                  <border color="rgb(0, 0, 255)" width="3"/>
                </line>
              </data>
            </slide>
            """
        )
        crossing = [
            issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"vbar", "col"}
        ]
        self.assertEqual(len(crossing), 1)

    def test_lint_xml_reports_diagonal_line_crossing_text_block(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="para" type="text" topLeftX="80" topLeftY="400" width="420" height="140">
                  <content fontSize="18"><p>这是一段测试文字用于验证线条穿过</p></content>
                </shape>
                <line id="diag" startX="80" startY="410" endX="500" endY="530">
                  <border color="rgb(255, 0, 0)" width="3"/>
                </line>
              </data>
            </slide>
            """
        )
        crossing = [
            issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"diag", "para"}
        ]
        self.assertEqual(len(crossing), 1)

    def test_lint_xml_ignores_diagonal_line_whose_bbox_but_not_segment_crosses_text(self) -> None:
        # The diagonal's axis-aligned bounding box overlaps the text, but the segment itself passes
        # through empty space in the opposite corner -- a naive bbox test would false-positive here.
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="corner-text" type="text" topLeftX="80" topLeftY="80" width="120" height="40">
                  <content fontSize="18"><p>corner</p></content>
                </shape>
                <line id="far-diag" startX="700" startY="80" endX="90" endY="500">
                  <border color="rgb(255, 0, 0)" width="3"/>
                </line>
              </data>
            </slide>
            """
        )
        crossing = [
            issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"far-diag", "corner-text"}
        ]
        self.assertEqual(crossing, [])

    def test_lint_xml_ignores_line_touching_text_frame_but_not_glyphs(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="lbl" type="text" topLeftX="80" topLeftY="80" width="300" height="200">
                  <content fontSize="18" verticalAlign="top"><p>短标签</p></content>
                </shape>
                <line id="below" startX="80" startY="270" endX="380" endY="270">
                  <border color="rgb(255, 0, 0)" width="2"/>
                </line>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_invisible_line_crossing_text(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
                  <content fontSize="60"><p>测试文字 ABC</p></content>
                </shape>
                <line id="ghost-line" startX="80" startY="245" endX="560" endY="245">
                  <border color="rgba(255, 0, 0, 0.03)" width="4"/>
                </line>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_vertical_line_grazing_text_left_edge(self) -> None:
        # Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 4: a vertical line sitting on the text
        # frame's left edge renders before the first glyph, so it must not be flagged.
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmm">
                  <content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
                    <p>Vertical edge graze</p>
                  </content>
                </shape>
                <line id="bmX" startX="120.00000000000001" startY="90" endX="120.00000000000001" endY="150.00833275470998">
                  <border color="rgba(0, 0, 0, 1)"/>
                </line>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_polyline_crossing_text(self) -> None:
        # Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 6: the crossing check is scoped to
        # <line> only, so a <polyline> over text is not flagged.
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmr">
                  <content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
                    <p>Polyline target</p>
                  </content>
                </shape>
                <polyline id="bmH" width="270" height="55" topLeftX="110" topLeftY="95">
                  <border color="rgba(0, 0, 0, 1)"/>
                </polyline>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_ignores_line_below_visual_glyph_height(self) -> None:
        # Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 7: the shape frame is 80px tall but the
        # single 20px line of glyphs occupies only its top; a line at the frame's lower region grazes
        # under the visual glyph box (underline look) and must not be flagged.
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape width="240" height="80" topLeftX="120" topLeftY="100" type="text" id="bmB">
                  <content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
                    <p>Visual height target</p>
                  </content>
                </shape>
                <line id="bmQ" startX="110" startY="150" endX="380.00185184550116" endY="150">
                  <border color="rgba(0, 0, 0, 1)"/>
                </line>
              </data>
            </slide>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)

    def test_lint_xml_uses_rotated_text_and_chart_bounds_for_canvas_validation(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape id="rotated-text" type="text" topLeftX="0" topLeftY="0" width="100" height="100" rotation="45"/>
                  <chart id="rotated-chart" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45">
                    <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                    <chartData>
                      <dim1><chartField name="category" valueType="string">A</chartField></dim1>
                      <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                    </chartData>
                  </chart>
                </data>
              </slide>
            </presentation>
            """
        )
        issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
        self.assertEqual(result["summary"]["error_count"], 2)
        self.assertEqual(issues_by_element["rotated-text"]["code"], "shape_out_of_canvas")
        self.assertAlmostEqual(issues_by_element["rotated-text"]["overflow"]["left"], 20.710678, places=5)
        self.assertAlmostEqual(issues_by_element["rotated-text"]["overflow"]["top"], 20.710678, places=5)
        self.assertEqual(issues_by_element["rotated-chart"]["code"], "chart_out_of_canvas")
        self.assertAlmostEqual(issues_by_element["rotated-chart"]["overflow"]["right"], 20.710678, places=5)

    def test_lint_xml_uses_declared_bounds_for_rect_and_ignores_images(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape id="rotated-rect" type="rect" topLeftX="900" topLeftY="0" width="100" height="100" rotation="45"/>
                  <img id="rotated-image" src="token" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45"/>
                </data>
              </slide>
            </presentation>
            """
        )
        issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issues_by_element["rotated-rect"]["code"], "shape_out_of_canvas")
        self.assertEqual(issues_by_element["rotated-rect"]["overflow"], {"left": 0, "top": 0, "right": 40, "bottom": 0})
        self.assertNotIn("rotated-image", issues_by_element)

    def test_detect_elements_out_of_canvas_limits_detection_to_whitelist(self) -> None:
        issues = xml_text_overlap_lint.detect_elements_out_of_canvas(
            [
                {"id": "table", "kind": "table", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 45},
                {"id": "chart", "kind": "chart", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
                {
                    "id": "text",
                    "kind": "shape",
                    "type": "text",
                    "x": 95,
                    "y": 0,
                    "width": 10,
                    "height": 10,
                    "rotation": 0,
                },
                {
                    "id": "rect",
                    "kind": "shape",
                    "type": "rect",
                    "x": 95,
                    "y": 0,
                    "width": 10,
                    "height": 10,
                    "rotation": 45,
                },
                {"id": "image", "kind": "img", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
                {
                    "id": "ellipse",
                    "kind": "shape",
                    "type": "ellipse",
                    "x": 95,
                    "y": 0,
                    "width": 10,
                    "height": 10,
                    "rotation": 0,
                },
            ],
            100,
            100,
        )

        self.assertEqual([issue["elements"] for issue in issues], [["table"], ["chart"], ["text"], ["rect"]])
        self.assertEqual(issues[-1]["bbox"], {"x": 95, "y": 0, "width": 10, "height": 10})

    def test_lint_xml_rejects_non_finite_rotation_values_from_xsd(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape id="infinite" type="text" topLeftX="-10" topLeftY="0" width="20" height="20" rotation="inf"/>
                  <shape id="negative-infinite" type="text" topLeftX="0" topLeftY="-10" width="20" height="20" rotation="-inf"/>
                  <chart id="not-a-number" topLeftX="950" topLeftY="0" width="20" height="20" rotation="nan">
                    <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                    <chartData>
                      <dim1><chartField name="category" valueType="string">A</chartField></dim1>
                      <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                    </chartData>
                  </chart>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 3)
        slide_issues = result["slides"][0]["issues"]
        self.assertTrue(all(issue["code"] == "sxsd_invalid_scalar" for issue in slide_issues))
        self.assertEqual({issue["actual"] for issue in slide_issues}, {"inf", "-inf", "nan"})

    def test_lint_xml_reports_table_bottom_overflow_from_declared_bounds(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="score-table" topLeftX="54" topLeftY="238" width="414" height="385">
                    <tr><td><content><p>Score</p></content></td></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issue["code"], "table_out_of_canvas")
        self.assertEqual(issue["elements"], ["score-table"])
        self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 0, "bottom": 83})
        self.assertEqual(issue["bbox"], {"x": 54, "y": 238, "width": 414, "height": 385})

    def test_lint_xml_reports_table_right_overflow_from_declared_bounds(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="wide-table" topLeftX="850" topLeftY="80" width="180" height="120">
                    <tr><td><content><p>Score</p></content></td></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issue["code"], "table_out_of_canvas")
        self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 70, "bottom": 0})

    def test_lint_xml_allows_table_with_declared_bounds_inside_canvas(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="inside-table" topLeftX="40" topLeftY="120" width="880" height="360">
                    <tr><td><content><p>Score</p></content></td></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(result["summary"]["warning_count"], 0)

    def test_lint_xml_reports_resolved_table_bounds_when_declared_sizes_are_missing(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="implicit-size-table" topLeftX="850" topLeftY="480">
                    <colgroup><col/><col/></colgroup>
                    <tr><td/><td/></tr>
                    <tr><td/><td/></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(issue["code"], "table_out_of_canvas")
        self.assertEqual(issue["bbox"], {"x": 850, "y": 480, "width": 220, "height": 74})
        self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 110, "bottom": 14})

    def test_lint_xml_xml_path_preserves_source_index_after_filtered_table(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="http://www.larkoffice.com/sml/2.0">
              <data>
                <table id="t1" topLeftX="20" topLeftY="20">
                  <tr><td/></tr>
                </table>
                <table id="t2" topLeftX="20" topLeftY="100" width="9999" height="100">
                  <tr><td/></tr>
                </table>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "table_out_of_canvas"
        )
        self.assertEqual(issue["element_ids"], ["t2"])
        self.assertEqual(
            issue["related_objects"][0]["xml_path"],
            "slide[1]/data/table[2]",
        )

    def test_lint_xml_duplicate_id_keeps_issue_bound_to_original_shape(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="dup" type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
                <shape id="dup" type="rect" topLeftX="100" topLeftY="40" width="50" height="50"/>
              </data>
            </slide>
            """
        )

        canvas_issue = next(
            issue for issue in result["slides"][0]["issues"]
            if issue["code"] == "shape_out_of_canvas"
        )
        self.assertEqual(canvas_issue["element_ids"], ["dup"])
        self.assertEqual(
            canvas_issue["related_objects"],
            [
                {
                    "element_id": "dup",
                    "kind": "shape",
                    "type": "rect",
                    "bbox": {"x": -20, "y": 40, "width": 50, "height": 50},
                    "xml_path": "slide[1]/data/shape[1]",
                }
            ],
        )
        self.assertTrue(
            canvas_issue["hint"].startswith(
                "Locate via related_objects[].xml_path. "
            )
        )
        duplicate_issue = next(
            issue for issue in result["slides"][0]["issues"]
            if issue["code"] == "duplicate_element_id"
        )
        self.assertEqual(
            duplicate_issue["hint"],
            "Locate via related_objects[].xml_path. "
            "Do not invent replacement IDs. For newly authored elements, remove the id attribute. "
            "When updating read-back XML, keep the server ID on the original element only and remove it "
            "from copied or new elements.",
        )
        self.assertEqual(duplicate_issue["element_ids"], ["dup", "dup"])
        self.assertEqual(
            [obj["xml_path"] for obj in duplicate_issue["related_objects"]],
            ["slide[1]/data/shape[1]", "slide[1]/data/shape[2]"],
        )

    def test_lint_xml_blocks_duplicate_table_cell_ids(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <table id="table-1" topLeftX="80" topLeftY="80" width="800" height="120">
                  <colgroup><col width="400"/><col width="400"/></colgroup>
                  <tr height="120">
                    <td id="bjs"><content fontSize="24"><p>Original cell</p></content></td>
                    <td id="bjs"><content fontSize="24"><p>Copied cell</p></content></td>
                  </tr>
                </table>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "duplicate_element_id"
        )
        self.assertFalse(result["summary"]["release_ready"])
        self.assertEqual(issue["element_ids"], ["bjs", "bjs"])
        self.assertEqual(
            issue["related_objects"],
            [
                {
                    "element_id": "bjs",
                    "kind": "td",
                    "type": "td",
                    "xml_path": "slide[1]/data/table[1]/tr[1]/td[1]",
                },
                {
                    "element_id": "bjs",
                    "kind": "td",
                    "type": "td",
                    "xml_path": "slide[1]/data/table[1]/tr[1]/td[2]",
                },
            ],
        )

    def test_lint_xml_blocks_duplicate_id_shared_by_shape_and_table_cell(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="baa" type="rect" topLeftX="40" topLeftY="40" width="80" height="80"/>
                <table id="table-1" topLeftX="160" topLeftY="40" width="200" height="80">
                  <tr height="80"><td id="baa"><content fontSize="12"><p>Cell</p></content></td></tr>
                </table>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "duplicate_element_id"
        )
        self.assertEqual(issue["element_ids"], ["baa", "baa"])
        self.assertEqual(
            [obj["xml_path"] for obj in issue["related_objects"]],
            ["slide[1]/data/shape[1]", "slide[1]/data/table[1]/tr[1]/td[1]"],
        )

    def test_lint_xml_blocks_duplicate_id_shared_by_shape_and_undefined(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="dup" type="rect" topLeftX="40" topLeftY="40" width="80" height="80"/>
                <undefined id="dup" type="video"/>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "duplicate_element_id"
        )
        self.assertFalse(result["summary"]["release_ready"])
        self.assertEqual(issue["element_ids"], ["dup", "dup"])
        self.assertEqual(
            issue["related_objects"],
            [
                {
                    "element_id": "dup",
                    "kind": "shape",
                    "type": "rect",
                    "bbox": {"x": 40, "y": 40, "width": 80, "height": 80},
                    "xml_path": "slide[1]/data/shape[1]",
                },
                {
                    "element_id": "dup",
                    "kind": "undefined",
                    "type": "video",
                    "xml_path": "slide[1]/data/undefined[1]",
                },
            ],
        )

    def test_lint_xml_does_not_report_unique_table_cell_and_note_ids(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <table id="table-1" topLeftX="80" topLeftY="80" width="800" height="120">
                  <colgroup><col width="400"/><col width="400"/></colgroup>
                  <tr height="120"><td id="baa"/><td id="bab"/></tr>
                </table>
              </data>
              <note id="bac"><content fontSize="12"><p>Note</p></content></note>
            </slide>
            """
        )

        self.assertNotIn(
            "duplicate_element_id",
            [issue["code"] for issue in result["slides"][0]["issues"]],
        )

    def test_lint_xml_blocks_duplicate_ids_across_slides(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide>
                <data>
                  <shape id="dup" type="rect" topLeftX="40" topLeftY="40" width="80" height="80"/>
                </data>
              </slide>
              <slide>
                <data>
                  <shape id="dup" type="rect" topLeftX="140" topLeftY="40" width="80" height="80"/>
                </data>
              </slide>
            </presentation>
            """
        )

        issue = next(
            issue
            for issue in result["document"]["errors"]
            if issue["code"] == "duplicate_element_id"
        )
        self.assertFalse(result["summary"]["release_ready"])
        self.assertEqual(issue["element_ids"], ["dup", "dup"])
        self.assertEqual(
            issue["related_objects"],
            [
                {
                    "element_id": "dup",
                    "kind": "shape",
                    "type": "rect",
                    "bbox": {"x": 40, "y": 40, "width": 80, "height": 80},
                    "xml_path": "slide[1]/data/shape[1]",
                },
                {
                    "element_id": "dup",
                    "kind": "shape",
                    "type": "rect",
                    "bbox": {"x": 140, "y": 40, "width": 80, "height": 80},
                    "xml_path": "slide[2]/data/shape[1]",
                },
            ],
        )

    def test_lint_xml_does_not_treat_slide_ids_as_element_ids(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide id="dup"><data/></slide>
              <slide id="dup"><data/></slide>
            </presentation>
            """
        )

        self.assertNotIn(
            "duplicate_element_id",
            [issue["code"] for issue in result["document"]["errors"]],
        )

    def test_lint_xml_does_not_treat_presentation_id_as_element_id(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" id="dup" width="960" height="540">
              <slide>
                <data><undefined id="dup" type="video"/></data>
              </slide>
            </presentation>
            """
        )

        self.assertNotIn(
            "duplicate_element_id",
            [
                issue["code"]
                for issue in [
                    *result["document"]["errors"],
                    *result["slides"][0]["errors"],
                ]
            ],
        )

    def test_lint_xml_blocks_duplicate_note_ids_across_slides(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide>
                <note id="baa"><content fontSize="12"><p>First note</p></content></note>
              </slide>
              <slide>
                <note id="baa"><content fontSize="12"><p>Copied note</p></content></note>
              </slide>
            </presentation>
            """
        )

        issue = next(
            issue
            for issue in result["document"]["errors"]
            if issue["code"] == "duplicate_element_id"
        )
        self.assertFalse(result["summary"]["release_ready"])
        self.assertEqual(issue["element_ids"], ["baa", "baa"])
        self.assertEqual(
            issue["related_objects"],
            [
                {
                    "element_id": "baa",
                    "kind": "note",
                    "type": "note",
                    "xml_path": "slide[1]/note[1]",
                },
                {
                    "element_id": "baa",
                    "kind": "note",
                    "type": "note",
                    "xml_path": "slide[2]/note[1]",
                },
            ],
        )

    def test_lint_xml_cross_kind_duplicate_id_does_not_change_related_object_kind(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="dup" type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
                <img id="dup" src="token" topLeftX="100" topLeftY="40" width="50" height="50"/>
              </data>
            </slide>
            """
        )

        issue = next(
            issue for issue in result["slides"][0]["issues"]
            if issue["code"] == "shape_out_of_canvas"
        )
        self.assertEqual(issue["related_objects"][0]["kind"], "shape")
        self.assertEqual(
            issue["related_objects"][0]["xml_path"],
            "slide[1]/data/shape[1]",
        )
        duplicate_issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "duplicate_element_id"
        )
        self.assertEqual(
            [obj["xml_path"] for obj in duplicate_issue["related_objects"]],
            ["slide[1]/data/shape[1]", "slide[1]/data/img[1]"],
        )

    def test_normalize_issue_does_not_repeat_xml_path_hint_prefix(self) -> None:
        xml_path = "slide[1]/data/shape[1]"
        element = {
            "id": "box",
            "_source_id": "box",
            "_ref": xml_path,
            "xml_path": xml_path,
            "kind": "shape",
            "type": "rect",
            "x": 0,
            "y": 0,
            "width": 40,
            "height": 40,
        }
        prefix = "Locate via related_objects[].xml_path."

        issue = xml_text_overlap_lint.normalize_issue(
            {
                "level": "error",
                "code": "shape_out_of_canvas",
                "elements": [xml_path],
                "canvas": {"width": 960, "height": 540},
                "bbox": {"x": -10, "y": 0, "width": 40, "height": 40},
                "overflow": {"left": 10, "top": 0, "right": 0, "bottom": 0},
                "hint": f"{prefix} Move the shape inside the canvas.",
            },
            1,
            {xml_path: element},
        )

        self.assertEqual(issue["hint"].count(prefix), 1)

    def test_lint_xml_elements_keep_locator_for_anonymous_related_object(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="text" topLeftX="100" topLeftY="100" width="300" height="100">
                  <content fontSize="24"><p>Important text</p></content>
                </shape>
                <img id="srv-42" src="token" topLeftX="100" topLeftY="100" width="300" height="100"/>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "image_covers_text"
        )
        self.assertEqual(
            issue["elements"],
            ["srv-42", "slide[1]/data/shape[1]"],
        )
        self.assertEqual(issue["element_ids"], ["srv-42"])
        self.assertEqual(len(issue["related_objects"]), 2)

    def test_normalize_issue_deduplicates_repeated_element_refs(self) -> None:
        xml_path = "slide[1]/data/shape[1]"
        element = {
            "id": "srv-42",
            "_source_id": "srv-42",
            "_ref": xml_path,
            "xml_path": xml_path,
            "kind": "shape",
            "type": "rect",
            "x": 0,
            "y": 0,
            "width": 40,
            "height": 40,
        }

        issue = xml_text_overlap_lint.normalize_issue(
            {
                "level": "warning",
                "code": "blank_slide",
                "measurement": {
                    "visible_element_count": 0,
                    "declared_element_count": 1,
                },
                "elements": [xml_path, xml_path],
            },
            1,
            {xml_path: element},
        )

        self.assertEqual(issue["elements"], ["srv-42"])
        self.assertEqual(issue["element_ids"], ["srv-42"])
        self.assertEqual(len(issue["related_objects"]), 1)

    def test_lint_xml_reports_duplicate_ids_for_every_linted_element_kind(self) -> None:
        duplicate_pairs = {
            "shape": (
                '<shape id="dup" type="rect" topLeftX="10" topLeftY="10" width="40" height="40"/>',
                '<shape id="dup" type="rect" topLeftX="60" topLeftY="10" width="40" height="40"/>',
            ),
            "chart": (
                '<chart id="dup" topLeftX="10" topLeftY="10" width="40" height="40"><chartPlotArea><chartPlot type="line"/></chartPlotArea><chartData><dim1><chartField name="category" valueType="string">A</chartField></dim1><dim2><chartField name="value" valueType="number">1</chartField></dim2></chartData></chart>',
                '<chart id="dup" topLeftX="60" topLeftY="10" width="40" height="40"><chartPlotArea><chartPlot type="line"/></chartPlotArea><chartData><dim1><chartField name="category" valueType="string">A</chartField></dim1><dim2><chartField name="value" valueType="number">1</chartField></dim2></chartData></chart>',
            ),
            "table": (
                '<table id="dup" topLeftX="10" topLeftY="10" width="40" height="40"><colgroup><col width="40"/></colgroup><tr height="40"><td/></tr></table>',
                '<table id="dup" topLeftX="60" topLeftY="10" width="40" height="40"><colgroup><col width="40"/></colgroup><tr height="40"><td/></tr></table>',
            ),
            "img": (
                '<img id="dup" src="token" topLeftX="10" topLeftY="10" width="40" height="40"/>',
                '<img id="dup" src="token" topLeftX="60" topLeftY="10" width="40" height="40"/>',
            ),
            "line": (
                '<line id="dup" startX="10" startY="10" endX="40" endY="40"><border color="rgb(0, 0, 0)"/></line>',
                '<line id="dup" startX="60" startY="10" endX="90" endY="40"><border color="rgb(0, 0, 0)"/></line>',
            ),
            "icon": (
                '<icon id="dup" iconType="iconpark/Base/setting.svg" topLeftX="10" topLeftY="10" width="40" height="40"><fill><fillColor color="rgb(0, 0, 0)"/></fill></icon>',
                '<icon id="dup" iconType="iconpark/Base/setting.svg" topLeftX="60" topLeftY="10" width="40" height="40"><fill><fillColor color="rgb(0, 0, 0)"/></fill></icon>',
            ),
            "polyline": (
                '<polyline id="dup" topLeftX="10" topLeftY="10" width="40" height="40"><border color="rgb(0, 0, 0)"/></polyline>',
                '<polyline id="dup" topLeftX="60" topLeftY="10" width="40" height="40"><border color="rgb(0, 0, 0)"/></polyline>',
            ),
        }
        for kind, pair in duplicate_pairs.items():
            with self.subTest(kind=kind):
                result = xml_text_overlap_lint.lint_xml(
                    f'<slide xmlns="https://www.larkoffice.com/sml/2.0"><data>{pair[0]}{pair[1]}</data></slide>'
                )
                issue = next(
                    issue
                    for issue in result["slides"][0]["issues"]
                    if issue["code"] == "duplicate_element_id"
                )
                self.assertEqual(issue["element_ids"], ["dup", "dup"])
                self.assertEqual(
                    [obj["xml_path"] for obj in issue["related_objects"]],
                    [
                        f"slide[1]/data/{kind}[1]",
                        f"slide[1]/data/{kind}[2]",
                    ],
                )

    def test_lint_xml_missing_id_does_not_collide_with_explicit_synthetic_like_id(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
                <shape id="shape-1" type="rect" topLeftX="100" topLeftY="40" width="50" height="50"/>
              </data>
            </slide>
            """
        )

        issue = next(
            issue for issue in result["slides"][0]["issues"]
            if issue["code"] == "shape_out_of_canvas"
        )
        self.assertEqual(issue["element_ids"], [])
        self.assertNotIn("element_id", issue["related_objects"][0])
        self.assertEqual(
            issue["related_objects"][0]["xml_path"],
            "slide[1]/data/shape[1]",
        )
        self.assertTrue(
            issue["hint"].startswith("Locate via related_objects[].xml_path. ")
        )
        self.assertNotIn(
            "duplicate_element_id",
            [candidate["code"] for candidate in result["slides"][0]["issues"]],
        )

    def test_lint_xml_empty_id_is_not_exposed_as_an_element_id(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="" type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "shape_out_of_canvas"
        )
        self.assertEqual(issue["element_ids"], [])
        self.assertNotIn("element_id", issue["related_objects"][0])
        self.assertEqual(
            issue["related_objects"][0]["xml_path"],
            "slide[1]/data/shape[1]",
        )

    def test_lint_xml_uses_resolved_table_bounds_for_canvas_validation(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="resolved-overflow-table" topLeftX="800" topLeftY="80" width="100" height="40">
                    <colgroup><col width="100"/><col width="100"/></colgroup>
                    <tr height="40"><td/><td/></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        issues = result["slides"][0]["issues"]
        canvas_issue = next(issue for issue in issues if issue["code"] == "table_out_of_canvas")
        mismatch_issue = next(issue for issue in issues if issue["code"] == "table_resolved_size_mismatch")
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(canvas_issue["bbox"], {"x": 800, "y": 80, "width": 200, "height": 40})
        self.assertEqual(canvas_issue["overflow"]["right"], 40)
        self.assertEqual(mismatch_issue["dimension"], "width")
        self.assertEqual(mismatch_issue["resolved_size"], canvas_issue["bbox"]["width"])

    def test_lint_xml_uses_the_same_anonymous_table_path_for_all_table_diagnostics(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <shape id="title" type="text" topLeftX="40" topLeftY="40" width="200" height="40"/>
                  <img id="logo" src="token" topLeftX="40" topLeftY="100" width="40" height="40"/>
                  <table topLeftX="900" topLeftY="80" width="100" height="40">
                    <colgroup><col width="100"/><col width="100"/></colgroup>
                    <tr height="40"><td/><td/></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        issues = result["slides"][0]["issues"]
        canvas_issue = next(issue for issue in issues if issue["code"] == "table_out_of_canvas")
        mismatch_issue = next(issue for issue in issues if issue["code"] == "table_resolved_size_mismatch")
        self.assertEqual(canvas_issue["elements"], ["slide[1]/data/table[1]"])
        self.assertEqual(mismatch_issue["elements"], ["slide[1]/data/table[1]"])
        self.assertEqual(
            canvas_issue["related_objects"][0]["xml_path"],
            "slide[1]/data/table[1]",
        )
        self.assertEqual(
            mismatch_issue["related_objects"][0]["xml_path"],
            "slide[1]/data/table[1]",
        )
        self.assertNotIn("element_id", canvas_issue["related_objects"][0])
        self.assertNotIn("element_id", mismatch_issue["related_objects"][0])

    def test_lint_xml_reports_info_when_table_target_size_resolves_larger_than_declared(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="size-mismatch" topLeftX="40" topLeftY="120" width="200" height="80">
                    <colgroup><col span="2" width="100"/><col width="50"/></colgroup>
                    <tr height="40"><td/><td/><td/></tr>
                    <tr height="60"><td/><td/><td/></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        issues_by_dimension = {issue["dimension"]: issue for issue in result["slides"][0]["issues"]}
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(result["summary"]["warning_count"], 0)
        self.assertEqual(result["summary"]["info_count"], 2)
        self.assertEqual(issues_by_dimension["width"]["level"], "info")
        self.assertEqual(issues_by_dimension["width"]["code"], "table_resolved_size_mismatch")
        self.assertEqual(issues_by_dimension["width"]["resolved_sizes"], [100, 100, 50])
        self.assertEqual(issues_by_dimension["width"]["resolved_size"], 250)
        self.assertEqual(issues_by_dimension["height"]["resolved_sizes"], [40, 60])
        self.assertEqual(issues_by_dimension["height"]["resolved_size"], 100)

    def test_lint_xml_does_not_report_info_when_table_target_size_is_resolved_exactly(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="size-match" topLeftX="40" topLeftY="120" width="300" height="100">
                    <colgroup><col width="100"/><col/></colgroup>
                    <tr height="40"><td/><td/></tr>
                    <tr><td/><td/></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(result["summary"]["warning_count"], 0)
        self.assertEqual(result["slides"][0]["issues"], [])

    def test_lint_xml_keeps_resolved_table_sizes_positive_when_target_is_too_small(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide xmlns="https://www.larkoffice.com/sml/2.0">
                <data>
                  <table id="narrow-table" topLeftX="40" topLeftY="120" width="1">
                    <colgroup><col/><col/></colgroup>
                    <tr><td/><td/></tr>
                  </table>
                </data>
              </slide>
            </presentation>
            """
        )
        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["dimension"], "width")
        self.assertEqual(issue["resolved_sizes"], [1, 1])
        self.assertEqual(issue["resolved_size"], 2)

    def test_fill_last_size_gap_preserves_target_when_positive_sizes_are_possible(self) -> None:
        final_sizes = xml_text_overlap_lint.fill_last_size_gap([10, 10], 3)
        self.assertEqual(final_sizes, [2, 1])
        self.assertEqual(sum(final_sizes), 3)

    def test_cli_reports_table_layout_size_info_for_weighted_min_layout_cases(self) -> None:
        cases = {
            "target-exact": (
                """
                <table topLeftX="40" topLeftY="120" width="360" height="150">
                  <colgroup><col width="100"/><col width="200"/></colgroup>
                  <tr height="40"><td/><td/></tr><tr height="60"><td/><td/></tr>
                </table>
                """,
                0,
            ),
            "declared-size-exceeds-target": (
                """
                <table topLeftX="40" topLeftY="120" width="200" height="80">
                  <colgroup><col span="2" width="100"/><col width="50"/></colgroup>
                  <tr height="40"><td/><td/><td/></tr><tr height="60"><td/><td/><td/></tr>
                </table>
                """,
                2,
            ),
            "remaining-space-insufficient": (
                """
                <table topLeftX="40" topLeftY="120" width="80" height="30">
                  <colgroup><col width="80"/><col/></colgroup>
                  <tr height="40"><td/><td/></tr><tr><td/><td/></tr>
                </table>
                """,
                2,
            ),
            "no-target-size": (
                """
                <table topLeftX="40" topLeftY="120">
                  <colgroup><col width="80"/><col/></colgroup>
                  <tr height="40"><td/><td/></tr><tr><td/><td/></tr>
                </table>
                """,
                0,
            ),
        }
        script_path = Path(xml_text_overlap_lint.__file__).resolve()
        with tempfile.TemporaryDirectory() as temp_dir:
            for name, (table_xml, expected_info_count) in cases.items():
                with self.subTest(case=name):
                    input_path = Path(temp_dir) / f"{name}.xml"
                    input_path.write_text(
                        f"""
                        <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
                          <slide xmlns="https://www.larkoffice.com/sml/2.0"><data>{table_xml}</data></slide>
                        </presentation>
                        """,
                        encoding="utf-8",
                    )
                    completed = subprocess.run(
                        [sys.executable, str(script_path), "--input", str(input_path)],
                        capture_output=True,
                        check=False,
                        text=True,
                    )
                    result = json.loads(completed.stdout)
                    self.assertEqual(completed.returncode, 0, completed.stderr)
                    self.assertEqual(result["summary"]["error_count"], 0)
                    self.assertEqual(result["summary"]["warning_count"], 0)
                    self.assertEqual(result["summary"]["info_count"], expected_info_count)
                    self.assertTrue(
                        all(issue["level"] == "info" for issue in result["slides"][0]["issues"]),
                        result["slides"][0]["issues"],
                    )

    def test_lint_xml_detects_invalid_template_text_stack_overlap(self) -> None:
        cases = [
            (
                "subtitle-too-high",
                """
                <shape type="text" topLeftX="40" topLeftY="80" width="240" height="90">
                  <content textType="title" fontSize="44"><p>Title</p></content>
                </shape>
                <shape type="text" topLeftX="40" topLeftY="90" width="240" height="80">
                  <content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
                </shape>
                """,
            ),
        ]
        for name, shapes in cases:
            with self.subTest(name=name):
                result = xml_text_overlap_lint.lint_xml(
                    f"""
                    <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
                      <slide xmlns="https://www.larkoffice.com/sml/2.0">
                        <data>{shapes}</data>
                      </slide>
                    </presentation>
                    """
                )
                self.assertEqual(result["summary"]["error_count"], 1)
                self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")


    def test_lint_xml_reports_vertical_text_image_overlap_as_warning(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0"><data>
              <shape id="text" type="text" vert="vert" topLeftX="100" topLeftY="100" width="100" height="100">
                <content><p>Vertical</p></content>
              </shape>
              <img id="image" src="token" topLeftX="120" topLeftY="120" width="20" height="20"/>
            </data></slide>
            """
        )
        issue = next(issue for issue in result["slides"][0]["issues"] if issue["code"] == "image_may_cover_vertical_text")
        self.assertEqual(issue["level"], "info")
        self.assertEqual(result["summary"]["error_count"], 0)
        self.assertEqual(result["summary"]["info_count"], 1)

    def test_lint_xml_related_objects_include_source_xml_paths(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide>
                <data>
                  <shape type="text" topLeftX="80" topLeftY="80" width="400" height="60">
                    <content fontSize="24"><p>Control slide</p></content>
                  </shape>
                </data>
              </slide>
              <slide>
                <data>
                  <shape type="text" topLeftX="70" topLeftY="55" width="820" height="70">
                    <content fontSize="32"><p>shape-3 mapping experiment</p></content>
                  </shape>
                  <shape type="text" topLeftX="80" topLeftY="165" width="400" height="70">
                    <content fontSize="18"><p>First, preserve source order.</p></content>
                  </shape>
                  <shape type="text" topLeftX="80" topLeftY="315" width="400" height="64">
                    <content fontSize="26"><p>TARGET_SHAPE_THREE</p></content>
                  </shape>
                  <img src="token" topLeftX="80" topLeftY="305" width="400" height="110"/>
                </data>
              </slide>
            </presentation>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][1]["issues"]
            if issue["code"] == "image_covers_text"
        )
        self.assertEqual(
            [(obj["kind"], obj["xml_path"]) for obj in issue["related_objects"]],
            [
                ("img", "slide[2]/data/img[1]"),
                ("shape", "slide[2]/data/shape[3]"),
            ],
        )
        self.assertTrue(
            all("element_id" not in obj for obj in issue["related_objects"])
        )

    def test_lint_xml_related_objects_include_line_xml_path(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="http://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="label" type="text" topLeftX="100" topLeftY="100" width="200" height="80">
                  <content fontSize="24"><p>Crossed text</p></content>
                </shape>
                <line id="connector" startX="80" startY="130" endX="330" endY="130">
                  <border color="rgb(15, 23, 42)" width="2"/>
                </line>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "bbox_overlap" and issue["elements"][0] == "connector"
        )
        self.assertEqual(
            {
                obj["element_id"]: obj["xml_path"]
                for obj in issue["related_objects"]
            },
            {
                "connector": "slide[1]/data/line[1]",
                "label": "slide[1]/data/shape[1]",
            },
        )


class XmlTextOverlapLintDensityTest(unittest.TestCase):
    def test_lint_xml_sparse_container_related_objects_include_icon_xml_path(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="http://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="60" topLeftY="120" width="400" height="300"/>
                <icon id="visual" iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="140" width="32" height="32">
                  <fill><fillColor color="rgb(37, 99, 235)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "sparse_container_content"
        )
        self.assertEqual(
            {
                obj["element_id"]: obj["xml_path"]
                for obj in issue["related_objects"]
            },
            {
                "card": "slide[1]/data/shape[1]",
                "visual": "slide[1]/data/icon[1]",
            },
        )

    def test_lint_xml_blocks_blank_slide(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
              <slide id="content-slide">
                <data>
                  <shape id="title" type="text" topLeftX="60" topLeftY="60" width="400" height="50">
                    <content fontSize="28"><p>Investment report</p></content>
                  </shape>
                </data>
              </slide>
              <slide id="blank-slide">
                <style><fill><fillColor color="rgba(255, 255, 255, 1)"/></fill></style>
                <data/>
                <note><content/></note>
              </slide>
            </presentation>
            """
        )

        self.assertEqual(result["summary"]["slide_count"], 2)
        self.assertEqual(result["summary"]["warning_count"], 0)
        self.assertEqual(result["summary"]["error_count"], 1)
        self.assertEqual(result["summary"]["status"], "blocked")
        self.assertFalse(result["summary"]["release_ready"])
        self.assertEqual(result["slides"][0]["issues"], [])
        self.assertEqual(result["slides"][1]["element_count"], 0)
        issue = result["slides"][1]["errors"][0]
        self.assertEqual(issue["level"], "error")
        self.assertEqual(issue["code"], "blank_slide")
        self.assertEqual(issue["element_ids"], [])
        self.assertEqual(issue["rule"]["id"], "blank_slide")
        self.assertEqual(issue["measurement"]["visible_element_count"], 0)
        self.assertEqual(issue["related_objects"], [])

    def test_lint_xml_blocks_blank_slide_with_only_transparent_image(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <img id="ghost" src="token" topLeftX="60" topLeftY="60" width="200" height="200" alpha="0"/>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 1)
        issue = result["slides"][0]["errors"][0]
        self.assertEqual(issue["code"], "blank_slide")

    def test_lint_xml_warns_when_large_container_is_mostly_empty(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="trend-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="370"/>
                <shape id="trend-title" type="text" topLeftX="515" topLeftY="147" width="380" height="28">
                  <content fontSize="15"><p>Core trends</p></content>
                </shape>
                <shape id="trend-copy" type="text" topLeftX="515" topLeftY="177" width="380" height="315">
                  <content fontSize="12"><p>First point</p><p>Second point</p><p>Third point</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["code"], "sparse_container_content")
        self.assertEqual(issue["target"]["container_id"], "trend-card")
        self.assertEqual(issue["target"], {
            "slide_number": 1,
            "container_id": "trend-card",
            "container_xml_path": "slide[1]/data/shape[1]",
            "container_type": "rect",
            "bbox": {"x": 500, "y": 135, "width": 410, "height": 370},
        })
        self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.15)
        self.assertEqual(issue["rule"], {
            "name": "large_container_visible_content_coverage",
            "threshold": 0.15,
            "comparison": "content_coverage_ratio < threshold",
            "id": "sparse_container_content",
        })
        self.assertEqual(issue["measurement"]["container_area"], 151700)
        self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.03)
        self.assertEqual(issue["elements"], ["trend-card", "trend-title", "trend-copy"])
        self.assertEqual(issue["element_ids"], ["trend-card", "trend-title", "trend-copy"])
        self.assertEqual(
            [obj["element_id"] for obj in issue["related_objects"]],
            ["trend-card", "trend-title", "trend-copy"],
        )
        self.assertEqual(result["slides"][0]["status"], "needs_screenshot_review")
        self.assertEqual(result["slides"][0]["warnings"], result["slides"][0]["issues"])

    def test_lint_xml_uses_xml_path_in_anonymous_sparse_container_message(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape type="rect" topLeftX="500" topLeftY="135" width="410" height="370"/>
                <shape id="trend-title" type="text" topLeftX="515" topLeftY="147" width="380" height="28">
                  <content fontSize="15"><p>Core trends</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        issue = next(
            issue
            for issue in result["slides"][0]["issues"]
            if issue["code"] == "sparse_container_content"
        )
        self.assertNotIn("container_id", issue["target"])
        self.assertEqual(
            issue["target"]["container_xml_path"],
            "slide[1]/data/shape[1]",
        )
        self.assertEqual(
            issue["message"],
            "large card slide[1]/data/shape[1] content coverage 1.0% is below 15.0%",
        )

    def test_lint_xml_warns_for_sparse_short_cards(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card-1" type="rect" topLeftX="60" topLeftY="180" width="400" height="105"/>
                <shape id="text-1" type="text" topLeftX="80" topLeftY="220" width="360" height="30">
                  <content fontSize="14"><p>期待认识大家</p></content>
                </shape>
                <shape id="card-2" type="rect" topLeftX="490" topLeftY="180" width="400" height="105"/>
                <shape id="text-2" type="text" topLeftX="510" topLeftY="220" width="360" height="30">
                  <content fontSize="14"><p>化学一起讨论</p></content>
                </shape>
                <shape id="card-3" type="rect" topLeftX="60" topLeftY="310" width="400" height="105"/>
                <shape id="text-3" type="text" topLeftX="80" topLeftY="350" width="360" height="30">
                  <content fontSize="14"><p>吉他随时交流</p></content>
                </shape>
                <shape id="card-4" type="rect" topLeftX="490" topLeftY="310" width="400" height="105"/>
                <shape id="text-4" type="text" topLeftX="510" topLeftY="350" width="360" height="30">
                  <content fontSize="14"><p>共度美好四年</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        container_issues = [
            issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
        ]
        self.assertEqual(
            [issue["target"]["container_id"] for issue in container_issues],
            ["card-1", "card-2", "card-3", "card-4"],
        )
        self.assertTrue(all(issue["target"]["bbox"]["height"] == 105 for issue in container_issues))
        self.assertTrue(all(issue["measurement"]["content_coverage_ratio"] < 0.15 for issue in container_issues))
        self.assertEqual(
            [issue["code"] for issue in result["slides"][0]["issues"]],
            [
                "sparse_container_content",
                "sparse_container_content",
                "sparse_container_content",
                "sparse_container_content",
                "sparse_slide_content",
            ],
        )

    def test_lint_xml_warns_when_whole_slide_has_too_little_effective_content(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
                <shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
                  <content fontSize="14"><p>One short line</p></content>
                </shape>
                <shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
                  <content fontSize="14"><p>Another line</p></content>
                </shape>
                <shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
                  <content fontSize="14"><p>Third line</p></content>
                </shape>
                <shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
                  <content fontSize="14"><p>Fourth line</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        issues = [issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_slide_content"]
        self.assertEqual(len(issues), 1)
        issue = issues[0]
        self.assertEqual(issue["target"]["bbox"], {"x": 0, "y": 0, "width": 960, "height": 540})
        self.assertEqual(issue["rule"]["threshold"], 0.035)
        self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.035)
        self.assertEqual(issue["measurement"]["content_element_count"], 4)
        self.assertNotIn("background", issue["elements"])

    def test_lint_xml_ignores_isolated_short_layout_bar(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="summary-bar" type="rect" topLeftX="52" topLeftY="82" width="856" height="105"/>
                <shape id="summary" type="text" topLeftX="72" topLeftY="115" width="816" height="30">
                  <content fontSize="14"><p>One concise summary</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["slides"][0]["issues"], [])

    def test_lint_xml_counts_rect_own_content_as_visible_content(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
                  <content fontSize="18">
                    <p>被吊物</p>
                    <p><span fontSize="36">32.0 t</span></p>
                    <p>钢结构模块</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["slides"][0]["issues"], [])

    def test_lint_xml_reports_nonzero_coverage_for_rect_own_content_reproduction(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
                  <content fontSize="18">
                    <p>被吊物</p>
                    <p>32.0 t</p>
                    <p>钢结构模块</p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )

        issue = result["slides"][0]["issues"][0]
        self.assertGreater(issue["measurement"]["visible_content_area"], 0)
        self.assertEqual(issue["measurement"]["content_element_count"], 1)
        self.assertGreater(issue["measurement"]["content_coverage_ratio"], 0)

    def test_lint_xml_still_warns_for_sparse_rect_own_content(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="sparse-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
                  <content fontSize="12"><p>A</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["target"]["container_id"], "sparse-card")
        self.assertGreater(issue["measurement"]["visible_content_area"], 0)
        self.assertEqual(issue["measurement"]["content_element_count"], 1)
        self.assertEqual(issue["elements"], ["sparse-card"])

    def test_lint_xml_unions_rect_own_content_with_child_content(self) -> None:
        self_only = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
                  <content fontSize="12"><p>A</p></content>
                </shape>
              </data>
            </slide>
            """
        )
        with_overlapping_child = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
                  <content fontSize="12"><p>A</p></content>
                </shape>
                <shape id="child" type="text" topLeftX="60" topLeftY="140" width="220" height="184">
                  <content fontSize="12"><p>A</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self_issue = self_only["slides"][0]["issues"][0]
        mixed_issue = with_overlapping_child["slides"][0]["issues"][0]
        self.assertEqual(
            mixed_issue["measurement"]["visible_content_area"],
            self_issue["measurement"]["visible_content_area"],
        )
        self.assertEqual(mixed_issue["measurement"]["content_element_count"], 2)

    def test_extract_density_elements_reads_nested_font_size_from_rect_content(self) -> None:
        elements = xml_text_overlap_lint.extract_density_elements(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
                  <content fontSize="12"><p><span fontSize="36">32.0 t</span></p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(elements[0]["fontSize"], 36)

    def test_extract_density_elements_does_not_attach_following_text_to_self_closing_rect(self) -> None:
        elements = xml_text_overlap_lint.extract_density_elements(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
                <shape id="title" type="text" topLeftX="80" topLeftY="160" width="180" height="30">
                  <content fontSize="18"><p>Following title</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(elements[0]["text"], "")
        self.assertEqual(elements[1]["text"], "Following title")

    def test_lint_xml_allows_container_with_large_visual_child(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
                <chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220">
                  <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                  <chartData>
                    <dim1><chartField name="category" valueType="string">A</chartField></dim1>
                    <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                  </chartData>
                </chart>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["warning_count"], 0)

    def test_lint_xml_does_not_let_transparent_visual_child_suppress_sparse_warning(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
                  <content fontSize="20"><p>Section title</p></content>
                </shape>
                <shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
                <chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220" alpha="0">
                  <chartPlotArea><chartPlot type="line"/></chartPlotArea>
                  <chartData>
                    <dim1><chartField name="category" valueType="string">A</chartField></dim1>
                    <dim2><chartField name="value" valueType="number">1</chartField></dim2>
                  </chartData>
                </chart>
              </data>
            </slide>
            """
        )

        issue = next(
            issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
        )
        self.assertEqual(issue["target"]["container_id"], "chart-card")

    def test_lint_xml_warns_for_small_empty_visual_placeholder_cards(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="letter-placeholder" type="rect" topLeftX="520" topLeftY="180" width="200" height="200"/>
                <shape id="letter" type="text" topLeftX="540" topLeftY="250" width="160" height="70">
                  <content fontSize="46"><p>Z</p></content>
                </shape>
                <shape id="empty-placeholder" type="rect" topLeftX="744" topLeftY="180" width="144" height="200"/>
              </data>
            </slide>
            """
        )

        issues = result["slides"][0]["issues"]
        self.assertEqual(
            [issue["target"]["container_id"] for issue in issues],
            ["letter-placeholder", "empty-placeholder"],
        )
        self.assertEqual(issues[1]["measurement"]["content_element_count"], 0)

    def test_lint_xml_applies_global_threshold_to_normal_text_card(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="70" topLeftY="184" width="260" height="288"/>
                <shape id="title" type="text" topLeftX="90" topLeftY="215" width="220" height="30">
                  <content fontSize="18"><p>梦境与现实</p></content>
                </shape>
                <shape id="copy" type="text" topLeftX="90" topLeftY="330" width="220" height="70">
                  <content fontSize="13"><p>边界溶解，逻辑失效。观众被拽入潜意识的迷宫。</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["target"]["container_id"], "card")
        self.assertEqual(issue["rule"]["threshold"], 0.15)

    def test_lint_xml_allows_image_overlay_rect(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <img id="hero" topLeftX="560" topLeftY="0" width="400" height="540"/>
                <shape id="tint" type="rect" topLeftX="560" topLeftY="0" width="400" height="540"/>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["warning_count"], 0)

    def test_lint_xml_does_not_let_transparent_image_overlay_suppress_sparse_warning(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
                  <content fontSize="20"><p>Section title</p></content>
                </shape>
                <shape id="card" type="rect" topLeftX="330" topLeftY="120" width="300" height="300"/>
                <img id="ghost-overlay" src="token" topLeftX="330" topLeftY="120" width="300" height="300" alpha="0"/>
              </data>
            </slide>
            """
        )

        issue = next(
            issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
        )
        self.assertEqual(issue["target"]["container_id"], "card")

    def test_lint_xml_allows_edge_spanning_layout_panel_and_nested_decoration(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="panel" type="rect" topLeftX="600" topLeftY="0" width="360" height="540"/>
                <shape id="decoration" type="rect" topLeftX="660" topLeftY="150" width="240" height="240"/>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["warning_count"], 0)

    def test_lint_xml_counts_icons_as_visible_content(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
                <icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180">
                  <fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["warning_count"], 0)

    def test_lint_xml_does_not_count_transparent_icon_as_visible_content(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
                  <content fontSize="20"><p>Section title</p></content>
                </shape>
                <shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
                <icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180" alpha="0">
                  <fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )

        issue = next(
            issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
        )
        self.assertEqual(issue["target"]["container_id"], "card")
        self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0)

    def test_lint_xml_warns_when_coverage_is_below_global_threshold(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
                <icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="70" height="70">
                  <fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )

        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["target"]["container_id"], "card")
        self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.122)
        self.assertEqual(issue["rule"]["threshold"], 0.15)

    def test_lint_xml_allows_quarter_coverage_under_lower_threshold(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
                <icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="100" height="100">
                  <fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
                </icon>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["slides"][0]["issues"], [])

    def test_lint_xml_allows_large_metric_card_above_lower_threshold(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="metric-card" type="rect" topLeftX="80" topLeftY="140" width="360" height="300"/>
                <shape id="metric" type="text" topLeftX="104" topLeftY="190" width="340" height="90">
                  <content fontSize="12"><p><strong><span fontSize="62">400</span></strong>+ 项</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["slides"][0]["issues"], [])

    def test_lint_xml_does_not_report_blank_slide_for_embed_only_content(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="http://www.larkoffice.com/sml/2.0">
              <data>
                <embed id="emb" topLeftX="280" topLeftY="130" width="400" height="280">
                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 280">
                    <circle cx="200" cy="140" r="100" fill="#2563EB"/>
                  </svg>
                </embed>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 0)
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertNotIn("blank_slide", codes)

    def test_lint_xml_does_not_report_blank_slide_for_line_only_content(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <line id="l1" startX="100" startY="100" endX="800" endY="100"><border/></line>
                <line id="l2" startX="100" startY="200" endX="800" endY="200"><border/></line>
                <line id="l3" startX="100" startY="300" endX="800" endY="300"><border/></line>
                <line id="l4" startX="100" startY="400" endX="800" endY="400"><border/></line>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 0)
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertNotIn("blank_slide", codes)

    def test_lint_xml_reports_bbox_overlap_measurement_from_decision_time_visual_bbox(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="left" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
                  <content fontSize="14"><p>overlap text <span fontSize="96">big</span></p></content>
                </shape>
                <shape id="right" type="text" topLeftX="80" topLeftY="80" width="300" height="80">
                  <content fontSize="14"><p>other overlap text</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        issue = result["slides"][0]["issues"][0]
        self.assertEqual(issue["code"], "bbox_overlap")
        # Must match the visual bbox that should_flag_overlap actually decided with (fontSize=14
        # from extract_elements), not the fontSize=96 max-descendant value that
        # extract_density_elements computes for the same "left" element id.
        self.assertEqual(issue["measurement"]["intersection_width"], 109.2)
        self.assertEqual(issue["measurement"]["intersection_height"], 6.8)
        self.assertEqual(issue["measurement"]["intersection_area"], 742.56)

    def test_has_similar_short_card_peer_excludes_the_element_itself(self) -> None:
        card_a = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
        card_b = {"kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100}
        card_c = {"kind": "shape", "type": "rect", "x": 0, "y": 200, "width": 300, "height": 100}

        self.assertFalse(
            xml_text_overlap_lint.has_similar_short_card_peer(card_a, [card_a, card_b])
        )
        self.assertTrue(
            xml_text_overlap_lint.has_similar_short_card_peer(card_a, [card_a, card_b, card_c])
        )

    def test_lint_xml_reports_schema_version_2_for_sparse_issues(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
              </data>
            </slide>
            """
        )

        issue = next(
            issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
        )
        self.assertEqual(issue["schema_version"], "2.0")

    def test_lint_xml_does_not_report_blank_slide_for_textless_decorative_shapes(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="deco1" type="ellipse" topLeftX="60" topLeftY="60" width="300" height="300">
                  <fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
                </shape>
                <shape id="deco2" type="triangle" topLeftX="500" topLeftY="200" width="200" height="200">
                  <fill><fillColor color="rgba(220, 38, 38, 1)"/></fill>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["summary"]["error_count"], 0)
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertNotIn("blank_slide", codes)

    def test_lint_xml_still_warns_for_sparse_slide_content_despite_full_bleed_background(self) -> None:
        # A plain textless shape now counts as "not blank" (see the test above), but a
        # full-bleed background rect must still NOT count toward sparse_slide_content's
        # meaningful-content coverage ratio -- otherwise every slide with a background would
        # trivially "pass" that density check.
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
                <shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
                  <content fontSize="14"><p>One short line</p></content>
                </shape>
                <shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
                  <content fontSize="14"><p>Another line</p></content>
                </shape>
                <shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
                  <content fontSize="14"><p>Third line</p></content>
                </shape>
                <shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
                  <content fontSize="14"><p>Fourth line</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertIn("sparse_slide_content", codes)

    def test_lint_xml_accepts_whitespace_around_attribute_equals_sign(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="visible" type="text" topLeftX = "80" topLeftY = "80" width = "300" height = "60">
                  <content><p>hello</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(result["slides"][0]["element_count"], 1)
        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertNotIn("blank_slide", codes)

    def test_lint_xml_reports_blank_slide_for_full_canvas_background_only(self) -> None:
        result = xml_text_overlap_lint.lint_xml(
            """
            <slide xmlns="https://www.larkoffice.com/sml/2.0">
              <data>
                <shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540">
                  <fill><fillColor color="rgba(240, 235, 220, 1)"/></fill>
                </shape>
              </data>
            </slide>
            """
        )

        codes = [issue["code"] for issue in result["slides"][0]["issues"]]
        self.assertIn("blank_slide", codes)

    def test_has_similar_short_card_peer_ignores_invisible_peers(self) -> None:
        visible_card = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
        ghost_1 = {
            "kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100, "alpha": 0,
        }
        ghost_2 = {
            "kind": "shape", "type": "rect", "x": 800, "y": 0, "width": 300, "height": 100, "alpha": 0,
        }

        self.assertFalse(
            xml_text_overlap_lint.has_similar_short_card_peer(
                visible_card, [visible_card, ghost_1, ghost_2]
            )
        )


SML_NAMESPACE = "https://www.larkoffice.com/sml/2.0"


class SxsdSyntaxTestCase(unittest.TestCase):
    def validate(self, xml: str) -> list[dict[str, object]]:
        result = xml_text_overlap_lint.lint_xml(xml)
        return [
            *result.get("issues", []),
            *(issue for slide in result["slides"] for issue in slide["issues"]),
        ]

    def assert_issue(
        self,
        issues: list[dict[str, object]],
        code: str,
        *,
        path: str | None = None,
        attr: str | None = None,
    ) -> dict[str, object]:
        for issue in issues:
            if issue.get("code") != code:
                continue
            if path is not None and issue.get("path") != path:
                continue
            if attr is not None and issue.get("attr") != attr:
                continue
            return issue
        self.fail(f"missing issue code={code!r} path={path!r} attr={attr!r}: {issues!r}")

    def assert_no_issue(self, issues: list[dict[str, object]], code: str) -> None:
        self.assertNotIn(code, [issue.get("code") for issue in issues])


class SxsdSyntaxAttributeTest(SxsdSyntaxTestCase):

    def test_xsd_pattern_translation_only_expands_whitespace_classes(self) -> None:
        self.assertEqual(
            sxsd_validator.python_pattern_for_xsd(r"\s+\S+\w+\d+"),
            "[ \\t\\n\\r]+[^ \\t\\n\\r]+\\w+\\d+",
        )

    def test_href_domain_pattern_does_not_use_backtracking_regex(self) -> None:
        pattern = r"[\w.-]+[.:]\S*"
        adversarial_value = ("a." * 20_000) + " "
        original_fullmatch = sxsd_validator.re.fullmatch
        translated_pattern = sxsd_validator.python_pattern_for_xsd(pattern)

        def reject_unsafe_pattern(candidate: str, value: str):
            if candidate == translated_pattern:
                raise AssertionError("href domain pattern must not use re.fullmatch")
            return original_fullmatch(candidate, value)

        with mock.patch.object(sxsd_validator.re, "fullmatch", side_effect=reject_unsafe_pattern):
            self.assertFalse(sxsd_validator.xsd_pattern_matches(pattern, adversarial_value))

    def test_href_domain_pattern_keeps_xsd_matching_behavior(self) -> None:
        pattern = r"[\w.-]+[.:]\S*"
        reference_pattern = sxsd_validator.re.compile(
            sxsd_validator.python_pattern_for_xsd(pattern)
        )

        for length in range(5):
            for characters in itertools.product("a.:-/ ©", repeat=length):
                value = "".join(characters)
                with self.subTest(value=value):
                    self.assertEqual(
                        sxsd_validator.xsd_pattern_matches(pattern, value),
                        reference_pattern.fullmatch(value) is not None,
                    )

    def test_accepts_valid_shape_attributes(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                  <content textType="body"><p>Valid</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(issues, [])

    def test_reports_missing_required_shape_attribute(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data><shape type="text" topLeftX="10" topLeftY="20" width="300"/></data>
            </slide>
            """
        )

        issue = self.assert_issue(
            issues,
            "sxsd_missing_required_attr",
            path="slide/data/shape",
            attr="height",
        )
        self.assertEqual(issue["expected"], "required attribute of type PositiveSize")
        self.assertIsNone(issue["actual"])

    def test_reports_invalid_scalar_value(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="NaN" topLeftY="20" width="300" height="80"/>
              </data>
            </slide>
            """
        )

        issue = self.assert_issue(issues, "sxsd_invalid_scalar", attr="topLeftX")
        self.assertEqual(issue["actual"], "NaN")

    def test_rejects_python_only_numeric_separator(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="rect" topLeftX="1_0" topLeftY="20" width="300" height="80"/>
              </data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_invalid_scalar", attr="topLeftX")

    def test_accepts_xsd_double_lexical_forms(self) -> None:
        for top_left_x in ("10", "-0.5", ".5", "1.", "1e2"):
            with self.subTest(top_left_x=top_left_x):
                issues = self.validate(
                    f"""
                    <slide xmlns="{SML_NAMESPACE}">
                      <data>
                        <shape type="rect" topLeftX="{top_left_x}" topLeftY="20" width="300" height="80"/>
                      </data>
                    </slide>
                    """
                )

                self.assertEqual(issues, [])

    def test_accepts_bullet_char_length_boundaries(self) -> None:
        for bullet_char in ("A", "12345678"):
            with self.subTest(bullet_char=bullet_char):
                issues = self.validate(
                    f"""
                    <slide xmlns="{SML_NAMESPACE}">
                      <data>
                        <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                          <content bulletChar="{bullet_char}"><p>Text</p></content>
                        </shape>
                      </data>
                    </slide>
                    """
                )

                self.assertEqual(issues, [])

    def test_rejects_bullet_char_outside_length_boundaries(self) -> None:
        for bullet_char in ("", "123456789"):
            with self.subTest(bullet_char=bullet_char):
                issues = self.validate(
                    f"""
                    <slide xmlns="{SML_NAMESPACE}">
                      <data>
                        <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                          <content bulletChar="{bullet_char}"><p>Text</p></content>
                        </shape>
                      </data>
                    </slide>
                    """
                )

                self.assert_issue(issues, "sxsd_value_out_of_range", attr="bulletChar")

    def test_rejects_zero_size_that_violates_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="0" height="80"/>
              </data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_value_out_of_range", attr="width")

    def test_reports_negative_size_rejected_by_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="-1" height="80"/>
              </data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_value_out_of_range", attr="width")

    def test_rejects_shape_enum_that_violates_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="not-a-shape" topLeftX="10" topLeftY="20" width="300" height="80"/>
              </data>
            </slide>
            """
        )

        issue = self.assert_issue(issues, "sxsd_invalid_enum", attr="type")
        self.assertLess(len(str(issue["message"])), 300)
        self.assertEqual(issue["actual"], "not-a-shape")

    def test_rejects_rotation_upper_bound_that_violates_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80" rotation="360"/>
              </data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_value_out_of_range", attr="rotation")

    def test_rejects_fill_color_that_violates_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <style><fill><fillColor color="red"/></fill></style>
              <data/>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_pattern_mismatch", attr="color")

    def test_reports_missing_required_image_src(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data><img topLeftX="10" topLeftY="20" width="300" height="80"/></data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_missing_required_attr", attr="src")

    def test_accepts_inline_attribute_simple_type(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                  <content><p><a href="https://example.com">Link</a></p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(issues, [])

    def test_reports_inline_attribute_pattern_mismatch(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                  <content><p><a href="not a uri">Link</a></p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_pattern_mismatch", attr="href")

    def test_accepts_values_matching_inline_union_members(self) -> None:
        for bullet_size in ("25%", "100%", "400%", "6", "14", "400"):
            with self.subTest(bullet_size=bullet_size):
                issues = self.validate(
                    f"""
                    <slide xmlns="{SML_NAMESPACE}">
                      <data>
                        <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                          <content bulletSize="{bullet_size}"><p>Text</p></content>
                        </shape>
                      </data>
                    </slide>
                    """
                )

                self.assertEqual(issues, [])

    def test_rejects_values_outside_inline_union_members(self) -> None:
        for bullet_size in ("24%", "401%", "5", "401", "abc"):
            with self.subTest(bullet_size=bullet_size):
                issues = self.validate(
                    f"""
                    <slide xmlns="{SML_NAMESPACE}">
                      <data>
                        <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                          <content bulletSize="{bullet_size}"><p>Text</p></content>
                        </shape>
                      </data>
                    </slide>
                    """
                )

                self.assert_issue(issues, "sxsd_pattern_mismatch", attr="bulletSize")

    def test_rejects_symbol_outside_python_word_semantics_in_href(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                  <content><p><a href="©:resource">Link</a></p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_pattern_mismatch", attr="href")

    def test_accepts_common_email_href_with_python_regex_semantics(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                  <content><p><a href="mailto:user@example.com">Email</a></p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(issues, [])

    def test_accepts_common_gradient_with_python_regex_semantics(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <style>
                <fill>
                  <fillColor color="linear-gradient(90deg, rgb(255, 0, 0) 0%, rgb(0, 0, 255) 100%)"/>
                </fill>
              </style>
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                  <content><p>Gradient</p></content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(issues, [])

    def test_rejects_non_xsd_whitespace_in_color_pattern(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <style><fill><fillColor color="rgb(1,\u00a02,3)"/></fill></style>
              <data/>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_pattern_mismatch", attr="color")

class SxsdSyntaxStructureTest(SxsdSyntaxTestCase):
    def test_accepts_nested_content_in_referenced_rich_text_shadow(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
                  <content>
                    <p><span><shadow color="rgba(0, 0, 0, 1)"><strong>Text</strong></shadow></span></p>
                  </content>
                </shape>
              </data>
            </slide>
            """
        )

        self.assertEqual(issues, [])

    def test_keeps_shape_effect_shadow_as_childless_local_type(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data>
                <shape type="rect" topLeftX="10" topLeftY="20" width="300" height="80">
                  <shadow><strong>Not rich text</strong></shadow>
                </shape>
              </data>
            </slide>
            """
        )

        self.assert_issue(
            issues,
            "sxsd_unexpected_child",
            path="slide/data/shape/shadow/strong",
        )

    def test_accepts_standalone_slide_fragment_without_namespace(self) -> None:
        issues = self.validate(
            '<slide><data><shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">'
            '<content><p>Text</p></content></shape></data></slide>'
        )

        self.assertEqual(issues, [])

    def test_rejects_presentation_without_namespace(self) -> None:
        issues = self.validate(
            '<presentation width="960" height="540"><slide/></presentation>'
        )

        self.assert_issue(issues, "sxsd_invalid_namespace", path="presentation")

    def test_rejects_wrong_namespace_that_violates_xsd(self) -> None:
        issues = self.validate('<slide xmlns="https://example.com/not-sml"><data/></slide>')

        self.assert_issue(issues, "sxsd_invalid_namespace", path="slide")

    def test_rejects_descendant_outside_document_namespace(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data xmlns="">
                <shape xmlns="{SML_NAMESPACE}" type="rect" topLeftX="10" topLeftY="20" width="300" height="80"/>
              </data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_invalid_namespace", path="slide/data")

    def test_rejects_unexpected_child_that_violates_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <shape type="text" topLeftX="10" topLeftY="20" width="300" height="80"/>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_unexpected_child", path="slide/shape")

    def test_rejects_child_order_that_violates_xsd(self) -> None:
        issues = self.validate(
            f"""
            <presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080">
              <slide/>
              <title>Late title</title>
            </presentation>
            """
        )

        self.assert_issue(issues, "sxsd_invalid_child_order", path="presentation/title")

    def test_enforces_presentation_slide_minimum_from_xsd(self) -> None:
        issues = self.validate(
            f'<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080"/>'
        )

        self.assert_issue(issues, "sxsd_missing_required_child", path="presentation")

    def test_enforces_presentation_slide_maximum_from_xsd(self) -> None:
        slides = "".join("<slide/>" for _ in range(101))
        issues = self.validate(
            f'<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080">{slides}</presentation>'
        )

        self.assert_issue(issues, "sxsd_too_many_children", path="presentation/slide")

    def test_rejects_multiple_choice_children_that_violate_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <style>
                <fill><fillColor/><fillImg src="token"/></fill>
              </style>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_too_many_children", path="slide/style/fill")

    def test_rejects_line_without_required_border_from_xsd(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data><line startX="0" startY="0" endX="100" endY="100"/></data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/line")

    def test_reports_missing_required_chart_structure(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data><chart topLeftX="0" topLeftY="0" width="300" height="200"/></data>
            </slide>
            """
        )

        self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/chart")

    def test_reports_missing_required_nested_sequence_child(self) -> None:
        issues = self.validate(
            f"""
            <slide xmlns="{SML_NAMESPACE}">
              <data><table topLeftX="0" topLeftY="0"><tr/></table></data>
            </slide>
            """
        )

        issue = self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/table/tr")
        self.assertEqual(issue["expected"], "td (at least 1)")


class SxsdSchemaModelTest(unittest.TestCase):
    def test_reports_unsupported_xsd_pattern_without_crashing(self) -> None:
        schema = rf"""
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
                   xmlns:sml="{SML_NAMESPACE}"
                   targetNamespace="{SML_NAMESPACE}"
                   elementFormDefault="qualified">
          <xs:simpleType name="UnsupportedPatternType">
            <xs:union>
              <xs:simpleType>
                <xs:restriction base="xs:string"><xs:pattern value="[\S]"/></xs:restriction>
              </xs:simpleType>
              <xs:simpleType>
                <xs:restriction base="xs:string"><xs:pattern value="z+"/></xs:restriction>
              </xs:simpleType>
            </xs:union>
          </xs:simpleType>
          <xs:complexType name="SlideType">
            <xs:attribute name="value" type="sml:UnsupportedPatternType"/>
          </xs:complexType>
        </xs:schema>
        """
        with tempfile.TemporaryDirectory() as temp_dir:
            schema_path = Path(temp_dir) / "schema.xsd"
            schema_path.write_text(schema, encoding="utf-8")
            try:
                issues = sxsd_validator.validate_sxsd(
                    ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}" value="A"/>'),
                    schema_path,
                )
            except (ValueError, sxsd_validator.re.error) as error:
                self.fail(f"SXSD pattern capability errors must be reported, not raised: {error}")

        self.assertEqual([issue["code"] for issue in issues], ["sxsd_unsupported_pattern"])
        self.assertEqual(issues[0]["attr"], "value")
        self.assertIn("pattern interpreter", str(issues[0]["hint"]).lower())

    def test_standalone_slide_uses_slide_type_without_global_element(self) -> None:
        schema = f"""
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
                   xmlns:sml="{SML_NAMESPACE}"
                   targetNamespace="{SML_NAMESPACE}"
                   elementFormDefault="qualified">
          <xs:complexType name="SlideType"><xs:sequence/></xs:complexType>
          <xs:complexType name="PresentationType">
            <xs:sequence><xs:element name="slide" type="sml:SlideType"/></xs:sequence>
          </xs:complexType>
          <xs:element name="presentation" type="sml:PresentationType"/>
        </xs:schema>
        """
        with tempfile.TemporaryDirectory() as temp_dir:
            schema_path = Path(temp_dir) / "schema.xsd"
            schema_path.write_text(schema, encoding="utf-8")
            issues = sxsd_validator.validate_sxsd(
                ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}"/>'),
                schema_path,
            )

        self.assertEqual(issues, [])

    def test_standalone_slide_requires_slide_type_in_xsd(self) -> None:
        schema = f"""
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
                   xmlns:sml="{SML_NAMESPACE}"
                   targetNamespace="{SML_NAMESPACE}"
                   elementFormDefault="qualified">
          <xs:complexType name="PresentationType"><xs:sequence/></xs:complexType>
          <xs:element name="presentation" type="sml:PresentationType"/>
        </xs:schema>
        """
        with tempfile.TemporaryDirectory() as temp_dir:
            schema_path = Path(temp_dir) / "schema.xsd"
            schema_path.write_text(schema, encoding="utf-8")
            issues = sxsd_validator.validate_sxsd(
                ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}"/>'),
                schema_path,
            )

        self.assertEqual([issue["code"] for issue in issues], ["sxsd_unexpected_root"])


if __name__ == "__main__":
    unittest.main()
