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

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

import asyncio
from urllib.parse import urlparse, parse_qs

import pytest
from hypothesis import given, settings
from hypothesis import strategies as st
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData

from pack_mcp_google_chat_notification.errors import handle_error, sanitize_url
from tests.strategies import webhook_urls_with_tokens


def _run(coro):
    """Helper para executar coroutines em testes síncronos."""
    return asyncio.get_event_loop().run_until_complete(coro)


class TestSanitizeUrlProperty:
    """Feature: pack-mcp-google-chat-notification, Property 2: Sanitização de URLs remove tokens sensíveis

    Valida: Requisitos 7.1, 7.2
    """

    @given(data=webhook_urls_with_tokens())
    @settings(max_examples=100)
    def test_sanitize_url_replaces_sensitive_params_with_stars(self, data):
        """**Validates: Requirements 7.1, 7.2**

        Para qualquer URL contendo parâmetros `key` e/ou `token` com valores
        arbitrários, sanitize_url deve retornar uma URL onde esses valores são
        substituídos por `***`, mantendo a estrutura da URL e todos os outros
        parâmetros intactos.
        """
        url, sensitive_params, extra_params = data
        result = sanitize_url(url)

        parsed_original = urlparse(url)
        parsed_result = urlparse(result)

        # A estrutura da URL (scheme, netloc, path) deve permanecer intacta
        assert parsed_result.scheme == parsed_original.scheme
        assert parsed_result.netloc == parsed_original.netloc
        assert parsed_result.path == parsed_original.path

        result_params = parse_qs(parsed_result.query, keep_blank_values=True)

        # Parâmetros sensíveis devem ter valor substituído por '***'
        for param_name in sensitive_params:
            assert param_name in result_params, (
                f"Parâmetro sensível '{param_name}' desapareceu da URL sanitizada"
            )
            for val in result_params[param_name]:
                assert val == "***", (
                    f"Valor do parâmetro sensível '{param_name}' não foi substituído por '***': {val}"
                )

        # Parâmetros não sensíveis devem manter seus valores originais
        for param_name, original_value in extra_params.items():
            assert param_name in result_params, (
                f"Parâmetro não sensível '{param_name}' desapareceu da URL sanitizada"
            )
            assert result_params[param_name] == [original_value], (
                f"Valor do parâmetro '{param_name}' foi alterado: "
                f"esperado {[original_value]}, obtido {result_params[param_name]}"
            )


# Estratégia para gerar tipos de exceção variados (excluindo ValueError e McpError)
_other_exception_types = st.sampled_from([
    RuntimeError,
    TypeError,
    KeyError,
    IOError,
    AttributeError,
    IndexError,
    OSError,
    ConnectionError,
    PermissionError,
])


class TestHandleErrorProperty:
    """Feature: pack-mcp-google-chat-notification, Property 8: Conversão de exceções pelo decorator handle_error

    Valida: Requisitos 6.1, 6.3
    """

    @given(message=st.text(min_size=1, max_size=200))
    @settings(max_examples=100)
    def test_value_error_preserves_original_message(self, message):
        """**Validates: Requirements 6.1, 6.3**

        Para qualquer ValueError lançada dentro de uma função decorada com
        @handle_error, o decorator deve convertê-la em McpError preservando
        a mensagem original.
        """

        @handle_error
        async def raises_value_error():
            raise ValueError(message)

        with pytest.raises(McpError) as exc_info:
            asyncio.get_event_loop().run_until_complete(raises_value_error())

        assert str(exc_info.value) == message
        assert exc_info.value.error.message == message

    @given(message=st.text(min_size=1, max_size=200))
    @settings(max_examples=100)
    def test_mcp_error_propagated_unchanged(self, message):
        """**Validates: Requirements 6.1, 6.3**

        Para qualquer McpError lançada dentro de uma função decorada com
        @handle_error, o decorator deve propagá-la sem alteração.
        """
        original_error = McpError(ErrorData(code=-1, message=message))

        @handle_error
        async def raises_mcp_error():
            raise original_error

        with pytest.raises(McpError) as exc_info:
            asyncio.get_event_loop().run_until_complete(raises_mcp_error())

        # Deve ser exatamente o mesmo objeto (mesma identidade)
        assert exc_info.value is original_error
        assert exc_info.value.error.message == message

    @given(
        exc_type=_other_exception_types,
        message=st.text(min_size=0, max_size=200),
    )
    @settings(max_examples=100)
    def test_other_exceptions_produce_generic_error(self, exc_type, message):
        """**Validates: Requirements 6.1, 6.3**

        Para qualquer exceção que não seja ValueError nem McpError, o decorator
        @handle_error deve convertê-la em McpError com a mensagem genérica
        "Erro interno do servidor".
        """

        @handle_error
        async def raises_other():
            raise exc_type(message)

        with pytest.raises(McpError) as exc_info:
            asyncio.get_event_loop().run_until_complete(raises_other())

        assert str(exc_info.value) == "Erro interno do servidor"
        assert exc_info.value.error.message == "Erro interno do servidor"
