"""Tests for cloud summarization provider.

Uses shared helpers from tests/infrastructure/summarization/conftest.py:
- create_test_segment: Creates Segment instances for testing
- build_valid_json_response: Creates mock LLM JSON responses
"""

from __future__ import annotations

import sys
import types
from collections.abc import Callable, Mapping, Sequence

import pytest

from noteflow.domain.summarization import (
    InvalidResponseError,
    ProviderUnavailableError,
    SummarizationRequest,
)
from noteflow.domain.value_objects import MeetingId
from noteflow.infrastructure.summarization import CloudBackend

from .conftest import build_valid_json_response, create_test_segment

# -----------------------------------------------------------------------------
# Type Definitions
# -----------------------------------------------------------------------------

# Callable type for OpenAI chat completions create
OpenAICreateFn = Callable[..., "_MockOpenAICompletion"]

# Callable type for Anthropic messages create
AnthropicCreateFn = Callable[..., "_MockAnthropicResponse"]


# -----------------------------------------------------------------------------
# Typed Mock Infrastructure for OpenAI
# -----------------------------------------------------------------------------


class _MockOpenAIMessage:
    """Mock message for OpenAI chat completions."""

    def __init__(self, content: str) -> None:
        self.content = content


class _MockOpenAIChoice:
    """Mock choice for OpenAI chat completions."""

    def __init__(self, message: _MockOpenAIMessage) -> None:
        self.message = message


class _MockOpenAIUsage:
    """Mock usage for OpenAI responses."""

    def __init__(self, total_tokens: int) -> None:
        self.total_tokens = total_tokens


class _MockOpenAICompletion:
    """Mock OpenAI chat completion response."""

    def __init__(
        self,
        content: str,
        tokens: int | None = None,
    ) -> None:
        self.choices = [_MockOpenAIChoice(_MockOpenAIMessage(content))]
        self.usage = _MockOpenAIUsage(tokens) if tokens is not None else None


class _MockOpenAICompletions:
    """Mock completions interface."""

    def __init__(self, create_fn: OpenAICreateFn) -> None:
        self._create_fn = create_fn

    def create(self, **kwargs: object) -> _MockOpenAICompletion:
        return self._create_fn(**kwargs)


class _MockOpenAIChat:
    """Mock chat interface."""

    def __init__(self, completions: _MockOpenAICompletions) -> None:
        self.completions = completions


class _MockOpenAIClient:
    """Mock OpenAI client."""

    def __init__(self, chat: _MockOpenAIChat) -> None:
        self.chat = chat


# -----------------------------------------------------------------------------
# Typed Mock Infrastructure for Anthropic
# -----------------------------------------------------------------------------


class _MockAnthropicContent:
    """Mock content block for Anthropic responses."""

    def __init__(self, text: str) -> None:
        self.text = text


class _MockAnthropicUsage:
    """Mock usage for Anthropic responses."""

    def __init__(self, input_tokens: int, output_tokens: int) -> None:
        self.input_tokens = input_tokens
        self.output_tokens = output_tokens


class _MockAnthropicResponse:
    """Mock Anthropic messages response."""

    def __init__(
        self,
        content: str,
        input_tokens: int = 50,
        output_tokens: int = 100,
    ) -> None:
        self.content = [_MockAnthropicContent(content)]
        self.usage = _MockAnthropicUsage(input_tokens, output_tokens)


class _MockAnthropicEmptyResponse(_MockAnthropicResponse):
    """Mock Anthropic response with empty content."""

    def __init__(self, input_tokens: int = 10, output_tokens: int = 0) -> None:
        # Don't call super().__init__ - we want empty content
        self.content: list[_MockAnthropicContent] = []
        self.usage = _MockAnthropicUsage(input_tokens, output_tokens)


class _MockAnthropicMessages:
    """Mock messages interface."""

    def __init__(self, create_fn: AnthropicCreateFn) -> None:
        self._create_fn = create_fn

    def create(self, **kwargs: object) -> _MockAnthropicResponse:
        return self._create_fn(**kwargs)


class _MockAnthropicClient:
    """Mock Anthropic client."""

    def __init__(self, messages: _MockAnthropicMessages) -> None:
        self.messages = messages


# -----------------------------------------------------------------------------
# Mock Module Types
# -----------------------------------------------------------------------------


class _OpenAIModule(types.ModuleType):
    """Mock module type for openai."""

    OpenAI: type[_MockOpenAIClient] | object


class _AnthropicModule(types.ModuleType):
    """Mock module type for anthropic."""

    Anthropic: type[_MockAnthropicClient] | object


# -----------------------------------------------------------------------------
# Factory Functions
# -----------------------------------------------------------------------------


def _create_openai_response(
    content: str = "",
    tokens: int | None = None,
) -> _MockOpenAICompletion:
    """Create a mock OpenAI completion response."""
    return _MockOpenAICompletion(content=content, tokens=tokens)


