"""Tests for Issue #293: Toggle model thinking visibility in wee-cli and WebUI.

Covers:
- ThinkingBuffer: strips thinking by default, passes through when show=True
- ThinkingBuffer: handles partial tags spanning chunk boundaries
- ThinkingBuffer: handles unclosed <think> blocks
- _strip_thinking: post-processing helper
- chat_stream: show_thinking=False (default) strips thinking from stdout
- chat_stream: show_thinking=True passes thinking to stdout
- build_parser: --show-thinking flag exists and defaults to False
- run_single_shot: show_thinking threads through to chat_stream
- agent_manager.strip_thinking_tags: existing behavior unchanged
"""

import json
import sys
import os
import unittest
from io import StringIO
from unittest.mock import MagicMock, patch, call

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from wee_cli import (
    ThinkingBuffer,
    _strip_thinking,
    build_parser,
    chat_stream,
    run_single_shot,
)

# ---------------------------------------------------------------------------
# ThinkingBuffer unit tests
# ---------------------------------------------------------------------------


class TestThinkingBuffer(unittest.TestCase):
    """Unit tests for the ThinkingBuffer streaming state machine."""

    def test_no_thinking_passthrough(self):
        """Non-thinking content passes through unchanged."""
        buf = ThinkingBuffer(show=False)
        result = buf.feed("Hello, world!")
        result += buf.flush()
        self.assertEqual(result, "Hello, world!")
        self.assertEqual(buf.thinking_blocks, [])

    def test_thinking_stripped_by_default(self):
        """<think>...</think> blocks are stripped from output when show=False."""
        buf = ThinkingBuffer(show=False)
        text = "Before<think>reasoning here</think>After"
        result = buf.feed(text)
        result += buf.flush()
        self.assertEqual(result, "BeforeAfter")
        self.assertEqual(buf.thinking_blocks, ["reasoning here"])

    def test_thinking_shown_when_enabled(self):
        """<think>...</think> blocks are included in output when show=True."""
        buf = ThinkingBuffer(show=True)
        text = "Before<think>reasoning here</think>After"
        result = buf.feed(text)
        result += buf.flush()
        self.assertEqual(result, "Before<think>reasoning here</think>After")
        self.assertEqual(buf.thinking_blocks, ["reasoning here"])

    def test_thinking_stripped_across_chunks(self):
        """Thinking stripping works when tags span multiple tokens."""
        buf = ThinkingBuffer(show=False)
        tokens = ["Pre", "<", "thi", "nk>", "some reason", "ing</", "think", ">Post"]
        result = "".join(buf.feed(t) for t in tokens)
        result += buf.flush()
        self.assertEqual(result, "PrePost")
        self.assertIn("some reasoning", buf.thinking_blocks[0])

    def test_thinking_shown_across_chunks(self):
        """Thinking passthrough works when tags span multiple tokens."""
        buf = ThinkingBuffer(show=True)
        tokens = ["Pre<thi", "nk>reas", "on</think>Post"]
        result = "".join(buf.feed(t) for t in tokens)
        result += buf.flush()
        self.assertIn("Pre", result)
        self.assertIn("Post", result)
        self.assertIn("reason", result)

    def test_unclosed_think_block_suppressed(self):
        """Unclosed <think> block at end of stream is suppressed when show=False."""
        buf = ThinkingBuffer(show=False)
        result = buf.feed("Before<think>dangling thought")
        result += buf.flush()
        self.assertEqual(result, "Before")
        self.assertEqual(len(buf.thinking_blocks), 1)
        self.assertIn("dangling thought", buf.thinking_blocks[0])

    def test_unclosed_think_block_shown(self):
        """Unclosed <think> block at end of stream is included when show=True."""
        buf = ThinkingBuffer(show=True)
        result = buf.feed("Before<think>dangling thought")
        result += buf.flush()
        self.assertIn("Before", result)
        self.assertIn("dangling thought", result)

    def test_multiple_thinking_blocks(self):
        """Multiple thinking blocks are all captured."""
        buf = ThinkingBuffer(show=False)
        text = "A<think>t1</think>B<think>t2</think>C"
        result = buf.feed(text)
        result += buf.flush()
        self.assertEqual(result, "ABC")
        self.assertEqual(len(buf.thinking_blocks), 2)
        self.assertEqual(buf.thinking_blocks[0], "t1")
        self.assertEqual(buf.thinking_blocks[1], "t2")

    def test_empty_think_block(self):
        """Empty <think></think> is stripped without error."""
        buf = ThinkingBuffer(show=False)
        result = buf.feed("Hello<think></think>World")
        result += buf.flush()
        self.assertEqual(result, "HelloWorld")

    def test_no_thinking_in_response(self):
        """Plain response without any thinking tags is unchanged."""
        buf = ThinkingBuffer(show=False)
        text = "This is a plain response with no thinking."
        result = buf.feed(text)
        result += buf.flush()
        self.assertEqual(result, text)
        self.assertEqual(buf.thinking_blocks, [])

    def test_single_token_per_char(self):
        """Works correctly even when fed one character at a time."""
        buf = ThinkingBuffer(show=False)
        text = "A<think>t</think>B"
        result = "".join(buf.feed(c) for c in text)
        result += buf.flush()
        self.assertEqual(result, "AB")
        self.assertEqual(buf.thinking_blocks, ["t"])


