"""Testes de propriedade para o módulo client.py.

Feature: pack-mcp-google-chat-notification
"""

import pytest
from hypothesis import given, settings, HealthCheck
from hypothesis import strategies as st

from pack_mcp_google_chat_notification.client import GoogleChatClient
from tests.strategies import valid_webhook_urls, invalid_webhook_urls


class TestSingletonIdentityProperty:
    """Feature: pack-mcp-google-chat-notification, Property 7: Identidade do singleton

    Valida: Requisitos 9.1, 9.2
    """

    @given(num_calls=st.integers(min_value=2, max_value=20))
    @settings(max_examples=100, deadline=None)
    def test_get_instance_always_returns_same_object(self, num_calls):
        """**Validates: Requirements 9.1, 9.2**

        Para qualquer sequência de chamadas a GoogleChatClient.get_instance(),
        todas devem retornar o mesmo objeto (mesma identidade id()).
        A primeira chamada cria a instância, e todas as subsequentes a reutilizam.
        """
        # Reset before each hypothesis example to ensure clean state
        GoogleChatClient.reset()

        first_instance = GoogleChatClient.get_instance()
        first_id = id(first_instance)

        for _ in range(num_calls - 1):
            instance = GoogleChatClient.get_instance()
            assert instance is first_instance, (
                f"get_instance() retornou objeto diferente: "
                f"id esperado={first_id}, id obtido={id(instance)}"
            )
            assert id(instance) == first_id


class TestWebhookUrlValidationProperty:
    """Feature: pack-mcp-google-chat-notification, Property 1: Validação de Webhook URL por prefixo

    Valida: Requisitos 4.1, 4.2
    """

    @given(url=valid_webhook_urls())
    @settings(max_examples=100, deadline=None)
    def test_valid_urls_are_accepted(self, url):
        """**Validates: Requirements 4.1, 4.2**

        Para qualquer URL que começa com https://chat.googleapis.com/,
        validate_webhook_url deve aceitar sem levantar exceção.
        """
        GoogleChatClient.reset()
        client = GoogleChatClient()
        # Should not raise
        client.validate_webhook_url(url)

    @given(url=invalid_webhook_urls())
    @settings(max_examples=100, deadline=None)
    def test_invalid_urls_are_rejected(self, url):
        """**Validates: Requirements 4.1, 4.2**

        Para qualquer URL que NÃO começa com https://chat.googleapis.com/,
        validate_webhook_url deve levantar ValueError com mensagem descritiva.
        """
        GoogleChatClient.reset()
        client = GoogleChatClient()
        with pytest.raises(ValueError, match="URL do webhook inválida"):
            client.validate_webhook_url(url)


class TestWebhookUrlFallbackProperty:
    """Feature: pack-mcp-google-chat-notification, Property 9: Fallback de webhook URL para variável de ambiente

    Valida: Requisitos 1.2, 8.1
    """

    @given(
        env_url=st.text(
            alphabet=st.characters(
                whitelist_categories=("L", "N"),
                whitelist_characters="-_/?.=&:",
            ),
            min_size=1,
            max_size=150,
        ).map(lambda s: "https://chat.googleapis.com/" + s)
    )
    @settings(
        max_examples=100,
        deadline=None,
        suppress_health_check=[HealthCheck.function_scoped_fixture],
    )
    def test_resolve_uses_env_var_when_no_param(self, env_url, monkeypatch):
        """**Validates: Requirements 1.2, 8.1**

        Para qualquer valor de GOOGLE_CHAT_WEBHOOK_URL definido como variável de ambiente,
        quando webhook_url não é fornecido como parâmetro, o client deve utilizar o valor
        da variável de ambiente. O valor resolvido deve ser idêntico ao da variável de ambiente.
        """
        monkeypatch.setenv("GOOGLE_CHAT_WEBHOOK_URL", env_url)
        GoogleChatClient.reset()
        client = GoogleChatClient.get_instance()

        resolved = client.resolve_webhook_url(None)

        assert resolved == env_url, (
            f"resolve_webhook_url(None) deveria retornar o valor da env var, "
            f"mas retornou '{resolved}' ao invés de '{env_url}'"
        )


from pack_mcp_google_chat_notification.client import build_text_payload
from tests.strategies import valid_texts


