"""Tests for the Sigil client."""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest

from sigil import SigilClient, SigilAuthError, SigilError, SigilNotFoundError


MOCK_PERSONA_RESPONSE = {
    "name": "staccato",
    "version": "1.0.0",
    "persona": {
        "identity": {
            "name": "staccato",
            "version": "1.0.0",
            "description": "A concise assistant",
        },
        "soul": "You are a concise, helpful assistant.",
        "voice": {
            "tone": ["direct", "clear"],
            "dos": ["Be brief", "Use examples"],
            "donts": ["Ramble", "Over-explain"],
            "example_phrases": ["Here's the key point:", "In short:"],
        },
        "values": [
            {"name": "Clarity", "description": "Clear communication", "priority": 10},
            {"name": "Brevity", "description": "Say more with less", "priority": 8},
        ],
        "behavioral_map": {
            "greeting": {
                "posture": "Warm but brief",
                "example": "Hello! How can I help?",
            }
        },
        "test_cases": [
            {
                "id": "tc1",
                "description": "Test brevity",
                "prompt": "Explain Python",
                "expected_behavior": "Brief explanation",
            }
        ],
    },
}


class TestSigilClientInit:
    """Test client initialization."""

    def test_init_with_api_key(self) -> None:
        client = SigilClient(api_key="sk_test")
        assert client.api_key == "sk_test"
        assert client.base_url == "http://localhost:8100"
        assert client.timeout == (10, 30)  # (connect, read) default
        assert client.retry_count == 3
        assert client.retry_backoff == 0.5

    def test_init_with_custom_base_url(self) -> None:
        client = SigilClient(api_key="sk_test", base_url="https://api.example.com/")
        assert client.base_url == "https://api.example.com"  # trailing slash stripped

    def test_init_with_custom_timeout(self) -> None:
        client = SigilClient(api_key="sk_test", timeout=30)
        assert client.timeout == 30

    def test_init_with_custom_timeout_tuple(self) -> None:
        client = SigilClient(api_key="sk_test", timeout=(10, 60))
        assert client.timeout == (10, 60)

    def test_init_with_custom_retry_settings(self) -> None:
        client = SigilClient(api_key="sk_test", retry_count=5, retry_backoff=1.0)
        assert client.retry_count == 5
        assert client.retry_backoff == 1.0

    def test_init_from_env_vars(self) -> None:
        with patch.dict(
            "os.environ",
            {"SIGIL_API_KEY": "sk_env", "SIGIL_BASE_URL": "https://env.example.com"},
        ):
            client = SigilClient()
            assert client.api_key == "sk_env"
            assert client.base_url == "https://env.example.com"

    def test_init_without_api_key_raises(self) -> None:
        with patch.dict("os.environ", {}, clear=True):
            with pytest.raises(SigilError, match="API key required"):
                SigilClient()


class TestSigilClientGet:
    """Test the get() method."""

    @patch("sigil.client.requests.Session")
    def test_get_latest_persona(self, mock_session_class: MagicMock) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.status_code = 200
        mock_response.json.return_value = MOCK_PERSONA_RESPONSE
        mock_session.request.return_value = mock_response

        client = SigilClient(api_key="sk_test")
        persona = client.get("staccato")

        assert persona.name == "staccato"
        assert persona.version == "1.0.0"
        assert persona.soul == "You are a concise, helpful assistant."

        mock_session.request.assert_called_once()
        call_args = mock_session.request.call_args
        assert call_args[0] == ("GET", "http://localhost:8100/v1/personas/staccato")

    @patch("sigil.client.requests.Session")
    def test_get_specific_version(self, mock_session_class: MagicMock) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.status_code = 200
        mock_response.json.return_value = MOCK_PERSONA_RESPONSE
        mock_session.request.return_value = mock_response

        client = SigilClient(api_key="sk_test")
        persona = client.get("staccato", version="1.0.0")

        call_args = mock_session.request.call_args
        assert call_args[0] == (
            "GET",
            "http://localhost:8100/v1/personas/staccato/1.0.0",
        )

    @patch("sigil.client.requests.Session")
    def test_get_not_found_raises(self, mock_session_class: MagicMock) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        mock_response = MagicMock()
        mock_response.ok = False
        mock_response.status_code = 404
        mock_session.request.return_value = mock_response

        client = SigilClient(api_key="sk_test")
        with pytest.raises(SigilNotFoundError):
            client.get("nonexistent")

    @patch("sigil.client.requests.Session")
    def test_get_auth_error_raises(self, mock_session_class: MagicMock) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        mock_response = MagicMock()
        mock_response.ok = False
        mock_response.status_code = 401
        mock_session.request.return_value = mock_response

        client = SigilClient(api_key="sk_bad")
        with pytest.raises(SigilAuthError):
            client.get("staccato")

    @patch("sigil.client.requests.Session")
    def test_get_server_error_raises(self, mock_session_class: MagicMock) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        mock_response = MagicMock()
        mock_response.ok = False
        mock_response.status_code = 500
        mock_response.json.return_value = {"detail": "Internal server error"}
        mock_session.request.return_value = mock_response

        client = SigilClient(api_key="sk_test")
        with pytest.raises(SigilError) as exc_info:
            client.get("staccato")
        assert exc_info.value.status_code == 500


