# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License - see LICENSE file for details

"""Tests for Pipeline 3: Streaming in CLI, Telegram, and Discord channels."""

import io
from unittest.mock import AsyncMock, MagicMock

import pytest

from familiar.core.providers import StreamEvent, StreamEventType


def _make_stream_events(chunks, tool_name=None):
    """Build a list of StreamEvents for testing."""
    events = []
    if tool_name:
        events.append(StreamEvent(type=StreamEventType.TOOL_START, tool_name=tool_name))
        events.append(StreamEvent(type=StreamEventType.TOOL_END, tool_name=tool_name))
    for chunk in chunks:
        events.append(StreamEvent(type=StreamEventType.TEXT, content=chunk))
    events.append(StreamEvent(type=StreamEventType.DONE))
    return events


# ── CLI: mock chat_stream → verify chunks printed ────────────

def test_cli_streaming_prints_chunks():
    """Verify CLI prints text chunks from chat_stream incrementally."""
    events = _make_stream_events(["Hello ", "world!"])

    mock_agent = MagicMock()
    mock_agent.chat_stream.return_value = iter(events)

    # Simulate what the CLI loop does for streaming
    output = io.StringIO()
    final_text = ""
    for event in mock_agent.chat_stream("test", user_id="u", channel="cli"):
        if event.type == StreamEventType.TEXT:
            output.write(event.content)
            final_text += event.content
        elif event.type == StreamEventType.TOOL_START:
            output.write(f"\n[Using {event.tool_name}...]")
        elif event.type == StreamEventType.TOOL_END:
            output.write(" done.")

    assert final_text == "Hello world!"
    assert output.getvalue() == "Hello world!"


def test_cli_streaming_shows_tool_events():
    """Verify CLI displays tool start/end indicators."""
    events = _make_stream_events(["Result here."], tool_name="web_search")
    mock_agent = MagicMock()
    mock_agent.chat_stream.return_value = iter(events)

    output = io.StringIO()
    for event in mock_agent.chat_stream("test", user_id="u", channel="cli"):
        if event.type == StreamEventType.TEXT:
            output.write(event.content)
        elif event.type == StreamEventType.TOOL_START:
            output.write(f"\n[Using {event.tool_name}...]")
        elif event.type == StreamEventType.TOOL_END:
            output.write(" done.\n")

    rendered = output.getvalue()
    assert "[Using web_search...]" in rendered
    assert "done." in rendered
    assert "Result here." in rendered


# ── CLI: chat_stream raises → fallback to chat() ─────────────

def test_cli_streaming_fallback_on_error():
    """If chat_stream() fails, CLI should fall back to chat()."""
    mock_agent = MagicMock()
    mock_agent.chat_stream.side_effect = RuntimeError("stream broken")
    mock_agent.chat.return_value = "Fallback response"

    # Simulate the CLI fallback logic
    response = None
    try:
        for _ in mock_agent.chat_stream("test", user_id="u", channel="cli"):
            pass
    except Exception:
        if response is None:
            response = mock_agent.chat("test", user_id="u", channel="cli")

    assert response == "Fallback response"
    mock_agent.chat.assert_called_once()


# ── Telegram: mock streaming → verify message edit ────────────

@pytest.mark.asyncio
async def test_telegram_streaming_edits_placeholder():
    """Telegram streaming should send placeholder then edit with final text."""
    events = _make_stream_events(["Streamed ", "response."])

    mock_agent = MagicMock()
    mock_agent.chat_stream.return_value = iter(events)

    # Simulate what telegram does: collect stream → edit placeholder
    collected = ""
    for event in mock_agent.chat_stream("msg", user_id="u", channel="telegram"):
        if event.type == StreamEventType.TEXT:
            collected += event.content
        elif event.type == StreamEventType.ERROR:
            collected = event.error or "(error)"

    final = collected or "(no response)"
    assert final == "Streamed response."

    # Verify we'd edit the placeholder
    mock_message = AsyncMock()
    await mock_message.edit_text(final[:4096])
    mock_message.edit_text.assert_called_once_with("Streamed response.")


# ── Discord: mock streaming → verify message edit ─────────────

@pytest.mark.asyncio
async def test_discord_streaming_edits_placeholder():
    """Discord streaming should send placeholder then edit with final text."""
    events = _make_stream_events(["Discord ", "stream!"])

    mock_agent = MagicMock()
    mock_agent.chat_stream.return_value = iter(events)

    collected = ""
    for event in mock_agent.chat_stream("msg", user_id="u", channel="discord"):
        if event.type == StreamEventType.TEXT:
            collected += event.content
        elif event.type == StreamEventType.ERROR:
            collected = event.error or "(error)"

    final = collected or "(no response)"
    assert final == "Discord stream!"

    mock_placeholder = AsyncMock()
    await mock_placeholder.edit(content=final[:2000])
    mock_placeholder.edit.assert_called_once_with(content="Discord stream!")
