"""
Tests for Chat API improvements:
1. History Immutability
2. Customizable continue strategy
3. chat() vs chat.complete() distinction
4. Removed continue_if_needed() methods
"""

from unittest.mock import Mock, patch

import pytest

from lexilux import Chat, ChatContinue, ChatHistory
from lexilux.chat.exceptions import ChatIncompleteResponseError
from lexilux.chat.models import ChatResult
from lexilux.exceptions import ServerError
from lexilux.usage import Usage


class TestHistoryImmutability:
    """Test that history parameters are immutable."""

    def test_chat_call_history_immutability(self, mock_post):
        """Test that chat() does not modify original history."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Create original history
        original_history = ChatHistory()
        original_history.add_user("Previous message")
        original_messages_count = len(original_history.messages)

        # Mock response
        mock_post.return_value.json.return_value = {
            "choices": [{"message": {"content": "Hello"}, "finish_reason": "stop"}],
            "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
        }

        # Call chat with history
        result = chat("New message", history=original_history)

        # Verify original history is not modified
        assert len(original_history.messages) == original_messages_count
        assert original_history.messages == [
            {"role": "user", "content": "Previous message"}
        ]

        # Verify result is correct
        assert result.text == "Hello"
        assert result.finish_reason == "stop"

    def test_chat_stream_history_immutability(self, mock_post):
        """Test that chat.stream() does not modify original history."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Create original history
        original_history = ChatHistory()
        original_history.add_user("Previous message")
        original_messages_count = len(original_history.messages)

        # Mock streaming response
        mock_post.return_value.iter_lines.return_value = [
            b'data: {"choices": [{"delta": {"content": "Hello"}, "finish_reason": null}]}',
            b'data: {"choices": [{"delta": {"content": " world"}, "finish_reason": "stop"}]}',
            b"data: [DONE]",
        ]

        # Call stream with history
        iterator = chat.stream("New message", history=original_history)
        chunks = list(iterator)

        # Verify original history is not modified
        assert len(original_history.messages) == original_messages_count
        assert original_history.messages == [
            {"role": "user", "content": "Previous message"}
        ]

        # Verify chunks
        assert len(chunks) >= 2

    def test_complete_history_immutability(self, mock_post):
        """Test that chat.complete() does not modify original history."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Create original history
        original_history = ChatHistory()
        original_history.add_user("Previous message")
        original_messages_count = len(original_history.messages)

        # Mock responses (initial + continue)
        responses = [
            {
                "choices": [
                    {"message": {"content": "Partial"}, "finish_reason": "length"}
                ],
                "usage": {
                    "prompt_tokens": 10,
                    "completion_tokens": 5,
                    "total_tokens": 15,
                },
            },
            {
                "choices": [
                    {"message": {"content": " complete"}, "finish_reason": "stop"}
                ],
                "usage": {
                    "prompt_tokens": 15,
                    "completion_tokens": 10,
                    "total_tokens": 25,
                },
            },
        ]
        mock_post.side_effect = [
            Mock(json=lambda: responses[0], raise_for_status=lambda: None),
            Mock(json=lambda: responses[1], raise_for_status=lambda: None),
        ]

        # Call complete with history
        result = chat.complete("New message", history=original_history, max_tokens=10)

        # Verify original history is not modified
        assert len(original_history.messages) == original_messages_count
        assert original_history.messages == [
            {"role": "user", "content": "Previous message"}
        ]

        # Verify result is merged
        assert "Partial" in result.text
        assert "complete" in result.text


class TestChatVsComplete:
    """Test distinction between chat() and chat.complete()."""

    def test_chat_accepts_truncation(self, mock_post):
        """Test that chat() accepts truncated responses."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Mock truncated response
        mock_post.return_value.json.return_value = {
            "choices": [
                {"message": {"content": "Partial response"}, "finish_reason": "length"}
            ],
            "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
        }

        result = chat("Write a long story", max_tokens=10)

        # chat() should return truncated result without error
        assert result.finish_reason == "length"
        assert result.text == "Partial response"

    def test_complete_handles_truncation(self, mock_post):
        """Test that chat.complete() automatically handles truncation."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Mock responses (initial truncated + continue complete)
        responses = [
            {
                "choices": [
                    {"message": {"content": "Partial"}, "finish_reason": "length"}
                ],
                "usage": {
                    "prompt_tokens": 10,
                    "completion_tokens": 5,
                    "total_tokens": 15,
                },
            },
            {
                "choices": [
                    {"message": {"content": " complete"}, "finish_reason": "stop"}
                ],
                "usage": {
                    "prompt_tokens": 15,
                    "completion_tokens": 10,
                    "total_tokens": 25,
                },
            },
        ]
        mock_post.side_effect = [
            Mock(json=lambda: responses[0], raise_for_status=lambda: None),
            Mock(json=lambda: responses[1], raise_for_status=lambda: None),
        ]

        result = chat.complete("Write a long story", max_tokens=10)

        # complete() should return complete result
        assert result.finish_reason == "stop"
        assert "Partial" in result.text
        assert "complete" in result.text

    def test_complete_without_history(self, mock_post):
        """Test that chat.complete() works without explicit history."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Mock complete response (no truncation)
        mock_post.return_value.json.return_value = {
            "choices": [
                {"message": {"content": "Complete response"}, "finish_reason": "stop"}
            ],
            "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
        }

        # Should work without history parameter
        result = chat.complete("Write a story", max_tokens=100)

        assert result.finish_reason == "stop"
        assert result.text == "Complete response"

    def test_complete_raises_if_still_truncated(self, mock_post):
        """Test that chat.complete() raises error if still truncated after max_continues."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Mock responses (all truncated)
        truncated_response = {
            "choices": [{"message": {"content": "Partial"}, "finish_reason": "length"}],
            "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
        }
        mock_post.return_value.json.return_value = truncated_response
        mock_post.return_value.raise_for_status = lambda: None

        # Should raise error if still truncated
        with pytest.raises(ChatIncompleteResponseError):
            chat.complete(
                "Write a very long story",
                max_tokens=5,
                max_continues=2,
                ensure_complete=True,
            )


class TestCustomizableContinueStrategy:
    """Test customizable continue strategy features."""

    def test_custom_continue_prompt_function(self, mock_post):
        """Test that continue_prompt can be a function."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")
        history = ChatHistory()

        # Mock responses
        responses = [
            {
                "choices": [
                    {"message": {"content": "Partial"}, "finish_reason": "length"}
                ],
                "usage": {
                    "prompt_tokens": 10,
                    "completion_tokens": 5,
                    "total_tokens": 15,
                },
            },
            {
                "choices": [
                    {"message": {"content": " complete"}, "finish_reason": "stop"}
                ],
                "usage": {
                    "prompt_tokens": 15,
                    "completion_tokens": 10,
                    "total_tokens": 25,
                },
            },
        ]
        mock_post.side_effect = [
            Mock(json=lambda: responses[0], raise_for_status=lambda: None),
            Mock(json=lambda: responses[1], raise_for_status=lambda: None),
        ]

        # Custom prompt function
        prompt_calls = []

        def custom_prompt(count, max_count, current_text, original_prompt):
            prompt_calls.append((count, max_count, current_text, original_prompt))
            return f"Continue {count}/{max_count}"

        chat.complete(
            "Write story",
            history=history,
            max_tokens=10,
            continue_prompt=custom_prompt,
        )

        # Verify custom prompt was called
        assert len(prompt_calls) > 0
        assert prompt_calls[0][0] == 1  # First continue
        assert "Write story" in prompt_calls[0][3]  # Original prompt

    def test_progress_callback(self, mock_post):
        """Test that on_progress callback is called."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")
        history = ChatHistory()

        # Mock responses
        responses = [
            {
                "choices": [
                    {"message": {"content": "Partial"}, "finish_reason": "length"}
                ],
                "usage": {
                    "prompt_tokens": 10,
                    "completion_tokens": 5,
                    "total_tokens": 15,
                },
            },
            {
                "choices": [
                    {"message": {"content": " complete"}, "finish_reason": "stop"}
                ],
                "usage": {
                    "prompt_tokens": 15,
                    "completion_tokens": 10,
                    "total_tokens": 25,
                },
            },
        ]
        mock_post.side_effect = [
            Mock(json=lambda: responses[0], raise_for_status=lambda: None),
            Mock(json=lambda: responses[1], raise_for_status=lambda: None),
        ]

        # Progress callback
        progress_calls = []

        def on_progress(count, max_count, current, all_results):
            progress_calls.append(
                (count, max_count, len(current.text), len(all_results))
            )

        chat.complete(
            "Write story",
            history=history,
            max_tokens=10,
            on_progress=on_progress,
        )

        # Verify progress callback was called
        assert len(progress_calls) > 0
        assert progress_calls[0][0] == 1  # First continue
        assert progress_calls[0][1] >= 1  # max_count

    def test_continue_delay(self, mock_post):
        """Test that continue_delay is applied."""
        import time

        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")
        history = ChatHistory()

        # Track call times
        call_times = []

        def track_time(*args, **kwargs):
            call_times.append(time.time())
            # Return appropriate response based on call count
            if len(call_times) == 1:
                # Initial request - truncated
                return Mock(
                    json=lambda: {
                        "choices": [
                            {
                                "message": {"content": "Partial1"},
                                "finish_reason": "length",
                            }
                        ],
                        "usage": {
                            "prompt_tokens": 10,
                            "completion_tokens": 5,
                            "total_tokens": 15,
                        },
                    },
                    raise_for_status=lambda: None,
                )
            elif len(call_times) == 2:
                # First continue - truncated
                return Mock(
                    json=lambda: {
                        "choices": [
                            {
                                "message": {"content": "Partial2"},
                                "finish_reason": "length",
                            }
                        ],
                        "usage": {
                            "prompt_tokens": 15,
                            "completion_tokens": 10,
                            "total_tokens": 25,
                        },
                    },
                    raise_for_status=lambda: None,
                )
            else:
                # Second continue - complete
                return Mock(
                    json=lambda: {
                        "choices": [
                            {
                                "message": {"content": " complete"},
                                "finish_reason": "stop",
                            }
                        ],
                        "usage": {
                            "prompt_tokens": 20,
                            "completion_tokens": 15,
                            "total_tokens": 35,
                        },
                    },
                    raise_for_status=lambda: None,
                )

        mock_post.side_effect = track_time

        # Test with fixed delay
        result = chat.complete(
            "Write story",
            history=history,
            max_tokens=10,
            max_continues=3,
            continue_delay=0.05,  # 50ms delay
        )

        # Verify delay was applied between continues (call 2 and 3 should have delay)
        if len(call_times) >= 3:
            delay_between_continues = call_times[2] - call_times[1]
            assert delay_between_continues >= 0.05, (
                f"Expected delay >= 0.05s, got {delay_between_continues}"
            )

        assert result.finish_reason == "stop"

    def test_error_handling_return_partial(self, mock_post):
        """Test that on_error='return_partial' returns partial result on error."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")
        history = ChatHistory()

        call_count = [0]

        def mock_response(*args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                # Initial request - truncated
                return Mock(
                    json=lambda: {
                        "choices": [
                            {
                                "message": {"content": "Partial"},
                                "finish_reason": "length",
                            }
                        ],
                        "usage": {
                            "prompt_tokens": 10,
                            "completion_tokens": 5,
                            "total_tokens": 15,
                        },
                    },
                    raise_for_status=lambda: None,
                )
            else:
                # Continue request fails - use LexiluxError to match production behavior
                raise ServerError("Network error")

        mock_post.side_effect = mock_response

        # Should return partial result instead of raising
        result = chat.complete(
            "Write story",
            history=history,
            max_tokens=10,
            max_continues=1,
            ensure_complete=False,  # Don't raise if still truncated
            on_error="return_partial",
        )

        # Should return partial result (may still be truncated if error occurred)
        assert "Partial" in result.text


class TestContinueRequestCustomization:
    """Test ChatContinue.continue_request() customization features."""

    def test_continue_request_with_custom_prompt(self, mock_post):
        """Test continue_request with custom prompt function."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Mock initial result
        initial_result = ChatResult(
            text="Partial",
            usage=Usage(input_tokens=10, output_tokens=5, total_tokens=15),
            finish_reason="length",
        )

        # Build messages list (new API uses plain messages, not ChatHistory)
        messages = [
            {"role": "user", "content": "Original prompt"},
            {"role": "assistant", "content": initial_result.text},
        ]

        # Mock continue response
        mock_post.return_value.json.return_value = {
            "choices": [{"message": {"content": " complete"}, "finish_reason": "stop"}],
            "usage": {"prompt_tokens": 15, "completion_tokens": 10, "total_tokens": 25},
        }
        mock_post.return_value.raise_for_status = lambda: None

        # Custom prompt function
        prompt_calls = []

        def custom_prompt(count, max_count, current_text, original_prompt):
            prompt_calls.append((count, max_count))
            return f"Continue {count}/{max_count}"

        result = ChatContinue.continue_request(
            chat,
            initial_result,
            messages=messages,
            continue_prompt=custom_prompt,
        )

        # Verify custom prompt was called
        assert len(prompt_calls) > 0
        assert result.finish_reason == "stop"

    def test_continue_request_messages_modified_in_place(self, mock_post):
        """Test that continue_request modifies the provided messages list in place."""
        chat = Chat(base_url="https://api.test.com/v1", api_key="test", model="test")

        # Mock initial result
        initial_result = ChatResult(
            text="Partial",
            usage=Usage(input_tokens=10, output_tokens=5, total_tokens=15),
            finish_reason="length",
        )

        # Build messages list (new API modifies list in place)
        messages = [
            {"role": "user", "content": "Original message"},
            {"role": "assistant", "content": initial_result.text},
        ]
        original_messages_count = len(messages)

        # Mock continue response
        mock_post.return_value.json.return_value = {
            "choices": [{"message": {"content": " complete"}, "finish_reason": "stop"}],
            "usage": {"prompt_tokens": 15, "completion_tokens": 10, "total_tokens": 25},
        }
        mock_post.return_value.raise_for_status = lambda: None

        ChatContinue.continue_request(
            chat,
            initial_result,
            messages=messages,
        )

        # Messages list is modified in place (has new continue prompt + assistant response)
        assert len(messages) > original_messages_count


class TestNeedsContinue:
    """Test ChatContinue.needs_continue() helper method."""

    def test_needs_continue_true(self):
        """Test needs_continue returns True for length finish_reason."""
        result = ChatResult(
            text="Partial",
            usage=Usage(),
            finish_reason="length",
        )
        assert ChatContinue.needs_continue(result) is True

    def test_needs_continue_false(self):
        """Test needs_continue returns False for non-length finish_reason."""
        result = ChatResult(
            text="Complete",
            usage=Usage(),
            finish_reason="stop",
        )
        assert ChatContinue.needs_continue(result) is False


@pytest.fixture
def mock_post():
    """Mock requests.Session.post for testing via BaseAPIClient with connection pooling."""
    with patch("requests.Session.post") as mock:
        yield mock