class TestSigilClientRetry:
    """Test retry logic with exponential backoff."""

    @patch("sigil.client.time.sleep")
    @patch("sigil.client.requests.Session")
    def test_retry_on_connection_error(
        self, mock_session_class: MagicMock, mock_sleep: MagicMock
    ) -> None:
        import requests as req

        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        # Fail twice, succeed on third attempt
        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.status_code = 200
        mock_response.json.return_value = MOCK_PERSONA_RESPONSE

        mock_session.request.side_effect = [
            req.exceptions.ConnectionError("Connection refused"),
            req.exceptions.ConnectionError("Connection refused"),
            mock_response,
        ]

        client = SigilClient(api_key="sk_test", retry_backoff=0.1)
        persona = client.get("staccato")

        assert persona.name == "staccato"
        assert mock_session.request.call_count == 3
        assert mock_sleep.call_count == 2

    @patch("sigil.client.time.sleep")
    @patch("sigil.client.requests.Session")
    def test_retry_on_timeout(
        self, mock_session_class: MagicMock, mock_sleep: MagicMock
    ) -> None:
        import requests as req

        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        # Fail once with timeout, succeed on second attempt
        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.status_code = 200
        mock_response.json.return_value = MOCK_PERSONA_RESPONSE

        mock_session.request.side_effect = [
            req.exceptions.Timeout("Request timed out"),
            mock_response,
        ]

        client = SigilClient(api_key="sk_test", retry_backoff=0.1)
        persona = client.get("staccato")

        assert persona.name == "staccato"
        assert mock_session.request.call_count == 2
        assert mock_sleep.call_count == 1

    @patch("sigil.client.time.sleep")
    @patch("sigil.client.requests.Session")
    def test_retry_on_503(
        self, mock_session_class: MagicMock, mock_sleep: MagicMock
    ) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        # 503 twice, then success
        mock_503 = MagicMock()
        mock_503.ok = False
        mock_503.status_code = 503

        mock_success = MagicMock()
        mock_success.ok = True
        mock_success.status_code = 200
        mock_success.json.return_value = MOCK_PERSONA_RESPONSE

        mock_session.request.side_effect = [mock_503, mock_503, mock_success]

        client = SigilClient(api_key="sk_test", retry_backoff=0.1)
        persona = client.get("staccato")

        assert persona.name == "staccato"
        assert mock_session.request.call_count == 3
        assert mock_sleep.call_count == 2

    @patch("sigil.client.time.sleep")
    @patch("sigil.client.requests.Session")
    def test_retry_on_429(
        self, mock_session_class: MagicMock, mock_sleep: MagicMock
    ) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        # 429 once, then success
        mock_429 = MagicMock()
        mock_429.ok = False
        mock_429.status_code = 429

        mock_success = MagicMock()
        mock_success.ok = True
        mock_success.status_code = 200
        mock_success.json.return_value = MOCK_PERSONA_RESPONSE

        mock_session.request.side_effect = [mock_429, mock_success]

        client = SigilClient(api_key="sk_test", retry_backoff=0.1)
        persona = client.get("staccato")

        assert persona.name == "staccato"
        assert mock_session.request.call_count == 2

    @patch("sigil.client.requests.Session")
    def test_no_retry_on_400(self, mock_session_class: MagicMock) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        # 400 should not retry
        mock_response = MagicMock()
        mock_response.ok = False
        mock_response.status_code = 400
        mock_response.json.return_value = {"detail": "Bad request"}
        mock_session.request.return_value = mock_response

        client = SigilClient(api_key="sk_test")
        with pytest.raises(SigilError):
            client.get("staccato")

        # Should only be called once (no retries)
        assert mock_session.request.call_count == 1

    @patch("sigil.client.requests.Session")
    def test_no_retry_on_500(self, mock_session_class: MagicMock) -> None:
        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        # 500 should not retry (not transient)
        mock_response = MagicMock()
        mock_response.ok = False
        mock_response.status_code = 500
        mock_response.json.return_value = {"detail": "Internal server error"}
        mock_session.request.return_value = mock_response

        client = SigilClient(api_key="sk_test")
        with pytest.raises(SigilError):
            client.get("staccato")

        # Should only be called once (no retries)
        assert mock_session.request.call_count == 1

    @patch("sigil.client.time.sleep")
    @patch("sigil.client.requests.Session")
    def test_exhausted_retries_raises(
        self, mock_session_class: MagicMock, mock_sleep: MagicMock
    ) -> None:
        import requests as req

        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        # Always fail with connection error
        mock_session.request.side_effect = req.exceptions.ConnectionError("Connection refused")

        client = SigilClient(api_key="sk_test", retry_count=2, retry_backoff=0.1)
        with pytest.raises(SigilError, match="Connection refused"):
            client.get("staccato")

        # Initial + 2 retries = 3 attempts
        assert mock_session.request.call_count == 3
        assert mock_sleep.call_count == 2

    @patch("sigil.client.time.sleep")
    @patch("sigil.client.requests.Session")
    def test_no_retries_when_retry_count_zero(
        self, mock_session_class: MagicMock, mock_sleep: MagicMock
    ) -> None:
        import requests as req

        mock_session = MagicMock()
        mock_session_class.return_value = mock_session

        mock_session.request.side_effect = req.exceptions.ConnectionError("Connection refused")

        client = SigilClient(api_key="sk_test", retry_count=0)
        with pytest.raises(SigilError):
            client.get("staccato")

        assert mock_session.request.call_count == 1
        assert mock_sleep.call_count == 0
