# -*- coding: utf-8 -*-
"""Unit tests for the ACP-update → TuiEvent normalizer."""

from __future__ import annotations

# ``assert normalize_update(...) == []`` reads clearer than ``not ...`` here.
# pylint: disable=use-implicit-booleaness-not-comparison

from types import SimpleNamespace

import pytest

from acp import (
    start_tool_call,
    text_block,
    tool_content,
    update_agent_message,
    update_agent_thought,
    update_tool_call,
)
from acp.schema import (
    AgentMessageChunk,
    AgentPlanUpdate,
    AvailableCommand,
    AvailableCommandsUpdate,
    PlanEntry,
    ResourceContentBlock,
    UsageUpdate,
)

from qwenpaw.cli.tui import events as E
from qwenpaw.cli.tui.normalize import normalize_update

pytestmark = [pytest.mark.unit, pytest.mark.p1]


def test_message_chunk_is_text_delta():
    out = normalize_update(update_agent_message(text_block("hi")))
    assert out == [E.TextDelta("hi")]


def test_empty_message_chunk_is_dropped():
    assert normalize_update(update_agent_message(text_block(""))) == []


def test_message_chunk_with_error_meta_is_transport_error():
    chunk = AgentMessageChunk(
        sessionUpdate="agent_message_chunk",
        content=text_block("Error: bad key"),
        field_meta={"qwenpaw.error": True},
    )
    assert normalize_update(chunk) == [E.TransportError("Error: bad key")]


def test_message_chunk_with_unrelated_meta_is_text():
    chunk = AgentMessageChunk(
        sessionUpdate="agent_message_chunk",
        content=text_block("hello"),
        field_meta={"other": 42},
    )
    assert normalize_update(chunk) == [E.TextDelta("hello")]


def test_usage_meta_chunk_is_token_usage():
    chunk = AgentMessageChunk(
        sessionUpdate="agent_message_chunk",
        content=text_block(""),
        field_meta={
            "usage": {
                "inputTokens": 1200,
                "outputTokens": 340,
                "totalTokens": 1540,
                "model": "qwen3.6-plus",
            },
        },
    )
    assert normalize_update(chunk) == [
        E.TokenUsage(
            input_tokens=1200,
            output_tokens=340,
            total_tokens=1540,
            model="qwen3.6-plus",
        ),
    ]


def test_thought_chunk():
    out = normalize_update(update_agent_thought(text_block("ponder")))
    assert out == [E.ThoughtDelta("ponder")]


def test_tool_call_start_and_update():
    start = normalize_update(
        start_tool_call("t1", "grep", kind="search", status="in_progress"),
    )
    assert start == [
        E.ToolCall(
            tool_call_id="t1",
            title="grep",
            kind="search",
            status="in_progress",
        ),
    ]

    done = normalize_update(
        update_tool_call(
            "t1",
            status="completed",
            content=[tool_content(text_block("3 hits"))],
        ),
    )
    assert len(done) == 1
    ev = done[0]
    assert ev.tool_call_id == "t1"
    assert ev.status == "completed"
    assert ev.output == "3 hits"


def test_tool_call_extracts_resource_link():
    # send_file_to_user → a text block + a resource_link content block.
    [ev] = normalize_update(
        update_tool_call(
            "t9",
            status="completed",
            content=[
                tool_content(text_block("File sent successfully.")),
                tool_content(
                    ResourceContentBlock(
                        type="resource_link",
                        uri="file:///tmp/report.pdf",
                        name="report.pdf",
                        mime_type="application/pdf",
                    ),
                ),
            ],
        ),
    )
    assert ev.output == "File sent successfully."
    assert ev.links == (
        E.FileLink(
            uri="file:///tmp/report.pdf",
            name="report.pdf",
            mime_type="application/pdf",
        ),
    )


def test_tool_call_without_links_has_empty_tuple():
    [ev] = normalize_update(
        update_tool_call(
            "t10",
            status="completed",
            content=[tool_content(text_block("plain output"))],
        ),
    )
    assert ev.links == ()


def test_tool_call_drops_non_local_link_schemes():
    # A buggy/hostile agent emitting an http(s) resource_link must not become a
    # one-click-openable FileLink; only file:// / local paths are surfaced.
    [ev] = normalize_update(
        update_tool_call(
            "t11",
            status="completed",
            content=[
                tool_content(
                    ResourceContentBlock(
                        type="resource_link",
                        uri="https://evil.example/login",
                        name="totally-a-file",
                    ),
                ),
            ],
        ),
    )
    assert ev.links == ()


def test_tool_call_renders_raw_input_params():
    [ev] = normalize_update(
        start_tool_call(
            "t2",
            "execute_shell_command",
            kind="execute",
            status="in_progress",
            raw_input={"command": "ls -la /tmp"},
        ),
    )
    assert ev.params == "command: ls -la /tmp"

    # Multi-key input renders one ``key: value`` line each; non-string
    # values are JSON-encoded.
    [multi] = normalize_update(
        start_tool_call(
            "t3",
            "grep",
            raw_input={"pattern": "TODO", "max": 5},
        ),
    )
    assert multi.params == "pattern: TODO\nmax: 5"


