"""
测试自适应截断 —— _execute_action_internal 中基于剩余上下文预算的截断逻辑
"""
import sys
import tempfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest
from aacode.core.react_loop import AsyncReActLoop
from aacode.utils.context_manager import ContextManager
from aacode.utils.tool_registry import get_global_registry, ToolSchema, ToolParameter


@pytest.fixture
def registry():
    reg = get_global_registry()
    reg.tools.clear()
    reg.schemas.clear()
    yield reg


def _make_loop(max_context_length=131072):
    """创建最小 AsyncReActLoop（返回 (loop, tmpdir_path)）"""
    td = tempfile.mkdtemp()
    from aacode.config import ContextConfig
    cfg = ContextConfig()
    cfg.max_context_length = max_context_length

    async def dummy_caller(messages):
        return {"text": "done", "tool_calls": []}

    cm = ContextManager(Path(td))
    loop = AsyncReActLoop(
        model_caller=dummy_caller,
        tools={},
        context_manager=cm,
        context_config=cfg,
    )
    return loop, td


def _reg(registry, name, func):
    schema = ToolSchema(name=name, description="", parameters=[])
    registry.register(func, schema)


class TestRunShellPassthrough:
    """run_shell dict 跳过框架截断"""

    @pytest.mark.asyncio
    async def test_run_shell_dict_passes_through(self, registry):
        async def shell(**kwargs):
            return {"success": True, "stdout": "A" * 100000}
        loop, td = _make_loop()
        _reg(registry, "run_shell", shell)
        loop.tools["run_shell"] = shell
        loop._adaptive_threshold_chars = 500

        result = await loop._execute_action_internal("run_shell", {"command": "test"})
        assert "success" in result
        assert "stdout" in result
        assert "Output too long" not in result

    @pytest.mark.asyncio
    async def test_run_shell_non_dict_still_truncated(self, registry):
        async def shell(**kwargs):
            return "A" * 100000
        loop, td = _make_loop()
        _reg(registry, "run_shell", shell)
        loop.tools["run_shell"] = shell
        loop._adaptive_threshold_chars = 500

        result = await loop._execute_action_internal("run_shell", {"command": "test"})
        assert "Output too long" in result
        assert "saved to file" in result


class TestAdaptiveThresholdBehavior:
    """自适应截断核心逻辑"""

    @pytest.mark.asyncio
    async def test_no_threshold_returns_full(self, registry):
        async def tool(**kwargs):
            return "B" * 50000
        loop, td = _make_loop()
        _reg(registry, "test_tool", tool)
        loop.tools["test_tool"] = tool

        result = await loop._execute_action_internal("test_tool", {})
        assert len(result) == 50000
        assert "Output too long" not in result

    @pytest.mark.asyncio
    async def test_output_under_threshold_returns_full(self, registry):
        async def tool(**kwargs):
            return "C" * 1000
        loop, td = _make_loop()
        _reg(registry, "test_tool", tool)
        loop.tools["test_tool"] = tool
        loop._adaptive_threshold_chars = 5000

        result = await loop._execute_action_internal("test_tool", {})
        assert len(result) == 1000
        assert "Output too long" not in result

    @pytest.mark.asyncio
    async def test_output_over_threshold_saves_and_returns_preview(self, registry):
        async def tool(**kwargs):
            return "D" * 20000
        loop, td = _make_loop()
        _reg(registry, "test_tool", tool)
        loop.tools["test_tool"] = tool
        loop._adaptive_threshold_chars = 5000

        result = await loop._execute_action_internal("test_tool", {})
        assert "Output too long" in result
        assert "saved to file" in result
        assert "tokens" in result

    @pytest.mark.asyncio
    async def test_preview_respects_max_preview_tokens(self, registry):
        from aacode.config import settings
        max_preview = int(settings.output.max_preview_tokens * 4)
        async def tool(**kwargs):
            return "E" * 100000
        loop, td = _make_loop()
        _reg(registry, "test_tool", tool)
        loop.tools["test_tool"] = tool
        loop._adaptive_threshold_chars = 50000

        result = await loop._execute_action_internal("test_tool", {})
        import re
        match = re.search(r"First (\d+) chars preview", result)
        assert match, "Preview line not found in result"
        shown = int(match.group(1))
        assert shown <= max_preview

    @pytest.mark.asyncio
    async def test_save_failure_fallback(self, registry):
        async def tool(**kwargs):
            return "F" * 20000
        loop, td = _make_loop()
        _reg(registry, "test_tool", tool)
        loop.tools["test_tool"] = tool
        loop._adaptive_threshold_chars = 5000

        # Mock save_large_output to fail
        original = loop.context_manager.save_large_output
        async def failing_save(*args, **kwargs):
            raise IOError("disk full")
        loop.context_manager.save_large_output = failing_save

        result = await loop._execute_action_internal("test_tool", {})
        assert "Save failed" in result or "truncated" in result

    @pytest.mark.asyncio
    async def test_preview_minimum_500_chars(self, registry):
        async def tool(**kwargs):
            return "G" * 20000
        loop, td = _make_loop()
        _reg(registry, "test_tool", tool)
        loop.tools["test_tool"] = tool
        loop._adaptive_threshold_chars = 100

        result = await loop._execute_action_internal("test_tool", {})
        assert "Output too long" in result


