# -*- coding: utf-8 -*-
"""
Channel Contract Tests

Contract tests for BaseChannel subclasses.

When BaseChannel changes, these tests ensure ALL channels still comply.
This prevents "fix Console, break DingTalk" regressions.

Usage:
    from tests.contract.channels import ChannelContractTest

    class TestMyChannelContract(ChannelContractTest):
        def create_instance(self):
            return MyChannel(process=mock_process, ...)
"""

from __future__ import annotations

import inspect
from abc import abstractmethod
from typing import Any

import pytest

from .. import BaseContractTest


class ChannelContractTest(BaseContractTest):
    """
    Contract tests for BaseChannel subclasses.

    This defines the interface contract that ALL channels must satisfy.
    When BaseChannel changes, these tests ensure all channels still comply.

    Contracts verified:
    1. Required abstract methods are implemented
    2. Method signatures are compatible
    3. Critical behavior invariants are maintained
    4. No abstract methods remain unimplemented
    """

    @abstractmethod
    def create_instance(self) -> Any:
        """Provide a configured channel instance for testing."""
        return None  # pragma: no cover

    # =========================================================================
    # Contract: No Abstract Methods Left Unimplemented
    # =========================================================================

    def test_no_abstract_methods_remaining(self, instance):
        """
        CRITICAL: All abstract methods from BaseChannel must be implemented.

        If BaseChannel adds a new abstract method, this test will FAIL
        for all subclasses until they implement it.
        """
        cls = instance.__class__
        abstract_methods = getattr(cls, "__abstractmethods__", set())

        if abstract_methods:
            pytest.fail(
                f"{cls.__name__} has unimplemented abstract methods: "
                f"{', '.join(abstract_methods)}. "
                f"These methods were added to BaseChannel "
                f"and must be implemented.",
            )

    def test_no_abstractmethods__in_instance(self, instance):
        """
        CRITICAL: Instance must not have abstract methods (Python ABC check).

        This catches cases where BaseChannel defines @abstractmethod
        but subclass doesn't implement it - Python will prevent instantiation.
        If this test runs, it means instance was created successfully.
        """
        # This test passing means the instance was successfully created,
        # which implies no abstract methods remain unimplemented.
        # If there were unimplemented abstract methods, create_instance()
        # would have raised TypeError during fixture setup.
        assert instance is not None, (
            "Instance creation failed - "
            "check for unimplemented abstract methods"
        )

    # =========================================================================
    # Contract: Required Methods Implementation (Non-ABC Check)
    # =========================================================================

    def test_required_methods_not_raising_not_implemented(self, instance):
        """
        CRITICAL: Required methods must not raise NotImplementedError.

        This checks that methods marked with 'raise NotImplementedError'
        in BaseChannel have been properly overridden by subclasses.
        """
        from qwenpaw.app.channels.base import BaseChannel

        cls = instance.__class__
        required_methods = [
            "start",
            "stop",
            "send",
            "build_agent_request_from_native",
        ]

        for method_name in required_methods:
            # Get the method from the subclass
            subclass_method = getattr(cls, method_name, None)
            if subclass_method is None:
                pytest.fail(
                    f"{cls.__name__} does not implement {method_name}()",
                )

            # Get the method from BaseChannel
            base_method = getattr(BaseChannel, method_name, None)
            if base_method is None:
                continue

            # Check if the subclass method is different from BaseChannel's
            # (i.e., it has been overridden)
            if subclass_method is base_method:
                pytest.fail(
                    f"{cls.__name__}.{method_name}() is not overridden. "
                    f"It must implement the method instead of "
                    f"inheriting from BaseChannel.",
                )

            # Try to extract source code to check for NotImplementedError
            try:
                source = inspect.getsource(subclass_method)
                if (
                    "NotImplementedError" in source
                    and method_name != "send_media"
                ):
                    pytest.fail(
                        f"{cls.__name__}.{method_name}() contains "
                        f"'NotImplementedError'. It must provide "
                        f"a real implementation.",
                    )
            except (OSError, TypeError):
                # Can't get source (e.g., built-in), skip this check
                pass

    # =========================================================================
    # Contract: Required Abstract Methods
    # =========================================================================

    def test_has_channel_type_attribute(self, instance):
        """Contract: All channels must define channel type."""
        assert hasattr(instance, "channel"), "Missing 'channel' attribute"
        assert instance.channel is not None, "'channel' cannot be None"
        assert isinstance(instance.channel, str), "'channel' must be a string"

    def test_has_start_method(self, instance):
        """Contract: All channels must implement start()."""
        assert hasattr(instance, "start"), "Missing start() method"
        assert callable(getattr(instance, "start")), "start must be callable"

    def test_has_stop_method(self, instance):
        """Contract: All channels must implement stop()."""
        assert hasattr(instance, "stop"), "Missing stop() method"
        assert callable(getattr(instance, "stop")), "stop must be callable"

    def test_has_send_method(self, instance):
        """Contract: All channels must implement send()."""
        assert hasattr(instance, "send"), "Missing send() method"
        assert callable(getattr(instance, "send")), "send must be callable"

    def test_has_from_config_method(self, instance):
        """Contract: All channels must implement from_config()."""
        cls = instance.__class__
        assert hasattr(
            cls,
            "from_config",
        ), f"{cls.__name__} missing from_config()"
        assert callable(
            getattr(cls, "from_config"),
        ), "from_config must be callable"

    def test_has_build_agent_request_from_native_method(self, instance):
        """All channels must implement build_agent_request_from_native."""
        attr_name = "build_agent_request_from_native"
        assert hasattr(instance, attr_name), f"Missing {attr_name}"
        method = getattr(instance, attr_name)
        assert callable(method), f"{attr_name} must be callable"

    # =========================================================================
    # Contract: Method Signature Compatibility
    # =========================================================================

    def test_start_method_signature_compatible(self, instance):
        """
        Contract: start() must accept no required arguments (except self).

        If BaseChannel changes start() signature, this catches incompatible
        subclasses.
        """
        sig = inspect.signature(instance.start)
        params = list(sig.parameters.values())

        # Check for required parameters beyond self/cls
        for param in params:
            if param.name in ("self", "cls"):
                continue
            if param.default is inspect.Parameter.empty and param.kind in (
                inspect.Parameter.POSITIONAL_ONLY,
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
            ):
                pytest.fail(
                    f"{instance.__class__.__name__}.start() has required "
                    f"parameter '{param.name}'. start() should accept no "
                    f"required arguments to match BaseChannel contract.",
                )

    def test_stop_method_signature_compatible(self, instance):
        """Contract: stop() must accept no required arguments."""
        sig = inspect.signature(instance.stop)
        params = list(sig.parameters.values())

        for param in params:
            if param.name in ("self", "cls"):
                continue
            if param.default is inspect.Parameter.empty and param.kind in (
                inspect.Parameter.POSITIONAL_ONLY,
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
            ):
                pytest.fail(
                    f"{instance.__class__.__name__}.stop() has required "
                    f"parameter '{param.name}'.",
                )

    def test_resolve_session_id_signature_compatible(self, instance):
        """
        Contract: resolve_session_id() must accept sender_id and optional meta.
        """
        sig = inspect.signature(instance.resolve_session_id)
        params = list(sig.parameters.values())
        param_names = [p.name for p in params if p.name not in ("self", "cls")]

        assert "sender_id" in param_names, (
            f"{instance.__class__.__name__}.resolve_session_id() missing "
            f"required 'sender_id' parameter"
        )

    # =========================================================================
    # Contract: Configuration Interface
    # =========================================================================

    def test_uses_manager_queue_attribute_exists(self, instance):
        """Channels should have uses_manager_queue class attribute."""
        cls = instance.__class__
        assert hasattr(
            cls,
            "uses_manager_queue",
        ), "Missing uses_manager_queue class attribute"

    def test_render_style_attributes_exist(self, instance):
        """Contract: Channels should have render-related attributes."""
        # These are set in BaseChannel.__init__
        assert hasattr(instance, "_render_style"), "Missing _render_style"
        assert hasattr(instance, "_renderer"), "Missing _renderer"

    # =========================================================================
    # Contract: Session Management
    # =========================================================================

    def test_resolve_session_id_returns_str(self, instance):
        """Contract: resolve_session_id must return string."""
        result = instance.resolve_session_id("test_user")
        assert isinstance(
            result,
            str,
        ), f"resolve_session_id must return str, got {type(result)}"

    def test_resolve_session_id_with_meta(self, instance):
        """Contract: resolve_session_id must accept optional meta parameter."""
        # Should not raise when meta is provided
        try:
            result = instance.resolve_session_id(
                "test_user",
                {"conversation_id": "123"},
            )
            assert isinstance(
                result,
                str,
            ), "resolve_session_id with meta must return str"
        except TypeError as e:
            pytest.fail(
                f"{instance.__class__.__name__}.resolve_session_id() does not "
                f"accept meta parameter: {e}",
            )

    def test_get_to_handle_from_request_exists(self, instance):
        """Contract: get_to_handle_from_request method must exist."""
        assert hasattr(instance, "get_to_handle_from_request")

    # =========================================================================
    # Contract: Policy Attributes
    # =========================================================================

    def test_policy_attributes_exist(self, instance):
        """Channels must have policy attributes for access control."""
        assert hasattr(instance, "dm_policy"), "Missing dm_policy"
        assert hasattr(instance, "group_policy"), "Missing group_policy"
        assert hasattr(instance, "allow_from"), "Missing allow_from"

    def test_policy_attributes_types(self, instance):
        """Contract: Policy attributes must have correct types."""
        assert isinstance(instance.dm_policy, str), "dm_policy must be str"
        assert isinstance(
            instance.group_policy,
            str,
        ), "group_policy must be str"
        assert isinstance(
            instance.allow_from,
            (set, list),
        ), "allow_from must be set or list"


__all__ = ["ChannelContractTest"]
