"""ExtractionService coverage — pattern-based message classification."""

from __future__ import annotations

from typing import Any, ClassVar
from uuid import uuid4

import anthropic as anthropic_mod
import pytest
from roboco.llm import ToonAdapter
from roboco.models import MessageType
from roboco.models.extraction import (
    ExtractionConfig,
    ExtractionContext,
)
from roboco.services.extraction import ExtractionPipeline, ExtractionService


def _ctx(content: str) -> ExtractionContext:
    return ExtractionContext(
        content=content,
        agent_id=uuid4(),
        channel_id=uuid4(),
        session_id=uuid4(),
        group_id=uuid4(),
    )


@pytest.fixture
def svc() -> ExtractionService:
    return ExtractionService()


# ---------------------------------------------------------------------------
# Empty / short content
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_extract_returns_empty_for_short_content(
    svc: ExtractionService,
) -> None:
    result = await svc.extract(_ctx("hi"))
    assert result.messages == []


@pytest.mark.asyncio
async def test_extract_handles_empty_segments(
    svc: ExtractionService,
) -> None:
    """Whitespace-only segments are skipped."""
    result = await svc.extract(_ctx("\n\n   \n\n   \n\n"))
    assert result.messages == []


# ---------------------------------------------------------------------------
# Pattern classification
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_classifies_reasoning(svc: ExtractionService) -> None:
    result = await svc.extract(
        _ctx("I'm thinking about how to solve this problem efficiently.")
    )
    assert len(result.messages) >= 1
    assert MessageType.REASONING in result.types_extracted


@pytest.mark.asyncio
async def test_classifies_dialogue(svc: ExtractionService) -> None:
    result = await svc.extract(_ctx("Hey, can someone help me debug this issue?"))
    assert len(result.messages) >= 1


@pytest.mark.asyncio
async def test_classifies_decision(svc: ExtractionService) -> None:
    result = await svc.extract(
        _ctx("Decision: I will use the async pattern for this case.")
    )
    assert len(result.messages) >= 1
    assert MessageType.DECISION in result.types_extracted


@pytest.mark.asyncio
async def test_classifies_action(svc: ExtractionService) -> None:
    result = await svc.extract(_ctx("Starting the deployment process now."))
    assert len(result.messages) >= 1
    assert MessageType.ACTION in result.types_extracted


@pytest.mark.asyncio
async def test_classifies_blocker(svc: ExtractionService) -> None:
    result = await svc.extract(_ctx("Blocked: waiting for QA to review the PR."))
    assert len(result.messages) >= 1
    assert MessageType.BLOCKER in result.types_extracted


@pytest.mark.asyncio
async def test_classifies_technical_with_code_block(
    svc: ExtractionService,
) -> None:
    content = "Here is the implementation:\n\n```python\ndef foo(): pass\n```"
    result = await svc.extract(_ctx(content))
    assert len(result.messages) >= 1
    assert MessageType.TECHNICAL in result.types_extracted


@pytest.mark.asyncio
async def test_unmatched_defaults_to_reasoning(svc: ExtractionService) -> None:
    """Content without recognizable patterns falls back to REASONING."""
    result = await svc.extract(_ctx("xyzzy plugh fnord arglebargle"))
    assert len(result.messages) >= 1
    # Default classification is REASONING.
    assert any(m.type == MessageType.REASONING for m in result.messages)


# ---------------------------------------------------------------------------
# Segmentation
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_extract_splits_on_double_newlines(
    svc: ExtractionService,
) -> None:
    content = "First paragraph here.\n\nSecond paragraph here."
    result = await svc.extract(_ctx(content))
    _PARAS = 2
    assert len(result.messages) == _PARAS


@pytest.mark.asyncio
async def test_extract_keeps_code_blocks_intact(svc: ExtractionService) -> None:
    content = (
        "Some explanation.\n\n```\nline1\nline2\nline3\n```\n\nSome more explanation."
    )
    result = await svc.extract(_ctx(content))
    code_segments = [m for m in result.messages if m.content.startswith("```")]
    assert len(code_segments) >= 1


# ---------------------------------------------------------------------------
# Config respect
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_extract_respects_max_segments() -> None:
    svc = ExtractionService(ExtractionConfig(max_segments_per_buffer=2))
    content = "First.\n\nSecond.\n\nThird.\n\nFourth.\n\nFifth."
    _PARAS = 2
    result = await svc.extract(_ctx(content))
    assert len(result.messages) <= _PARAS


@pytest.mark.asyncio
async def test_extract_respects_min_content_length() -> None:
    svc = ExtractionService(ExtractionConfig(min_content_length=100))
    result = await svc.extract(_ctx("Short message only."))
    assert result.messages == []


# ---------------------------------------------------------------------------
# Result metadata
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_extract_populates_confidence_scores(
    svc: ExtractionService,
) -> None:
    result = await svc.extract(_ctx("I'm thinking carefully about this."))
    assert result.confidence_scores
    for score in result.confidence_scores.values():
        assert 0.0 <= score <= 1.0


