"""Tests for live session context breakdown."""

from unittest.mock import MagicMock, patch

from agent.context_breakdown import compute_session_context_breakdown

def _make_agent(
    *,
    stable: str = "identity and guidance",
    context: str = "",
    volatile: str = "timestamp line",
    tools: list | None = None,
    context_length: int = 200_000,
    last_prompt_tokens: int = 0,
):
    agent = MagicMock()
    agent.model = "openai/gpt-5.4"
    agent.tools = tools or [
        {"type": "function", "function": {"name": "terminal", "description": "run"}},
        {"type": "function", "function": {"name": "mcp_demo_tool", "description": "mcp"}},
        {"type": "function", "function": {"name": "delegate_task", "description": "spawn"}},
    ]
    agent._memory_store = None
    agent._memory_enabled = True
    agent._user_profile_enabled = True
    agent.context_compressor = MagicMock(
        context_length=context_length,
        last_prompt_tokens=last_prompt_tokens,
    )
    return agent, {"stable": stable, "context": context, "volatile": volatile}

def test_breakdown_includes_major_categories():
    stable = (
        "base guidance\n"
        "<available_skills>\n  demo:\n    - hello: hi\n</available_skills>"
    )
    context = "# Project Context\nFollow AGENTS.md"
    volatile = "Current time: now"
    history = [{"role": "user", "content": "hello there"}]
    agent, parts = _make_agent(stable=stable, context=context, volatile=volatile)

    with patch("agent.system_prompt.build_system_prompt_parts", return_value=parts):
        data = compute_session_context_breakdown(agent, history)

    ids = {item["id"] for item in data["categories"]}
    assert {"system_prompt", "tool_definitions", "rules", "skills", "mcp", "subagent_definitions", "conversation"} <= ids
    assert data["context_max"] == 200_000
    assert data["estimated_total"] > 0

def test_context_used_never_exceeds_model_window():
    """Regression for #109760: anchor + appended-delta estimate can overshoot the window
    (355.8k / 262.1k); one prompt can never be larger than the model's context."""
    from agent.context_breakdown import context_usage_fields
    from agent.usage_anchor import capture_usage_anchor

    history = [{"role": "user", "content": "start"}, {"role": "assistant", "content": "ok"}]
    agent, parts = _make_agent(context_length=262_144)
    agent._turn_base_usage_anchor = capture_usage_anchor(250_000, 1_000, history)
    history = history + [{"role": "user", "content": "x" * 800_000}]

    with patch("agent.system_prompt.build_system_prompt_parts", return_value=parts):
        data = compute_session_context_breakdown(agent, history)

    assert data["context_source"] == "provider_usage_plus_estimate"
    assert data["context_used"] <= data["context_max"]
    assert data["context_percent"] == 100

    seeded = context_usage_fields(MagicMock(context_length=262_144, last_prompt_tokens=400_000,
                                            last_real_prompt_tokens=150_000))
    assert seeded["context_used"] <= seeded["context_max"]

def test_empty_transcript_still_reports_the_conversation_category():
    """Zero conversation tokens is a measurement, not an absence (#87903).

    A fresh session (or one whose first turn never completed) estimates the
    conversation at zero. Dropping the row there made "the transcript is empty"
    render identically to "the breakdown never measured it" — the Desktop
    Context usage panel hid Conversation until a turn completed. Structurally
    absent categories (mcp/memory/skills with nothing configured) stay dropped.
    """
    agent, parts = _make_agent(
        tools=[{"type": "function", "function": {"name": "terminal", "description": "run"}}]
    )
    history = []  # nothing said yet

    with patch("agent.system_prompt.build_system_prompt_parts", return_value=parts):
        data = compute_session_context_breakdown(agent, history)

    categories = {item["id"]: item["tokens"] for item in data["categories"]}
    assert categories["conversation"] == 0
    # Optional surfaces with nothing configured are still omitted at zero —
    # "not configured" is exactly what their absence communicates.
    assert "mcp" not in categories
    assert "skills" not in categories

# ── /context renderers (pure functions over the payload) ────────────────────

from agent.context_breakdown import (  # noqa: E402
    render_context_breakdown_lines,
    render_context_grid,
)

def _payload(**overrides):
    base = {
        "categories": [
            {"id": "system_prompt", "label": "System prompt", "tokens": 10_000},
            {"id": "tool_definitions", "label": "Tool definitions", "tokens": 20_000},
            {"id": "skills", "label": "Skills", "tokens": 5_000},
            {"id": "conversation", "label": "Conversation", "tokens": 15_000},
        ],
        "context_max": 200_000,
        "context_percent": 25,
        "context_used": 50_000,
        "estimated_total": 50_000,
        "model": "openai/gpt-test",
    }
    base.update(overrides)
    return base

def test_grid_is_5x20_and_mostly_free():
    rows = render_context_grid(_payload())
    assert len(rows) == 5
    cells = " ".join(rows).split(" ")
    assert len(cells) == 100
    # 50k / 200k → 25 used cells, 75 free
    assert cells.count("·") == 75
    # Category glyphs proportional: 10k→5, 20k→10, 5k→2-3, 15k→7-8 cells
    assert cells.count("■") == 5
    assert cells.count("▣") == 10

def test_breakdown_lines_grid_toggle():
    with_grid = render_context_breakdown_lines(_payload(), grid=True)
    without = render_context_breakdown_lines(_payload(), grid=False)
    assert any("·" in line for line in with_grid[:5])
    assert not any("·" in line for line in without[:2])
