"""Tests for Simons AI Assistant client."""
from __future__ import annotations

import json
from typing import Any, Dict, Iterator
from unittest.mock import MagicMock, patch

import pytest

from cpz.simons import SimonsClient, AsyncSimonsClient
from cpz.simons_models import (
    CodeProposal,
    SimonsChunk,
    SimonsMemory,
    SimonsResponse,
    TokenUsage,
)


class MockCPZClient:
    """Mock CPZ client for testing."""
    api_key = "cpz_key_test123456789012345678"
    secret_key = "cpz_secret_test12345678901234567890123456789012"


@pytest.fixture
def mock_cpz_client():
    """Create a mock CPZ client."""
    return MockCPZClient()


@pytest.fixture
def simons_client(mock_cpz_client):
    """Create a Simons client with mock CPZ client."""
    return SimonsClient(mock_cpz_client)


class TestTokenUsage:
    """Tests for TokenUsage dataclass."""

    def test_from_dict_standard(self):
        data = {"input_tokens": 100, "output_tokens": 200, "total_tokens": 300}
        usage = TokenUsage.from_dict(data)
        assert usage.input_tokens == 100
        assert usage.output_tokens == 200
        assert usage.total_tokens == 300

    def test_from_dict_openai_format(self):
        data = {"prompt_tokens": 50, "completion_tokens": 100, "total_tokens": 150}
        usage = TokenUsage.from_dict(data)
        assert usage.input_tokens == 50
        assert usage.output_tokens == 100
        assert usage.total_tokens == 150

    def test_from_dict_empty(self):
        usage = TokenUsage.from_dict({})
        assert usage.input_tokens == 0
        assert usage.output_tokens == 0
        assert usage.total_tokens == 0


class TestCodeProposal:
    """Tests for CodeProposal dataclass."""

    def test_from_dict(self):
        data = {
            "file_path": "strategy.py",
            "diff": "--- a\n+++ b\n@@ ...",
            "description": "Add RSI calculation",
            "applied": False,
        }
        proposal = CodeProposal.from_dict(data)
        assert proposal.file_path == "strategy.py"
        assert proposal.diff == "--- a\n+++ b\n@@ ..."
        assert proposal.description == "Add RSI calculation"
        assert proposal.applied is False

    def test_from_dict_alternate_keys(self):
        data = {"path": "main.py", "content": "diff content"}
        proposal = CodeProposal.from_dict(data)
        assert proposal.file_path == "main.py"
        assert proposal.diff == "diff content"


class TestSimonsResponse:
    """Tests for SimonsResponse dataclass."""

    def test_from_dict_full(self):
        data = {
            "content": "Here's my analysis...",
            "model": "claude-sonnet-4-5-20250929",
            "usage": {"input_tokens": 100, "output_tokens": 500, "total_tokens": 600},
            "cost": 0.0123,
            "thinking": "Let me think about this...",
            "proposals": [
                {"file_path": "test.py", "diff": "diff", "description": "change"}
            ],
        }
        response = SimonsResponse.from_dict(data)
        assert response.content == "Here's my analysis..."
        assert response.model == "claude-sonnet-4-5-20250929"
        assert response.tokens_used.total_tokens == 600
        assert response.cost == 0.0123
        assert response.thinking == "Let me think about this..."
        assert len(response.proposals) == 1
        assert response.proposals[0].file_path == "test.py"

    def test_from_dict_minimal(self):
        data = {"response": "Simple response"}
        response = SimonsResponse.from_dict(data)
        assert response.content == "Simple response"
        assert response.model == ""
        assert response.proposals is None

    def test_from_dict_alternate_content_keys(self):
        # Test 'message' key
        data = {"message": "From message key"}
        assert SimonsResponse.from_dict(data).content == "From message key"

        # Test 'text' key
        data = {"text": "From text key"}
        assert SimonsResponse.from_dict(data).content == "From text key"


class TestSimonsChunk:
    """Tests for SimonsChunk dataclass."""

    def test_from_sse_text(self):
        chunk = SimonsChunk.from_sse("text", "Hello")
        assert chunk.type == "text"
        assert chunk.content == "Hello"
        assert chunk.delta == "Hello"

    def test_from_sse_done(self):
        chunk = SimonsChunk.from_sse("done", "[DONE]")
        assert chunk.type == "done"
        assert chunk.delta == ""

    def test_from_sse_thinking(self):
        chunk = SimonsChunk.from_sse("thinking", "Analyzing...")
        assert chunk.type == "thinking"
        assert chunk.content == "Analyzing..."

    def test_from_sse_error(self):
        chunk = SimonsChunk.from_sse("error", "Something went wrong")
        assert chunk.type == "error"