@pytest.mark.asyncio
async def test_extract_records_pattern_matches(
    svc: ExtractionService,
) -> None:
    result = await svc.extract(_ctx("Decision: going with option A."))
    assert result.pattern_matches  # Non-empty.


# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_pipeline_invokes_callback() -> None:
    pipeline = ExtractionPipeline()
    received: list = []

    async def on_message(msg: Any) -> None:
        received.append(msg)

    pipeline.on_message(on_message)
    result = await pipeline.process_buffer(
        _ctx("First message here.\n\nSecond message here.")
    )
    assert result.message_count >= 1
    assert len(received) == result.message_count


@pytest.mark.asyncio
async def test_pipeline_swallows_callback_errors() -> None:
    """Callback failure should not abort the pipeline."""
    pipeline = ExtractionPipeline()

    async def bad_callback(_msg: Any) -> None:
        raise RuntimeError("boom")

    pipeline.on_message(bad_callback)
    # Should complete without raising despite callback error.
    result = await pipeline.process_buffer(_ctx("Hello there.\n\nGoodbye."))
    assert result is not None


# ---------------------------------------------------------------------------
# Mentions extraction (line 209)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_extract_logs_mentions_found(svc: ExtractionService) -> None:
    """When @mentions are present, the debug branch fires (line 209)."""
    content = "@be-dev-1 can you take a look at this code please?"
    result = await svc.extract(_ctx(content))
    assert len(result.messages) >= 1


@pytest.mark.asyncio
async def test_extract_skips_empty_segments_from_segmenter(
    svc: ExtractionService, monkeypatch: pytest.MonkeyPatch
) -> None:
    """If _segment_content yields a whitespace-only segment, it's skipped.

    Triggers the defensive `if not segment.strip(): continue` (line 193).
    """
    monkeypatch.setattr(
        svc, "_segment_content", lambda _content: ["   ", "Real content here."]
    )
    result = await svc.extract(_ctx("Long enough content to bypass min length."))
    # Only the non-empty segment becomes a message.
    assert len(result.messages) == 1


# ---------------------------------------------------------------------------
# extract_with_llm (lines 323-404)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_extract_with_llm_falls_back_on_error(
    svc: ExtractionService, monkeypatch: pytest.MonkeyPatch
) -> None:
    """If the Anthropic call fails, falls back to pattern-based extract."""

    class _BadClient:
        def __init__(self, **_kwargs: object) -> None:
            self.messages = self

        async def create(self, **_kwargs: object) -> object:
            raise RuntimeError("API down")

    monkeypatch.setattr(anthropic_mod, "AsyncAnthropic", _BadClient)
    result = await svc.extract_with_llm(_ctx("I'm thinking about this."))
    # Pattern fallback path executes; messages list is not empty.
    assert result is not None
    assert len(result.messages) >= 1


@pytest.mark.asyncio
async def test_extract_with_llm_parses_response(
    svc: ExtractionService, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Mock a successful LLM response and verify it gets parsed."""

    class _Block:
        text = "json-or-toon"

    class _Resp:
        content: ClassVar = [_Block()]

    class _OkClient:
        def __init__(self, **_kwargs: object) -> None:
            self.messages = self

        async def create(self, **_kwargs: object) -> _Resp:
            return _Resp()

    def _decode_dicts(_self: ToonAdapter, _text: str) -> list[dict[str, object]]:
        return [
            {"type": "reasoning", "content": "thinking", "confidence": 0.9},
            {"type": "action", "content": "doing", "confidence": 0.95},
        ]

    monkeypatch.setattr(anthropic_mod, "AsyncAnthropic", _OkClient)
    monkeypatch.setattr(ToonAdapter, "decode", _decode_dicts)

    result = await svc.extract_with_llm(_ctx("Some content for the LLM."))
    assert result is not None
    _COUNT = 2
    assert len(result.messages) == _COUNT


@pytest.mark.asyncio
async def test_extract_with_llm_handles_non_dict_segments(
    svc: ExtractionService, monkeypatch: pytest.MonkeyPatch
) -> None:
    """When the toon decoder returns non-dict segments, fallback type."""

    class _Block:
        text = "raw response"

    class _Resp:
        content: ClassVar = [_Block()]

    class _OkClient:
        def __init__(self, **_kwargs: object) -> None:
            self.messages = self

        async def create(self, **_kwargs: object) -> _Resp:
            return _Resp()

    # Patch toon to yield strings instead of dicts.
    def _decode_strings(_self: ToonAdapter, _text: str) -> list[str]:
        return ["plain string segment", "another"]

    monkeypatch.setattr(anthropic_mod, "AsyncAnthropic", _OkClient)
    monkeypatch.setattr(ToonAdapter, "decode", _decode_strings)

    result = await svc.extract_with_llm(_ctx("Some content for the LLM."))
    assert result is not None
    _COUNT = 2
    assert len(result.messages) == _COUNT