# ---------------------------------------------------------------------------
# _strip_thinking utility
# ---------------------------------------------------------------------------


class TestStripThinking(unittest.TestCase):
    """Tests for _strip_thinking post-processing helper."""

    def test_strips_complete_block(self):
        result = _strip_thinking("Hello<think>reasoning</think>World")
        self.assertNotIn("<think>", result)
        self.assertNotIn("reasoning", result)
        self.assertIn("Hello", result)
        self.assertIn("World", result)

    def test_strips_unclosed_block(self):
        result = _strip_thinking("Before<think>trailing")
        self.assertNotIn("<think>", result)
        self.assertNotIn("trailing", result)
        self.assertEqual(result, "Before")

    def test_strips_multiline_block(self):
        result = _strip_thinking("A<think>\nline1\nline2\n</think>B")
        self.assertEqual(result, "AB")

    def test_no_op_without_thinking(self):
        text = "Just a normal response."
        result = _strip_thinking(text)
        self.assertEqual(result, text)

    def test_strips_multiple_blocks(self):
        result = _strip_thinking("A<think>t1</think>B<think>t2</think>C")
        self.assertEqual(result, "ABC")


# ---------------------------------------------------------------------------
# build_parser: --show-thinking flag
# ---------------------------------------------------------------------------


class TestBuildParserShowThinking(unittest.TestCase):
    """Test that --show-thinking flag exists and defaults correctly."""

    def test_default_is_false(self):
        parser = build_parser()
        args = parser.parse_args(["Hello"])
        self.assertFalse(args.show_thinking)

    def test_flag_sets_true(self):
        parser = build_parser()
        args = parser.parse_args(["--show-thinking", "Hello"])
        self.assertTrue(args.show_thinking)

    def test_flag_in_help(self):
        parser = build_parser()
        help_text = parser.format_help()
        self.assertIn("show-thinking", help_text)


# ---------------------------------------------------------------------------
# chat_stream: show_thinking integration
# ---------------------------------------------------------------------------


def _make_mock_stream(tokens):
    """Build a mock OpenAI streaming response from a list of token strings."""
    chunks = []
    for token in tokens:
        chunk = MagicMock()
        chunk.choices = [MagicMock()]
        chunk.choices[0].delta.content = token
        chunk.choices[0].delta.tool_calls = None
        chunk.usage = None
        chunks.append(chunk)
    # Final chunk with no content
    final = MagicMock()
    final.choices = [MagicMock()]
    final.choices[0].delta.content = None
    final.choices[0].delta.tool_calls = None
    final.usage = None
    chunks.append(final)
    return iter(chunks)