class TestTextPayloadSerializationProperty:
    """Feature: pack-mcp-google-chat-notification, Property 4: Serialização de payload de texto

    Valida: Requisitos 11.2, 1.1
    """

    @given(text=valid_texts())
    @settings(max_examples=100, deadline=None)
    def test_build_text_payload_produces_correct_structure(self, text):
        """**Validates: Requirements 11.2, 1.1**

        Para qualquer string de texto não vazia, build_text_payload(text) deve produzir
        exatamente {"text": text}, e o valor no payload deve ser idêntico ao texto original.
        """
        payload = build_text_payload(text)

        assert payload == {"text": text}, (
            f"build_text_payload deveria produzir exatamente {{'text': text}}, "
            f"mas produziu {payload}"
        )
        assert payload["text"] is text, (
            "O valor no payload deve ser idêntico (mesma referência) ao texto original"
        )


from pack_mcp_google_chat_notification.client import build_card_payload, parse_card_payload
from tests.strategies import valid_titles, valid_sections, card_payloads


class TestCardV2RoundTripProperty:
    """Feature: pack-mcp-google-chat-notification, Property 3: Round-trip de payload Card V2

    Valida: Requisitos 11.3
    """

    @given(data=card_payloads())
    @settings(max_examples=100, deadline=None)
    def test_build_then_parse_produces_equivalent_values(self, data):
        """**Validates: Requirements 11.3**

        Para qualquer combinação válida de title (não vazio), sections (lista não vazia de dicts)
        e subtitle (opcional), serializar com build_card_payload e depois deserializar com
        parse_card_payload deve produzir valores equivalentes aos originais.
        """
        title, sections, subtitle = data

        payload = build_card_payload(title, sections, subtitle=subtitle)
        parsed_title, parsed_sections, parsed_subtitle = parse_card_payload(payload)

        assert parsed_title == title, (
            f"Title round-trip falhou: esperado={title!r}, obtido={parsed_title!r}"
        )
        assert parsed_sections == sections, (
            f"Sections round-trip falhou: esperado={sections!r}, obtido={parsed_sections!r}"
        )
        assert parsed_subtitle == subtitle, (
            f"Subtitle round-trip falhou: esperado={subtitle!r}, obtido={parsed_subtitle!r}"
        )

class TestCardV2ConditionalSubtitleProperty:
    """Feature: pack-mcp-google-chat-notification, Property 5: Inclusão condicional de subtítulo no Card V2

    Valida: Requisitos 2.2, 2.3
    """

    @given(
        title=valid_titles(),
        sections=valid_sections(),
        subtitle=st.one_of(st.none(), st.text(min_size=1, max_size=200)),
    )
    @settings(max_examples=100, deadline=None)
    def test_subtitle_present_iff_provided(self, title, sections, subtitle):
        """**Validates: Requirements 2.2, 2.3**

        Para qualquer payload Card V2 válido, o subtítulo deve estar presente no header
        do card se e somente se foi fornecido como parâmetro. Quando fornecido, o valor
        no payload deve ser idêntico ao original. Quando omitido (None), a chave "subtitle"
        não deve existir no header.
        """
        payload = build_card_payload(title, sections, subtitle=subtitle)
        header = payload["cardsV2"][0]["card"]["header"]

        if subtitle is not None:
            assert "subtitle" in header, (
                f"subtitle foi fornecido ({subtitle!r}) mas não está presente no header"
            )
            assert header["subtitle"] == subtitle, (
                f"subtitle no header ({header['subtitle']!r}) difere do original ({subtitle!r})"
            )
        else:
            assert "subtitle" not in header, (
                f"subtitle não foi fornecido (None) mas a chave 'subtitle' existe no header: "
                f"{header!r}"
            )


from urllib.parse import urlparse, parse_qs, urlencode
from tests.strategies import valid_thread_keys


def _webhook_urls_with_query_params() -> st.SearchStrategy[str]:
    """Gera URLs de webhook válidas que sempre possuem query parameters (key e token)."""
    path = st.text(
        alphabet=st.characters(whitelist_categories=("L", "N"), whitelist_characters="-_/"),
        min_size=1,
        max_size=50,
    )
    param_value = st.text(
        alphabet=st.characters(whitelist_categories=("L", "N"), whitelist_characters="-_"),
        min_size=1,
        max_size=30,
    )

    @st.composite
    def build(draw):
        p = draw(path)
        key_val = draw(param_value)
        token_val = draw(param_value)
        query = urlencode({"key": key_val, "token": token_val})
        return f"https://chat.googleapis.com/v1/spaces/{p}/messages?{query}"

    return build()


