"""Tests for RAG pipeline, Ollama provider, and session management."""

from __future__ import annotations

import json
from collections.abc import AsyncIterator
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, patch

import httpx
import pytest

from redhat_oracle.core.config import OracleConfig
from redhat_oracle.core.llm.base import LLMProvider
from redhat_oracle.core.llm.ollama import OllamaLLM, OllamaUnavailableError
from redhat_oracle.core.rag import RAGPipeline
from redhat_oracle.core.session import (
    ChatSession,
    load_latest_session,
    load_session,
    new_session,
    save_session,
)

_DUMMY_REQUEST = httpx.Request("POST", "http://localhost:11434/api/generate")


class FakeLLM(LLMProvider):
    """Fake LLM that returns canned responses."""

    def __init__(self, response: str = "Test response") -> None:
        self._response = response

    async def generate(self, prompt: str, system: str | None = None) -> str:
        return self._response

    async def stream(self, prompt: str, system: str | None = None) -> AsyncIterator[str]:
        for word in self._response.split():
            yield word + " "


class FakeStore:
    """Fake vector store that returns canned search results."""

    def __init__(self, results: list[dict[str, Any]] | None = None) -> None:
        self._results = results or []

    def search(
        self,
        query: str,
        source: str | None = None,
        top_k: int = 10,
        min_score: float = 0.3,
    ) -> list[dict[str, Any]]:
        return self._results


# --- Ollama tests ---


def _make_ollama_mocks(
    lines: list[str],
) -> AsyncMock:
    """Build a mock httpx.AsyncClient that streams the given JSON lines."""
    from contextlib import asynccontextmanager

    mock_response = AsyncMock()
    mock_response.raise_for_status = lambda: None
    mock_response.aiter_lines = lambda: _async_iter(lines)

    mock_client = AsyncMock()

    @asynccontextmanager
    async def fake_stream(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
        yield mock_response

    mock_client.stream = fake_stream
    mock_client.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client.__aexit__ = AsyncMock(return_value=None)
    return mock_client


class TestOllamaLLM:
    @pytest.mark.asyncio
    async def test_stream_tokens(self) -> None:
        lines = [
            json.dumps({"response": "Hello", "done": False}),
            json.dumps({"response": " world", "done": False}),
            json.dumps({"response": "!", "done": True}),
        ]
        mock_client = _make_ollama_mocks(lines)

        with patch("httpx.AsyncClient", return_value=mock_client):
            ollama = OllamaLLM()
            tokens: list[str] = []
            async for token in ollama.stream("test prompt"):
                tokens.append(token)

        assert tokens == ["Hello", " world", "!"]

    @pytest.mark.asyncio
    async def test_generate_concatenates_tokens(self) -> None:
        lines = [
            json.dumps({"response": "Hello", "done": False}),
            json.dumps({"response": " world", "done": True}),
        ]
        mock_client = _make_ollama_mocks(lines)

        with patch("httpx.AsyncClient", return_value=mock_client):
            ollama = OllamaLLM()
            result = await ollama.generate("test prompt")

        assert result == "Hello world"

    @pytest.mark.asyncio
    async def test_unavailable_raises(self) -> None:
        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)
        mock_client.get = AsyncMock(side_effect=httpx.ConnectError("refused"))

        with (
            patch("httpx.AsyncClient", return_value=mock_client),
            pytest.raises(OllamaUnavailableError),
        ):
            ollama = OllamaLLM()
            await ollama._check_available()

    @pytest.mark.asyncio
    async def test_stream_connection_error(self) -> None:
        from contextlib import asynccontextmanager

        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=None)

        @asynccontextmanager
        async def raise_connect(*args: Any, **kwargs: Any) -> AsyncIterator[Any]:
            raise httpx.ConnectError("refused")
            yield  # pragma: no cover

        mock_client.stream = raise_connect

        with patch("httpx.AsyncClient", return_value=mock_client):
            ollama = OllamaLLM()
            with pytest.raises(OllamaUnavailableError):
                async for _ in ollama.stream("test"):
                    pass


# --- Session tests ---