class TestChatStreamThinking(unittest.TestCase):
    """Integration tests for chat_stream with thinking visibility."""

    def _make_client(self, tokens):
        client = MagicMock()
        client.chat.completions.create.return_value = _make_mock_stream(tokens)
        return client

    def test_thinking_hidden_by_default(self):
        """By default, thinking content is not written to stdout."""
        tokens = ["Before", "<think>", "secret reasoning", "</think>", "After"]
        client = self._make_client(tokens)
        stdout_capture = StringIO()
        with patch("sys.stdout", stdout_capture):
            response = chat_stream(
                client=client,
                model="test-model",
                messages=[{"role": "user", "content": "test"}],
                show_thinking=False,
            )
        output = stdout_capture.getvalue()
        self.assertNotIn("secret reasoning", output)
        self.assertIn("Before", output)
        self.assertIn("After", output)
        # Full response with thinking is preserved in return value for history
        self.assertIn("secret reasoning", response)

    def test_thinking_shown_when_enabled(self):
        """With show_thinking=True, thinking content is written to stdout."""
        tokens = ["Before", "<think>", "reasoning", "</think>", "After"]
        client = self._make_client(tokens)
        stdout_capture = StringIO()
        with patch("sys.stdout", stdout_capture):
            response = chat_stream(
                client=client,
                model="test-model",
                messages=[{"role": "user", "content": "test"}],
                show_thinking=True,
            )
        output = stdout_capture.getvalue()
        self.assertIn("reasoning", output)
        self.assertIn("Before", output)
        self.assertIn("After", output)

    def test_no_thinking_unaffected(self):
        """Responses without thinking tags are unaffected by show_thinking."""
        tokens = ["Hello", " ", "World"]
        client = self._make_client(tokens)
        stdout_capture = StringIO()
        with patch("sys.stdout", stdout_capture):
            response = chat_stream(
                client=client,
                model="test-model",
                messages=[{"role": "user", "content": "test"}],
                show_thinking=False,
            )
        output = stdout_capture.getvalue()
        self.assertIn("Hello World", output)
        self.assertEqual(response, "Hello World")

    def test_thinking_preserved_in_return_value(self):
        """Full response including thinking is always returned (for session history)."""
        tokens = ["A<think>thinking</think>B"]
        client = self._make_client(tokens)
        stdout_capture = StringIO()
        with patch("sys.stdout", stdout_capture):
            response = chat_stream(
                client=client,
                model="test-model",
                messages=[{"role": "user", "content": "test"}],
                show_thinking=False,
            )
        self.assertIn("<think>thinking</think>", response)


# ---------------------------------------------------------------------------
# agent_manager.strip_thinking_tags: existing behavior unchanged
# ---------------------------------------------------------------------------


class TestAgentManagerStripThinkingTags(unittest.TestCase):
    """Verify that agent_manager's strip_thinking_tags still works as before."""

    def setUp(self):
        import agent_manager

        self.mgr = agent_manager.SessionManager.__new__(agent_manager.SessionManager)

    def test_strips_complete_block(self):
        result = self.mgr.strip_thinking_tags("Hello<think>reasoning</think>World")
        self.assertNotIn("<think>", result)
        self.assertNotIn("reasoning", result)
        self.assertIn("Hello", result)
        self.assertIn("World", result)

    def test_strips_unclosed_block(self):
        result = self.mgr.strip_thinking_tags("Before<think>trailing")
        self.assertEqual(result, "Before")

    def test_no_op_without_tags(self):
        text = "Normal response text."
        result = self.mgr.strip_thinking_tags(text)
        self.assertEqual(result, text)

    def test_strips_multiline(self):
        result = self.mgr.strip_thinking_tags("Start<think>\nline1\nline2\n</think>End")
        self.assertEqual(result, "Start" + "End".strip())


if __name__ == "__main__":
    unittest.main()