class TestAdaptiveThresholdCalculation:
    """自适应阈值公式验证"""

    def _calc(self, current_tokens, max_context=16000, ratio=0.6):
        remaining = max(0, max_context - current_tokens)
        return int(remaining * ratio * 4)

    def test_threshold_from_remaining_budget(self):
        from aacode.config import settings
        r = settings.output.budget_ratio
        chars = self._calc(5000, 20000, r)
        assert chars == int(15000 * 0.6 * 4)

    def test_threshold_decreases_as_context_fills(self):
        early = self._calc(2000, 20000)
        late = self._calc(15000, 20000)
        assert early > late

    def test_threshold_non_negative(self):
        chars = self._calc(50000, 10000)
        assert chars == 0
        assert chars >= 0

    def test_empty_context_gives_max_threshold(self):
        chars = self._calc(0, 16000)
        assert chars == int(16000 * 0.6 * 4)


class TestDisplayFormatObservation:
    """显示格式化验证"""

    @pytest.mark.asyncio
    async def test_short_output_untouched(self, registry):
        async def tool(**kwargs):
            return "short content"
        loop, td = _make_loop()
        _reg(registry, "test_tool", tool)
        loop.tools["test_tool"] = tool

        result = await loop._execute_action_internal("test_tool", {})
        formatted = loop._format_observation_for_display("test_tool", result)
        assert "short content" in formatted
        assert "truncated" not in formatted.lower()

    def test_display_long_output_truncated(self):
        content = "X" * 10000
        formatted = AsyncReActLoop._format_observation_for_display(None, "any", content)
        assert "Display truncated" in formatted
        assert "Agent received full content" in formatted

    def test_display_short_output_not_truncated(self):
        content = "hello"
        formatted = AsyncReActLoop._format_observation_for_display(None, "any", content)
        assert formatted == "hello"


class TestOutputConfigNewDefaults:
    """OutputConfig 新默认值"""

    def test_budget_ratio(self):
        from aacode.config import OutputConfig
        assert OutputConfig().budget_ratio == 0.6

    def test_max_preview_tokens(self):
        from aacode.config import OutputConfig
        assert OutputConfig().max_preview_tokens == 2000

    def test_display_preview_chars(self):
        from aacode.config import OutputConfig
        assert OutputConfig().display_preview_chars == 3000

    def test_old_fields_removed(self):
        from aacode.config import OutputConfig
        c = OutputConfig()
        for attr in [
            "test_output_threshold", "code_content_threshold",
            "normal_output_threshold", "test_output_preview",
            "code_content_preview", "normal_output_preview",
            "test_summary_enabled", "test_summary_max_lines",
        ]:
            assert not hasattr(c, attr), f"{attr} should be removed"
