"""Retire stale vision_analyze / screenshot tool images inside the protected tail.

Issue #92699: native embeds sitting in protect_last_n ride every later
request, compression savings stay tiny, and anti-thrash disables further
compaction. Pass 2 of ``_prune_old_tool_results`` only strips images
*outside* the tail; this suite pins the extra keep-newest pass.
"""

from __future__ import annotations

from agent.context_compressor import (
    ContextCompressor,
    _MAX_KEEP_TOOL_IMAGES,
    _content_has_images,
    _tool_content_has_images,
)


def _compressor() -> ContextCompressor:
    c = ContextCompressor.__new__(ContextCompressor)
    c.quiet_mode = True
    return c


def _image_tool(i: int, *, blob: str = "A" * 400) -> list[dict]:
    return [
        {
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {
                    "id": f"call_{i}",
                    "type": "function",
                    "function": {
                        "name": "vision_analyze",
                        "arguments": f'{{"image_url":"shot{i}.png"}}',
                    },
                }
            ],
        },
        {
            "role": "tool",
            "tool_call_id": f"call_{i}",
            "content": [
                {"type": "text", "text": f"Image attached natively shot {i}"},
                {
                    "type": "image_url",
                    "image_url": {"url": f"data:image/png;base64,{blob}{i}"},
                },
            ],
        },
    ]


def _image_bearing_tools(messages: list[dict]) -> list[dict]:
    return [
        m
        for m in messages
        if m.get("role") == "tool" and _tool_content_has_images(m.get("content"))
    ]


class TestRetireStaleToolImagesInProtectedTail:
    def test_keep_newest_images_inside_a_large_protect_window(self):
        """Five native embeds all fit in protect_last_n=20; only newest N stay."""
        msgs: list[dict] = [{"role": "user", "content": "screenshot QA"}]
        for i in range(5):
            msgs.extend(_image_tool(i))
        msgs.append({"role": "user", "content": "compare the last few"})

        out, pruned = _compressor()._prune_old_tool_results(
            msgs, protect_tail_count=20,
        )
        assert pruned >= 5 - _MAX_KEEP_TOOL_IMAGES
        kept = _image_bearing_tools(out)
        assert len(kept) == _MAX_KEEP_TOOL_IMAGES
        kept_ids = [m["tool_call_id"] for m in kept]
        assert kept_ids == [f"call_{i}" for i in range(5 - _MAX_KEEP_TOOL_IMAGES, 5)]

    def test_multimodal_envelopes_outside_keep_window_become_text(self):
        msgs: list[dict] = [{"role": "user", "content": "go"}]
        for i in range(_MAX_KEEP_TOOL_IMAGES + 1):
            msgs.append({
                "role": "assistant",
                "content": None,
                "tool_calls": [{
                    "id": f"env_{i}",
                    "type": "function",
                    "function": {"name": "vision_analyze", "arguments": "{}"},
                }],
            })
            msgs.append({
                "role": "tool",
                "tool_call_id": f"env_{i}",
                "content": {
                    "_multimodal": True,
                    "content": [
                        {"type": "text", "text": f"shot {i}"},
                        {
                            "type": "image_url",
                            "image_url": {"url": f"data:image/png;base64,XYZ{i}"},
                        },
                    ],
                    "text_summary": f"native shot {i}",
                },
            })

        out, _ = _compressor()._prune_old_tool_results(
            msgs, protect_tail_count=20,
        )
        oldest = next(m for m in out if m.get("tool_call_id") == "env_0")
        assert isinstance(oldest["content"], str)
        assert "native shot 0" in oldest["content"]
        newest = next(
            m for m in out
            if m.get("tool_call_id") == f"env_{_MAX_KEEP_TOOL_IMAGES}"
        )
        assert _tool_content_has_images(newest["content"])

    def test_user_uploads_are_not_retired(self):
        user_img = {
            "role": "user",
            "content": [
                {"type": "text", "text": "look"},
                {
                    "type": "image_url",
                    "image_url": {"url": "data:image/png;base64,USERUPLOAD"},
                },
            ],
        }
        msgs = [user_img]
        for i in range(_MAX_KEEP_TOOL_IMAGES + 2):
            msgs.extend(_image_tool(i))

        out, _ = _compressor()._prune_old_tool_results(
            msgs, protect_tail_count=20,
        )
        assert _content_has_images(out[0]["content"])
        assert out[0]["content"][1]["image_url"]["url"].endswith("USERUPLOAD")


class TestSharedImageStripHelper:
    """One strip policy for pass 3.5 and the demote pass (#92783 follow-up)."""

    def test_demote_pass_drops_stale_api_content_on_image_strip(self):
        """Pass 2's image demotion must drop the api_content sidecar.

        Before the shared _strip_images_from_tool_msg helper, only pass 3.5
        dropped the sidecar; the demote branches left it behind, letting
        replay restore pre-strip bytes.
        """
        from agent.context_compressor import _strip_images_from_tool_msg

        msg = {
            "role": "tool",
            "tool_call_id": "c1",
            "api_content": "stale exact-wire copy with image bytes",
            "content": [
                {"type": "text", "text": "shot"},
                {
                    "type": "image_url",
                    "image_url": {"url": "data:image/png;base64," + "A" * 400},
                },
            ],
        }
        new_msg = _strip_images_from_tool_msg(msg)
        assert new_msg is not None
        assert "api_content" not in new_msg
        # Input untouched (copy-on-write).
        assert "api_content" in msg
        assert _content_has_images(msg["content"])
        assert not _content_has_images(new_msg["content"])

    def test_envelope_collapses_to_summary_string(self):
        from agent.context_compressor import _strip_images_from_tool_msg

        msg = {
            "role": "tool",
            "tool_call_id": "c2",
            "api_content": "stale",
            "content": {
                "_multimodal": True,
                "content": [
                    {"type": "text", "text": "s"},
                    {
                        "type": "image_url",
                        "image_url": {"url": "data:image/png;base64,XYZ"},
                    },
                ],
                "text_summary": "native shot",
            },
        }
        new_msg = _strip_images_from_tool_msg(msg)
        assert new_msg is not None
        assert isinstance(new_msg["content"], str)
        assert "native shot" in new_msg["content"]
        assert "api_content" not in new_msg

    def test_imageless_content_returns_none(self):
        from agent.context_compressor import _strip_images_from_tool_msg

        msg = {"role": "tool", "tool_call_id": "c3", "content": "plain text"}
        assert _strip_images_from_tool_msg(msg) is None
        msg2 = {
            "role": "tool",
            "tool_call_id": "c4",
            "content": [{"type": "text", "text": "no images here"}],
        }
        assert _strip_images_from_tool_msg(msg2) is None