def test_tool_call_accepts_raw_input_camel_case_alias():
    [ev] = normalize_update(
        SimpleNamespace(
            session_update="tool_call",
            tool_call_id="t-camel",
            title="grep",
            rawInput={"pattern": "TODO"},
        ),
    )

    assert ev.params == "pattern: TODO"


def test_tool_call_pretty_prints_long_nested_params():
    """A nested value too long to read inline becomes indented JSON."""
    [ev] = normalize_update(
        start_tool_call(
            "t4",
            "web_search",
            raw_input={
                "query": "textual collapsible",
                "options": {
                    "max_results": 10,
                    "domains": ["docs.textual.io", "github.com"],
                },
            },
        ),
    )
    lines = ev.params.splitlines()
    # The scalar param stays inline (and first, for the title summary)…
    assert lines[0] == "query: textual collapsible"
    # …while the long nested value spreads over indented lines.
    assert "options: {" in lines
    assert '  "max_results": 10,' in lines


def test_tool_call_keeps_short_containers_inline():
    [ev] = normalize_update(
        start_tool_call("t5", "grep", raw_input={"tags": ["a", "b"]}),
    )
    assert ev.params == 'tags: ["a", "b"]'


def test_tool_call_string_json_input_is_pretty_printed():
    raw = (
        '{"path": "/tmp/x.py", "recursive": true,'
        ' "filters": ["*.py", "*.md"]}'
    )
    [ev] = normalize_update(start_tool_call("t6", "find", raw_input=raw))
    lines = ev.params.splitlines()
    assert lines[0] == "{"
    assert '  "path": "/tmp/x.py",' in lines


def test_tool_output_json_is_pretty_printed():
    [ev] = normalize_update(
        update_tool_call(
            "t7",
            status="completed",
            content=[
                tool_content(
                    text_block(
                        '{"status": "ok", "files": ["a.py", "b.py"],'
                        ' "total_matches": 42}',
                    ),
                ),
            ],
        ),
    )
    lines = ev.output.splitlines()
    assert lines[0] == "{"
    assert '  "status": "ok",' in lines
    assert '  "total_matches": 42' in lines


def test_tool_output_non_json_is_unchanged():
    text = "3 files changed, 42 insertions(+) {not json trailing}"
    [ev] = normalize_update(
        update_tool_call(
            "t8",
            status="completed",
            content=[tool_content(text_block(text))],
        ),
    )
    assert ev.output == text


def test_plan_update():
    upd = AgentPlanUpdate(
        session_update="plan",
        entries=[
            PlanEntry(content="step 1", status="completed", priority="high"),
            PlanEntry(content="step 2", status="pending", priority="low"),
        ],
    )
    out = normalize_update(upd)
    assert len(out) == 1
    plan = out[0]
    assert isinstance(plan, E.PlanUpdate)
    assert [e.content for e in plan.entries] == ["step 1", "step 2"]
    assert plan.entries[0].status == "completed"


def test_usage_update():
    out = normalize_update(
        UsageUpdate(session_update="usage_update", used=1200, size=8000),
    )
    assert out == [E.Usage(used=1200, size=8000, threshold=None)]


def test_usage_update_carries_compaction_threshold():
    out = normalize_update(
        UsageUpdate(
            session_update="usage_update",
            used=800_000,
            size=1_000_000,
            field_meta={"compactRatio": 0.8},
        ),
    )
    assert out == [E.Usage(used=800_000, size=1_000_000, threshold=0.8)]


def test_usage_update_ignores_out_of_range_threshold():
    out = normalize_update(
        UsageUpdate(
            session_update="usage_update",
            used=1,
            size=2,
            field_meta={"compactRatio": 1.5},  # not in (0, 1) -> dropped
        ),
    )
    assert out == [E.Usage(used=1, size=2, threshold=None)]


def test_available_commands_update():
    upd = AvailableCommandsUpdate(
        session_update="available_commands_update",
        available_commands=[
            AvailableCommand(name="model", description="switch model"),
            AvailableCommand(name="agent", description=""),
        ],
    )
    out = normalize_update(upd)
    assert out == [
        E.AvailableCommands(
            commands=[
                E.SlashCommand(name="model", description="switch model"),
                E.SlashCommand(name="agent", description=""),
            ],
        ),
    ]


def test_session_info_update_is_session_title():
    from acp.schema import SessionInfoUpdate

    upd = SessionInfoUpdate(
        sessionUpdate="session_info_update",
        title="Fix the parser",
    )
    assert normalize_update(upd) == [E.SessionTitle("Fix the parser")]

    # No title → nothing surfaced.
    cleared = SessionInfoUpdate(sessionUpdate="session_info_update")
    assert normalize_update(cleared) == []


def test_unknown_update_is_empty():
    class Weird:
        session_update = "something_new"

    assert normalize_update(Weird()) == []