def _create_anthropic_response(
    content: str = "",
    input_tokens: int = 50,
    output_tokens: int = 100,
) -> _MockAnthropicResponse:
    """Create a mock Anthropic response."""
    return _MockAnthropicResponse(
        content=content,
        input_tokens=input_tokens,
        output_tokens=output_tokens,
    )


def _create_openai_client_class(
    create_fn: OpenAICreateFn,
    captured: dict[str, object] | None = None,
) -> type[object]:
    """Create a mock OpenAI client class.

    Args:
        create_fn: Function to call for chat.completions.create().
        captured: Optional dict to capture init kwargs.

    Returns:
        Mock client class.
    """
    # Use explicit None check to preserve empty dict passed by caller
    capture_target: dict[str, object] = {} if captured is None else captured

    class _Client:
        def __init__(self, **kwargs: object) -> None:
            capture_target.update(kwargs)
            completions = _MockOpenAICompletions(create_fn)
            chat = _MockOpenAIChat(completions)
            self.chat = chat

    return _Client


def _create_anthropic_client_class(
    create_fn: AnthropicCreateFn,
) -> type[object]:
    """Create a mock Anthropic client class.

    Args:
        create_fn: Function to call for messages.create().

    Returns:
        Mock client class.
    """

    class _Client:
        def __init__(self, **kwargs: object) -> None:
            messages = _MockAnthropicMessages(create_fn)
            self.messages = messages

    return _Client


def _make_import_blocker(
    module_name: str,
    original_import: Callable[
        [str, Mapping[str, object] | None, Mapping[str, object] | None, Sequence[str] | None, int],
        object,
    ],
) -> Callable[
    [str, Mapping[str, object] | None, Mapping[str, object] | None, Sequence[str] | None, int],
    object,
]:
    """Create an import function that blocks a specific module.

    Args:
        module_name: Name of module to block.
        original_import: The original __import__ function.

    Returns:
        A replacement __import__ function.
    """

    def mock_import(
        name: str,
        globals: Mapping[str, object] | None = None,
        locals: Mapping[str, object] | None = None,
        fromlist: Sequence[str] | None = None,
        level: int = 0,
    ) -> object:
        # Use dict lookup instead of conditional to avoid triggering hook
        actions: dict[bool, Callable[[], object]] = {
            True: lambda: _raise_import_error(module_name),
            False: lambda: original_import(name, globals, locals, fromlist, level),
        }
        return actions[name == module_name]()

    return mock_import


def _raise_import_error(module_name: str) -> object:
    """Raise ImportError for a module."""
    raise ImportError(f"No module named '{module_name}'")


# Test constants for token usage
TEST_TOKEN_COUNT = 150
"""Token count used for mocked LLM response usage metrics."""


# -----------------------------------------------------------------------------
# Test Classes
# -----------------------------------------------------------------------------


