"""Tests for CLI manual compression messaging."""

from unittest.mock import MagicMock, patch

from tests.cli.test_cli_init import _make_cli


def _make_history() -> list[dict[str, str]]:
    return [
        {"role": "user", "content": "one"},
        {"role": "assistant", "content": "two"},
        {"role": "user", "content": "three"},
        {"role": "assistant", "content": "four"},
    ]


def test_manual_compress_keeps_tui_composer_editable(capsys):
    """A follow-up can be drafted and queued while /compress runs."""
    shell = _make_cli()
    history = _make_history()
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id

    observed = {}

    def compress(*_args, **_kwargs):
        # The classic TUI's TextArea consults this state for its read_only
        # condition. Compression must retain its status spinner without
        # preventing the user from drafting the next prompt.
        observed["running"] = shell._command_running
        observed["blocks_input"] = getattr(shell, "_command_blocks_input", shell._command_running)
        return list(history), ""

    shell.agent._compress_context.side_effect = compress

    with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
        shell._manual_compress()

    assert observed == {"running": True, "blocks_input": False}


def test_manual_compress_reports_noop_without_success_banner(capsys):
    shell = _make_cli()
    history = _make_history()
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id  # no-op compression: no split
    shell.agent._compress_context.return_value = (list(history), "")
    # Explicitly signal this is NOT a lock-skip to avoid MagicMock
    # getattr returning a truthy mock for unset attributes.
    shell.agent._compression_skipped_due_to_lock = False

    def _estimate(messages, **_kwargs):
        assert messages == history
        return 100

    with patch("agent.model_metadata.estimate_request_tokens_rough", side_effect=_estimate):
        shell._manual_compress()

    output = capsys.readouterr().out
    assert "No changes from compression" in output
    assert "✅ Compressed" not in output
    assert "Approx request size: ~100 tokens (unchanged)" in output


def test_manual_compress_reports_aborted_summary_without_success_banner(capsys):
    shell = _make_cli()
    history = _make_history()
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id
    shell.agent.context_compressor._last_compress_aborted = True
    shell.agent.context_compressor._last_summary_fallback_used = False
    shell.agent.context_compressor._last_summary_error = (
        "Provider 'opencode-zen' is set in config.yaml but no API key was found."
    )
    shell.agent._compress_context.return_value = (list(history), "")
    # Explicit non-lock-skip: MagicMock getattr would return a truthy mock.
    shell.agent._compression_skipped_due_to_lock = False

    with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
        shell._manual_compress()

    output = capsys.readouterr().out
    assert "⚠️ Compression aborted: 4 messages preserved" in output
    assert "no messages were removed" in output
    assert "no API key was found" in output
    assert "✅ Compressed:" not in output


def test_manual_compress_explains_when_token_estimate_rises(capsys):
    shell = _make_cli()
    history = _make_history()
    compressed = [
        history[0],
        {"role": "assistant", "content": "Dense summary that still counts as more tokens."},
        history[-1],
    ]
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id  # no-op: no split
    shell.agent._compress_context.return_value = (compressed, "")
    shell.agent._compression_skipped_due_to_lock = False

    def _estimate(messages, **_kwargs):
        if messages == history:
            return 100
        if messages == compressed:
            return 120
        raise AssertionError(f"unexpected transcript: {messages!r}")

    with patch("agent.model_metadata.estimate_request_tokens_rough", side_effect=_estimate):
        shell._manual_compress()

    output = capsys.readouterr().out
    assert "✅ Compressed: 4 → 3 messages" in output
    assert "Approx request size: ~100 → ~120 tokens" in output
    assert "denser summaries" in output


def test_manual_compress_syncs_session_id_after_split():
    """Regression for cli.session_id desync after /compress.

    _compress_context ends the parent session and creates a new child session,
    mutating agent.session_id. Without syncing, cli.session_id still points
    at the ended parent — causing /status, /resume, exit summary, and the
    next end_session() call (e.g. from /resume <id>) to target the wrong row.
    """
    shell = _make_cli()
    history = _make_history()
    old_id = shell.session_id
    new_child_id = "20260101_000000_child1"

    compressed = [
        {"role": "user", "content": "[summary]"},
        history[-1],
    ]
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    # Simulate _compress_context mutating agent.session_id as a side effect.
    def _fake_compress(*args, **kwargs):
        shell.agent.session_id = new_child_id
        return (compressed, "")
    shell.agent._compress_context.side_effect = _fake_compress
    shell.agent._compression_skipped_due_to_lock = False
    shell.agent.session_id = old_id  # starts in sync
    shell._pending_title = "stale title"

    with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
        shell._manual_compress()

    # CLI session_id must now point at the continuation child, not the parent.
    assert shell.session_id == new_child_id
    assert shell.session_id != old_id
    # Pending title must be cleared — titles belong to the parent lineage and
    # get regenerated for the continuation.
    assert shell._pending_title is None