class TestSession:
    def test_new_session_timestamp_name(self, tmp_path: Path) -> None:
        config = OracleConfig(data_dir=str(tmp_path))
        session = new_session(config)
        assert session.name  # Non-empty
        assert len(session.name) == 15  # YYYYMMDD_HHMMSS

    def test_add_message(self) -> None:
        session = ChatSession(name="test")
        msg = session.add_message("user", "Hello")
        assert msg.role == "user"
        assert msg.content == "Hello"
        assert len(session.messages) == 1

    def test_save_and_load(self, tmp_path: Path) -> None:
        config = OracleConfig(data_dir=str(tmp_path))
        session = ChatSession(name="test_session")
        session.add_message("user", "Hello")
        session.add_message("assistant", "Hi there!")

        save_session(session, config)

        loaded = load_session("test_session", config)
        assert loaded is not None
        assert len(loaded.messages) == 2
        assert loaded.messages[0].content == "Hello"
        assert loaded.messages[1].content == "Hi there!"

    def test_load_nonexistent_returns_none(self, tmp_path: Path) -> None:
        config = OracleConfig(data_dir=str(tmp_path))
        assert load_session("nonexistent", config) is None

    def test_load_latest(self, tmp_path: Path) -> None:
        config = OracleConfig(data_dir=str(tmp_path))

        s1 = ChatSession(name="session_001")
        s1.add_message("user", "First")
        save_session(s1, config)

        s2 = ChatSession(name="session_002")
        s2.add_message("user", "Second")
        save_session(s2, config)

        latest = load_latest_session(config)
        assert latest is not None
        assert latest.name == "session_002"

    def test_load_latest_no_sessions(self, tmp_path: Path) -> None:
        config = OracleConfig(data_dir=str(tmp_path))
        assert load_latest_session(config) is None

    def test_get_history_limit(self) -> None:
        session = ChatSession(name="test")
        for i in range(20):
            session.add_message("user", f"Message {i}")

        history = session.get_history(max_messages=5)
        assert len(history) == 5
        assert history[0].content == "Message 15"

    def test_message_sources(self) -> None:
        session = ChatSession(name="test")
        sources = [{"source": "github", "path": "main.py"}]
        msg = session.add_message("assistant", "Answer", sources=sources)
        assert msg.sources == sources


# --- RAG pipeline tests ---


class TestRAGPipeline:
    @pytest.mark.asyncio
    async def test_query_with_context(self) -> None:
        config = OracleConfig()
        search_results = [
            {
                "id": "chunk1",
                "text": "def sort_list(items): return sorted(items)",
                "metadata": {
                    "source": "github",
                    "org": "myorg",
                    "repo": "myrepo",
                    "path": "utils.py",
                },
                "score": 0.85,
            }
        ]

        store = FakeStore(results=search_results)
        llm = FakeLLM(response="Use the sorted() function")

        pipeline = RAGPipeline(config, store, llm)  # type: ignore[arg-type]
        result = await pipeline.query("How do I sort a list?")

        assert result.response == "Use the sorted() function"
        assert len(result.sources) == 1
        assert result.sources[0]["path"] == "utils.py"
        assert result.sources[0]["score"] == 0.85

    @pytest.mark.asyncio
    async def test_query_no_results(self) -> None:
        config = OracleConfig()
        store = FakeStore(results=[])
        llm = FakeLLM(response="I don't have context for that")

        pipeline = RAGPipeline(config, store, llm)  # type: ignore[arg-type]
        result = await pipeline.query("Random question")

        assert result.response == "I don't have context for that"
        assert result.sources == []

    @pytest.mark.asyncio
    async def test_stream_query(self) -> None:
        config = OracleConfig()
        store = FakeStore(results=[])
        llm = FakeLLM(response="Streaming response here")

        pipeline = RAGPipeline(config, store, llm)  # type: ignore[arg-type]
        token_stream, sources = await pipeline.stream_query("test question")

        tokens: list[str] = []
        async for token in token_stream:
            tokens.append(token)

        assert len(tokens) > 0
        assert "".join(tokens).strip() == "Streaming response here"

    @pytest.mark.asyncio
    async def test_query_with_session_history(self) -> None:
        config = OracleConfig()
        store = FakeStore(results=[])
        llm = FakeLLM(response="Context-aware answer")

        session = ChatSession(name="test")
        session.add_message("user", "What is Python?")
        session.add_message("assistant", "Python is a programming language.")

        pipeline = RAGPipeline(config, store, llm)  # type: ignore[arg-type]
        result = await pipeline.query("Tell me more", session=session)

        assert result.response == "Context-aware answer"


# --- Helpers ---


async def _async_iter(items: list[str]) -> AsyncIterator[str]:
    """Create an async iterator from a list."""
    for item in items:
        yield item