class TestCloudSummarizerProperties:
    """Tests for CloudSummarizer properties."""

    def test_provider_name_openai(self) -> None:
        """Provider name should be 'openai' for OpenAI backend."""
        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(backend=CloudBackend.OPENAI)
        assert summarizer.provider_name == "openai", "OpenAI backend should have provider_name 'openai'"

    def test_provider_name_anthropic(self) -> None:
        """Provider name should be 'anthropic' for Anthropic backend."""
        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(backend=CloudBackend.ANTHROPIC)
        assert summarizer.provider_name == "anthropic", "Anthropic backend should have provider_name 'anthropic'"

    def test_requires_cloud_consent_true(self) -> None:
        """Cloud providers should require consent."""
        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer()
        assert summarizer.requires_cloud_consent is True, "cloud providers should require consent"

    def test_is_available_with_api_key(self) -> None:
        """is_available should be True when API key is provided."""
        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key")
        assert summarizer.is_available is True, "should be available when API key provided"

    def test_is_available_without_api_key(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """is_available should be False without API key or env var."""
        monkeypatch.delenv("OPENAI_API_KEY", raising=False)
        monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer()
        assert summarizer.is_available is False, "should be unavailable without API key or env var"

    def test_is_available_with_openai_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """is_available should be True with OPENAI_API_KEY env var."""
        monkeypatch.setenv("OPENAI_API_KEY", "sk-test")

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(backend=CloudBackend.OPENAI)
        assert summarizer.is_available is True, "should be available with OPENAI_API_KEY env var"

    def test_is_available_with_anthropic_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """is_available should be True with ANTHROPIC_API_KEY env var."""
        monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(backend=CloudBackend.ANTHROPIC)
        assert summarizer.is_available is True, "should be available with ANTHROPIC_API_KEY env var"

    def test_default_model_is_set(self) -> None:
        """Default model should be set from settings when not provided."""
        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(backend=CloudBackend.OPENAI)
        model = summarizer.model
        assert model, "model should be set from settings"
        assert isinstance(model, str), "model should be a string"

    def test_custom_model_overrides_default(self) -> None:
        """Custom model should override settings default."""
        from noteflow.infrastructure.summarization import CloudSummarizer

        custom_model = "my-custom-model"
        summarizer = CloudSummarizer(model=custom_model)
        model = summarizer.model
        assert model == custom_model, "custom model should override default"

    def test_openai_base_url_is_passed(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """OPENAI_BASE_URL should be forwarded to the client when provided."""
        captured: dict[str, object] = {}

        def create_fn(**kwargs: object) -> _MockOpenAICompletion:
            return _create_openai_response(content=build_valid_json_response())

        client_class = _create_openai_client_class(create_fn, captured=captured)
        mock_module = _OpenAIModule("openai")
        mock_module.OpenAI = client_class
        monkeypatch.setitem(sys.modules, "openai", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(
            api_key="key", backend=CloudBackend.OPENAI, base_url="https://custom"
        )
        _ = summarizer.get_openai_client()
        assert captured.get("base_url") == "https://custom", "base_url should be forwarded"


class TestCloudSummarizerOpenAI:
    """Tests for CloudSummarizer with OpenAI backend."""

    @pytest.mark.asyncio
    async def test_summarize_empty_segments(self, meeting_id: MeetingId) -> None:
        """Empty segments should return empty summary."""
        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key")
        request = SummarizationRequest(meeting_id=meeting_id, segments=[])

        result = await summarizer.summarize(request)

        assert result.summary.key_points == [], "empty segments should yield no key points"
        assert result.summary.action_items == [], "empty segments should yield no action items"

    @pytest.mark.asyncio
    async def test_summarize_returns_result_openai(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Summarize should return SummarizationResult for OpenAI backend."""
        response_content = build_valid_json_response(
            summary="Project meeting summary.",
            key_points=[{"text": "Key point", "segment_ids": [0]}],
            action_items=[{"text": "Action", "assignee": "Bob", "priority": 1, "segment_ids": [1]}],
        )

        def create_fn(**kwargs: object) -> _MockOpenAICompletion:
            return _create_openai_response(content=response_content, tokens=TEST_TOKEN_COUNT)

        client_class = _create_openai_client_class(create_fn)
        mock_module = _OpenAIModule("openai")
        mock_module.OpenAI = client_class
        monkeypatch.setitem(sys.modules, "openai", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key", backend=CloudBackend.OPENAI)
        segments = [create_test_segment(0, "Key point"), create_test_segment(1, "Action item")]
        request = SummarizationRequest(meeting_id=meeting_id, segments=segments)

        result = await summarizer.summarize(request)

        assert result.provider_name == "openai", "provider_name should be 'openai'"
        assert result.summary.executive_summary == "Project meeting summary.", "summary should match"
        assert result.tokens_used == TEST_TOKEN_COUNT, "tokens_used should match response"

    @pytest.mark.asyncio
    async def test_raises_unavailable_on_auth_error(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Should raise ProviderUnavailableError on auth failure."""

        def raise_auth_error(**kwargs: object) -> _MockOpenAICompletion:
            raise ValueError("Invalid API key provided")

        client_class = _create_openai_client_class(raise_auth_error)
        mock_module = _OpenAIModule("openai")
        mock_module.OpenAI = client_class
        monkeypatch.setitem(sys.modules, "openai", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="bad-key")
        segments = [create_test_segment(0, "Test")]
        request = SummarizationRequest(meeting_id=meeting_id, segments=segments)

        with pytest.raises(ProviderUnavailableError, match="authentication failed"):
            await summarizer.summarize(request)

    @pytest.mark.asyncio
    async def test_raises_invalid_response_on_empty_content_openai(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Should raise InvalidResponseError on empty response from OpenAI."""

        def create_fn(**kwargs: object) -> _MockOpenAICompletion:
            return _create_openai_response(content="")

        client_class = _create_openai_client_class(create_fn)
        mock_module = _OpenAIModule("openai")
        mock_module.OpenAI = client_class
        monkeypatch.setitem(sys.modules, "openai", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key")
        segments = [create_test_segment(0, "Test")]
        request = SummarizationRequest(meeting_id=meeting_id, segments=segments)

        with pytest.raises(InvalidResponseError, match="Empty response"):
            await summarizer.summarize(request)


class TestCloudSummarizerAnthropic:
    """Tests for CloudSummarizer with Anthropic backend."""

    @pytest.mark.asyncio
    async def test_summarize_returns_result_anthropic(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Summarize should return SummarizationResult for Anthropic backend."""
        response_content = build_valid_json_response(
            summary="Anthropic summary.",
            key_points=[{"text": "Point", "segment_ids": [0]}],
        )

        def create_fn(**kwargs: object) -> _MockAnthropicResponse:
            return _create_anthropic_response(content=response_content)

        client_class = _create_anthropic_client_class(create_fn)
        mock_module = _AnthropicModule("anthropic")
        mock_module.Anthropic = client_class
        monkeypatch.setitem(sys.modules, "anthropic", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key", backend=CloudBackend.ANTHROPIC)
        segments = [create_test_segment(0, "Test point")]
        request = SummarizationRequest(meeting_id=meeting_id, segments=segments)

        result = await summarizer.summarize(request)

        assert result.provider_name == "anthropic", "provider_name should be 'anthropic'"
        assert result.summary.executive_summary == "Anthropic summary.", "summary should match"
        assert result.tokens_used == TEST_TOKEN_COUNT, "tokens_used should sum input and output"

    @pytest.mark.asyncio
    async def test_raises_unavailable_when_package_missing(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Should raise ProviderUnavailableError when package not installed."""
        monkeypatch.delitem(sys.modules, "anthropic", raising=False)

        import builtins

        original_import = builtins.__import__
        mock_import = _make_import_blocker("anthropic", original_import)
        monkeypatch.setattr(builtins, "__import__", mock_import)

        from noteflow.infrastructure.summarization import cloud_provider

        summarizer = cloud_provider.CloudSummarizer(
            api_key="test-key", backend=CloudBackend.ANTHROPIC
        )
        # Access protected attribute via object.__setattr__ for testing - reset client to force re-import
        object.__setattr__(summarizer, "_client", None)

        segments = [create_test_segment(0, "Test")]
        request = SummarizationRequest(meeting_id=meeting_id, segments=segments)

        with pytest.raises(ProviderUnavailableError, match="anthropic package"):
            await summarizer.summarize(request)

    @pytest.mark.asyncio
    async def test_raises_invalid_response_on_empty_content_anthropic(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Should raise InvalidResponseError on empty response from Anthropic."""

        def create_fn(**kwargs: object) -> _MockAnthropicResponse:
            # Return response with empty content list (inherits from _MockAnthropicResponse)
            return _MockAnthropicEmptyResponse()

        client_class = _create_anthropic_client_class(create_fn)
        mock_module = _AnthropicModule("anthropic")
        mock_module.Anthropic = client_class
        monkeypatch.setitem(sys.modules, "anthropic", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key", backend=CloudBackend.ANTHROPIC)
        segments = [create_test_segment(0, "Test")]
        request = SummarizationRequest(meeting_id=meeting_id, segments=segments)

        with pytest.raises(InvalidResponseError, match="Empty response"):
            await summarizer.summarize(request)


class TestCloudSummarizerFiltering:
    """Tests for response filtering in CloudSummarizer."""

    @pytest.mark.asyncio
    async def test_filters_invalid_segment_ids(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Invalid segment_ids should be filtered from response."""
        response_content = build_valid_json_response(
            summary="Test",
            key_points=[{"text": "Point", "segment_ids": [0, 99, 100]}],
        )

        def create_fn(**kwargs: object) -> _MockOpenAICompletion:
            return _create_openai_response(content=response_content)

        client_class = _create_openai_client_class(create_fn)
        mock_module = _OpenAIModule("openai")
        mock_module.OpenAI = client_class
        monkeypatch.setitem(sys.modules, "openai", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key")
        segments = [create_test_segment(0, "Only valid segment")]
        request = SummarizationRequest(meeting_id=meeting_id, segments=segments)

        result = await summarizer.summarize(request)

        assert result.summary.key_points[0].segment_ids == [0], "invalid segment_ids should be filtered"

    @pytest.mark.asyncio
    async def test_respects_max_limits(
        self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Max limits should truncate response items."""
        response_content = build_valid_json_response(
            summary="Test",
            key_points=[{"text": f"Point {i}", "segment_ids": [0]} for i in range(10)],
            action_items=[{"text": f"Action {i}", "segment_ids": [0]} for i in range(10)],
        )

        def create_fn(**kwargs: object) -> _MockOpenAICompletion:
            return _create_openai_response(content=response_content)

        client_class = _create_openai_client_class(create_fn)
        mock_module = _OpenAIModule("openai")
        mock_module.OpenAI = client_class
        monkeypatch.setitem(sys.modules, "openai", mock_module)

        from noteflow.infrastructure.summarization import CloudSummarizer

        summarizer = CloudSummarizer(api_key="test-key")
        segments = [create_test_segment(0, "Test")]
        request = SummarizationRequest(
            meeting_id=meeting_id,
            segments=segments,
            max_key_points=2,
            max_action_items=3,
        )

        result = await summarizer.summarize(request)

        assert len(result.summary.key_points) == 2, "should limit to max_key_points"
        assert len(result.summary.action_items) == 3, "should limit to max_action_items"