class TestSimonsMemory:
    """Tests for SimonsMemory dataclass."""

    def test_from_dict(self):
        data = {
            "user_id": "user123",
            "preferences": {"risk_profile": "moderate"},
            "facts": ["Prefers momentum strategies"],
            "created_at": "2025-01-01T00:00:00Z",
            "updated_at": "2025-01-15T00:00:00Z",
        }
        memory = SimonsMemory.from_dict(data)
        assert memory.user_id == "user123"
        assert memory.preferences == {"risk_profile": "moderate"}
        assert memory.facts == ["Prefers momentum strategies"]

    def test_to_dict(self):
        memory = SimonsMemory(
            user_id="user456",
            preferences={"style": "swing"},
            facts=["Uses daily timeframe"],
        )
        result = memory.to_dict()
        assert result["user_id"] == "user456"
        assert result["preferences"]["style"] == "swing"
        assert "Uses daily timeframe" in result["facts"]


class TestSimonsClient:
    """Tests for SimonsClient."""

    def test_init(self, simons_client):
        assert simons_client._client.api_key == "cpz_key_test123456789012345678"

    def test_get_headers(self, simons_client):
        headers = simons_client._get_headers()
        assert headers["X-CPZ-Key"] == "cpz_key_test123456789012345678"
        assert headers["X-CPZ-Secret"] == "cpz_secret_test12345678901234567890123456789012"
        assert headers["Content-Type"] == "application/json"

    def test_get_stream_headers(self, simons_client):
        headers = simons_client._get_stream_headers()
        assert headers["Accept"] == "text/event-stream"

    @patch("cpz.simons.requests.post")
    def test_chat_success(self, mock_post, simons_client):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "content": "AAPL shows strong momentum signals...",
            "model": "claude-sonnet-4-5-20250929",
            "usage": {"input_tokens": 50, "output_tokens": 200},
            "cost": 0.005,
        }
        mock_response.raise_for_status = MagicMock()
        mock_post.return_value = mock_response

        response = simons_client.chat("Analyze AAPL")

        assert response.content == "AAPL shows strong momentum signals..."
        assert response.model == "claude-sonnet-4-5-20250929"
        assert response.cost == 0.005
        mock_post.assert_called_once()

    @patch("cpz.simons.requests.post")
    def test_chat_permission_error(self, mock_post, simons_client):
        mock_response = MagicMock()
        mock_response.status_code = 403
        mock_response.json.return_value = {
            "error": "Simons access not authorized",
            "message": "Enable simons scope",
        }
        mock_post.return_value = mock_response

        with pytest.raises(PermissionError) as exc_info:
            simons_client.chat("Hello")

        assert "simons" in str(exc_info.value).lower()

    @patch("cpz.simons.requests.post")
    def test_chat_with_strategy_id(self, mock_post, simons_client):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"content": "Strategy analysis..."}
        mock_response.raise_for_status = MagicMock()
        mock_post.return_value = mock_response

        simons_client.chat(
            "Analyze my strategy",
            strategy_id="strat-123",
            mode="agent",
            model="claude-opus-4-5-20251101",
        )

        call_args = mock_post.call_args
        payload = call_args.kwargs["json"]
        assert payload["strategy_id"] == "strat-123"
        assert payload["mode"] == "agent"
        assert payload["model"] == "claude-opus-4-5-20251101"

    @patch("cpz.simons.requests.post")
    def test_stream_success(self, mock_post, simons_client):
        # Simulate SSE response
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.raise_for_status = MagicMock()
        mock_response.iter_lines.return_value = iter([
            "event: text",
            "data: Hello",
            "",
            "event: text",
            "data: World",
            "",
            "data: [DONE]",
        ])
        mock_post.return_value = mock_response

        chunks = list(simons_client.stream("Test message"))

        assert len(chunks) == 3
        assert chunks[0].type == "text"
        assert chunks[0].content == "Hello"
        assert chunks[1].content == "World"
        assert chunks[2].type == "done"

    @patch("cpz.simons.requests.get")
    def test_get_memory_success(self, mock_get, simons_client):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "user_id": "user123",
            "preferences": {"risk": "low"},
            "facts": ["Fact 1"],
        }
        mock_response.raise_for_status = MagicMock()
        mock_get.return_value = mock_response

        memory = simons_client.get_memory()

        assert memory.user_id == "user123"
        assert memory.preferences["risk"] == "low"

    @patch("cpz.simons.requests.post")
    def test_update_memory_success(self, mock_post, simons_client):
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"success": True}
        mock_response.raise_for_status = MagicMock()
        mock_post.return_value = mock_response

        result = simons_client.update_memory(
            preferences={"risk_profile": "aggressive"},
            facts=["New fact"],
        )

        assert result is True
        call_args = mock_post.call_args
        payload = call_args.kwargs["json"]
        assert payload["preferences"]["risk_profile"] == "aggressive"
        assert "New fact" in payload["facts"]

    def test_update_memory_empty(self, simons_client):
        # Should return True without making request if nothing to update
        result = simons_client.update_memory()
        assert result is True


class TestAsyncSimonsClient:
    """Tests for AsyncSimonsClient."""

    def test_init(self, mock_cpz_client):
        client = AsyncSimonsClient(mock_cpz_client)
        assert client._client.api_key == "cpz_key_test123456789012345678"

    def test_get_headers(self, mock_cpz_client):
        client = AsyncSimonsClient(mock_cpz_client)
        headers = client._get_headers()
        assert headers["X-CPZ-Key"] == "cpz_key_test123456789012345678"