def test_manual_compress_flushes_compressed_history_to_child_session_db():
    """Manual /compress must persist the handoff in the continuation DB.

    _compress_context rotates the agent to a new child session and returns a
    compressed transcript whose first messages include the handoff summary. The
    CLI then replaces its in-memory conversation_history with that transcript.
    Because the child DB starts empty, the flush must start from offset 0 rather
    than treating the compressed history as already persisted.
    """
    shell = _make_cli()
    history = _make_history()
    old_id = shell.session_id
    new_child_id = "20260101_000000_child1"
    compressed = [
        {"role": "user", "content": "[CONTEXT COMPACTION — REFERENCE ONLY] compacted"},
        history[-1],
    ]
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.session_id = old_id

    def _fake_compress(*args, **kwargs):
        shell.agent.session_id = new_child_id
        return (compressed, "")

    shell.agent._compress_context.side_effect = _fake_compress
    shell.agent._compression_skipped_due_to_lock = False

    with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
        shell._manual_compress()

    shell.agent._flush_messages_to_session_db.assert_called_once_with(compressed, None)


def test_manual_compress_does_not_flush_full_history_when_session_id_unchanged():
    shell = _make_cli()
    history = _make_history()
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.session_id = shell.session_id
    shell.agent._compress_context.return_value = (list(history), "")
    shell.agent._compression_skipped_due_to_lock = False

    with patch("agent.model_metadata.estimate_messages_tokens_rough", return_value=100):
        shell._manual_compress()

    shell.agent._flush_messages_to_session_db.assert_not_called()


def test_manual_compress_runs_when_auto_compaction_disabled(capsys):
    """compression.enabled: false disables *automatic* compaction only.

    Manual /compress must still work: the context-overflow error path
    (agent/conversation_loop.py) explicitly directs users to /compress when
    auto-compaction is off, and the gateway's /compress handler has never
    gated on the flag. Regression for the CLI refusing with "Compression is
    disabled in config."
    """
    shell = _make_cli()
    history = _make_history()
    compressed = [
        {"role": "user", "content": "[summary]"},
        history[-1],
    ]
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = False
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id
    shell.agent._compress_context.return_value = (compressed, "")
    # Explicit non-lock-skip: MagicMock getattr would return a truthy mock.
    shell.agent._compression_skipped_due_to_lock = False

    with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
        shell._manual_compress()

    output = capsys.readouterr().out
    assert "Compression is disabled" not in output
    shell.agent._compress_context.assert_called_once()
    # Manual compression bypasses the summary-failure cooldown.
    assert shell.agent._compress_context.call_args.kwargs.get("force") is True
    assert shell.conversation_history == compressed


def test_manual_compress_no_sync_when_session_id_unchanged():
    """If compression is a no-op (agent.session_id didn't change), the CLI
    must NOT clear _pending_title or otherwise disturb session state.
    """
    shell = _make_cli()
    history = _make_history()
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id
    shell.agent._compress_context.return_value = (list(history), "")
    shell.agent._compression_skipped_due_to_lock = False
    shell._pending_title = "keep me"

    with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
        shell._manual_compress()

    # No split → pending title untouched.
    assert shell._pending_title == "keep me"


def test_manual_compress_shows_lock_skip_without_confirmed_holder(capsys):
    """When _compress_context skips due to the compression lock WITHOUT a
    confirmed holder (signal=True — acquisition failed but
    get_compression_lock_holder returned nothing, e.g. a SQLite error made
    try_acquire return False), _manual_compress must print the unconfirmed
    lock-skip wording, NOT claim another compression is definitely running,
    and NOT show the misleading "No changes from compression" no-op text."""
    shell = _make_cli()
    history = _make_history()
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id

    # Simulate _compress_context setting the lock-skip signal and
    # returning unchanged messages.
    def _fake_compress(*args, **kwargs):
        shell.agent._compression_skipped_due_to_lock = True
        return (list(history), "")

    shell.agent._compress_context.side_effect = _fake_compress

    with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
        shell._manual_compress()

    output = capsys.readouterr().out
    assert "Compression skipped" in output
    assert "could not acquire" in output
    # No confirmed holder → must not assert one is running.
    assert "already in progress" not in output
    assert "No changes from compression" not in output
    # Signal should be cleared after use.
    assert shell.agent._compression_skipped_due_to_lock is None


def test_manual_compress_shows_lock_in_progress_with_holder(capsys):
    """When the lock holder is a descriptive string, include it in the
    status message so the user knows which process to investigate."""
    shell = _make_cli()
    history = _make_history()
    shell.conversation_history = history
    shell.agent = MagicMock()
    shell.agent.compression_enabled = True
    shell.agent._cached_system_prompt = ""
    shell.agent.tools = None
    shell.agent.session_id = shell.session_id

    def _fake_compress(*args, **kwargs):
        shell.agent._compression_skipped_due_to_lock = "pid=12345"
        return (list(history), "")

    shell.agent._compress_context.side_effect = _fake_compress

    with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
        shell._manual_compress()

    output = capsys.readouterr().out
    assert "Compression already in progress" in output
    assert "pid=12345" in output
    assert "No changes from compression" not in output