class TestThreadUrlConstructionProperty:
    """Feature: pack-mcp-google-chat-notification, Property 6: Construção de URL com parâmetros de thread

    Valida: Requisitos 3.1
    """

    @given(
        webhook_url=_webhook_urls_with_query_params(),
        thread_key=valid_thread_keys(),
    )
    @settings(max_examples=100, deadline=None)
    def test_build_url_includes_thread_params_and_preserves_original(self, webhook_url, thread_key):
        """**Validates: Requirements 3.1**

        Para qualquer webhook URL válida e qualquer thread_key não vazio, a URL construída
        para envio em thread deve conter os query parameters threadKey=<thread_key> e
        messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD, preservando os parâmetros
        originais da URL.
        """
        GoogleChatClient.reset()
        client = GoogleChatClient()

        thread_params = {
            "threadKey": thread_key,
            "messageReplyOption": "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD",
        }

        result_url = client._build_url(webhook_url, thread_params)
        parsed = urlparse(result_url)
        result_params = parse_qs(parsed.query, keep_blank_values=True)

        # Deve conter threadKey com o valor correto
        assert "threadKey" in result_params, (
            f"threadKey não encontrado na URL construída: {result_url}"
        )
        assert result_params["threadKey"] == [thread_key], (
            f"threadKey esperado=[{thread_key!r}], obtido={result_params['threadKey']}"
        )

        # Deve conter messageReplyOption com o valor correto
        assert "messageReplyOption" in result_params, (
            f"messageReplyOption não encontrado na URL construída: {result_url}"
        )
        assert result_params["messageReplyOption"] == ["REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"], (
            f"messageReplyOption esperado=['REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD'], "
            f"obtido={result_params['messageReplyOption']}"
        )

        # Deve preservar os parâmetros originais da URL
        original_parsed = urlparse(webhook_url)
        original_params = parse_qs(original_parsed.query, keep_blank_values=True)
        for key, values in original_params.items():
            if key not in ("threadKey", "messageReplyOption"):
                assert key in result_params, (
                    f"Parâmetro original '{key}' não preservado na URL construída"
                )
                assert result_params[key] == values, (
                    f"Valor do parâmetro original '{key}' alterado: "
                    f"esperado={values}, obtido={result_params[key]}"
                )


from unittest.mock import AsyncMock, patch, MagicMock
import httpx


class TestHttpResponseHandlingProperty:
    """Feature: pack-mcp-google-chat-notification, Property 10: Tratamento de respostas HTTP

    Valida: Requisitos 5.4, 5.5
    """

    @given(
        status_code=st.integers(min_value=200, max_value=200),
        response_body=st.text(min_size=1, max_size=200),
    )
    @settings(max_examples=100, deadline=None)
    @pytest.mark.asyncio
    async def test_status_200_returns_success(self, status_code, response_body):
        """**Validates: Requirements 5.4**

        Para qualquer resposta HTTP com status code 200, o client deve retornar
        o body da resposta como dict (sucesso).
        """
        GoogleChatClient.reset()
        client = GoogleChatClient()

        mock_response = MagicMock()
        mock_response.status_code = status_code
        mock_response.json.return_value = {"text": response_body}
        mock_response.text = response_body

        with patch.object(client._http_client, "post", new_callable=AsyncMock) as mock_post:
            mock_post.return_value = mock_response

            result = await client.post(
                "https://chat.googleapis.com/v1/spaces/test/messages",
                {"text": "hello"},
            )

            assert result == {"text": response_body}, (
                f"Para status 200, post() deveria retornar o body da resposta, "
                f"mas retornou {result}"
            )

    @given(
        status_code=st.integers(min_value=100, max_value=599).filter(lambda x: x != 200),
        response_body=st.text(min_size=1, max_size=200),
    )
    @settings(max_examples=100, deadline=None)
    @pytest.mark.asyncio
    async def test_non_200_raises_error_with_status_and_body(self, status_code, response_body):
        """**Validates: Requirements 5.4, 5.5**

        Para qualquer status code diferente de 200, o client deve levantar ValueError
        contendo o status code e o body da resposta.
        """
        GoogleChatClient.reset()
        client = GoogleChatClient()

        mock_response = MagicMock()
        mock_response.status_code = status_code
        mock_response.text = response_body

        with patch.object(client._http_client, "post", new_callable=AsyncMock) as mock_post:
            mock_post.return_value = mock_response

            with pytest.raises(ValueError) as exc_info:
                await client.post(
                    "https://chat.googleapis.com/v1/spaces/test/messages",
                    {"text": "hello"},
                )

            error_message = str(exc_info.value)
            assert str(status_code) in error_message, (
                f"Erro deveria conter o status code {status_code}, "
                f"mas a mensagem foi: {error_message!r}"
            )
            assert response_body in error_message, (
                f"Erro deveria conter o body da resposta {response_body!r}, "
                f"mas a mensagem foi: {error_message!r}"
            )
