from __future__ import annotations

import json
from collections.abc import Iterator
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

import pytest
from fastapi.testclient import TestClient

from session_hub_api.app import app
from session_hub_api.dependencies import get_query_api
from session_hub_core.exceptions import SessionNotFoundError
from session_hub_core.models import (
    HostInfo,
    HostStats,
    MessageRow,
    MessageSearchResult,
    SessionDetail,
    SessionRow,
    SessionSearchResult,
    Stats,
    ToolStats,
)

NOW = datetime.now(UTC)


def make_session_row(**overrides: Any) -> SessionRow:
    values: dict[str, Any] = {
        "host": "host-a",
        "tool": "devin",
        "session_id": "s1",
        "title": "Fix the bug",
        "working_directory": "/work/proj",
        "model_name": "gpt-5",
        "created_at": NOW,
        "last_activity_at": NOW,
        "message_count": 3,
        "hidden": False,
        "synced_at": NOW,
    }
    values.update(overrides)
    return SessionRow(**values)


def make_message_row(**overrides: Any) -> MessageRow:
    values: dict[str, Any] = {
        "node_id": "n1",
        "parent_node_id": None,
        "message_id": "m1",
        "role": "user",
        "content": "hello world",
        "created_at": NOW,
        "metadata": {"note": "x"},
    }
    values.update(overrides)
    return MessageRow(**values)


def make_session_detail(**overrides: Any) -> SessionDetail:
    values: dict[str, Any] = {
        "host": "host-a",
        "tool": "devin",
        "session_id": "s1",
        "title": "Fix the bug",
        "working_directory": "/work/proj",
        "model_name": "gpt-5",
        "created_at": NOW,
        "last_activity_at": NOW,
        "message_count": 3,
        "hidden": False,
        "synced_at": NOW,
        "messages": [make_message_row()],
        "metadata": {"repo": "proj"},
    }
    values.update(overrides)
    return SessionDetail(**values)


def make_stats(**overrides: Any) -> Stats:
    values: dict[str, Any] = {
        "total_sessions": 2,
        "total_messages": 5,
        "by_host": {"host-a": HostStats(sessions=2, messages=5, last_sync=NOW)},
        "by_tool": {"devin": ToolStats(sessions=2, messages=5)},
        "oldest_session": NOW,
        "newest_session": NOW,
    }
    values.update(overrides)
    return Stats(**values)


class FakeQueryAPI:
    def __init__(self) -> None:
        self.rows: list[SessionRow] = []
        self.detail: SessionDetail | None = None
        self.messages: list[MessageSearchResult] = []
        self.session_results: list[SessionSearchResult] = []
        self.stats: Stats | None = None
        self.hosts: list[HostInfo] = []
        self.probe_fails = False

    async def list_sessions(
        self,
        tool: str | None = None,
        host: str | None = None,
        working_directory: str | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> list[SessionRow]:
        return self.rows

    async def get_session(self, host: str, tool: str, session_id: str) -> SessionDetail:
        if self.detail is None:
            raise SessionNotFoundError(f"Session {host}/{tool}/{session_id} not found")
        return self.detail

    async def search_messages(
        self,
        query: str,
        tool: str | None = None,
        host: str | None = None,
        role: str | None = None,
        limit: int = 50,
        mode: str = "fts",
    ) -> list[MessageSearchResult]:
        return self.messages

    async def search_sessions(
        self,
        query: str,
        tool: str | None = None,
        host: str | None = None,
        limit: int = 50,
        mode: str = "fts",
    ) -> list[SessionRow]:
        return self.rows

    async def search_similar_sessions(
        self,
        query: str,
        tool: str | None = None,
        host: str | None = None,
        limit: int = 20,
    ) -> list[SessionSearchResult]:
        return self.session_results

    async def search_similar_messages(
        self,
        query: str,
        tool: str | None = None,
        host: str | None = None,
        role: str | None = None,
        limit: int = 50,
    ) -> list[MessageSearchResult]:
        return self.messages

    async def get_stats(self) -> Stats:
        assert self.stats is not None
        return self.stats

    async def get_hosts(self) -> list[HostInfo]:
        if self.probe_fails:
            raise RuntimeError("database is down")
        return self.hosts


client = TestClient(app)


@pytest.fixture()
def fake_api() -> Iterator[FakeQueryAPI]:
    api = FakeQueryAPI()
    app.dependency_overrides[get_query_api] = lambda: api
    yield api
    app.dependency_overrides.clear()


def test_list_sessions_returns_json(fake_api: FakeQueryAPI) -> None:
    fake_api.rows = [make_session_row()]
    response = client.get("/api/sessions")
    assert response.status_code == 200
    body = response.json()
    assert isinstance(body, list)
    assert body[0]["host"] == "host-a"
    assert body[0]["title"] == "Fix the bug"
    assert body[0]["message_count"] == 3


def test_list_sessions_passes_filters_and_pagination(fake_api: FakeQueryAPI) -> None:
    response = client.get(
        "/api/sessions", params={"tool": "devin", "host": "host-a", "limit": 10, "offset": 5}
    )
    assert response.status_code == 200


def test_get_session_returns_detail(fake_api: FakeQueryAPI) -> None:
    fake_api.detail = make_session_detail()
    response = client.get("/api/sessions/host-a/devin/s1")
    assert response.status_code == 200
    body = response.json()
    assert body["host"] == "host-a"
    assert body["messages"][0]["content"] == "hello world"


def test_get_session_unknown_returns_404(fake_api: FakeQueryAPI) -> None:
    response = client.get("/api/sessions/host-a/devin/does-not-exist")
    assert response.status_code == 404
    assert "not found" in response.json()["detail"]


def test_get_session_messages_returns_json(fake_api: FakeQueryAPI) -> None:
    fake_api.detail = make_session_detail()
    response = client.get("/api/sessions/host-a/devin/s1/messages")
    assert response.status_code == 200
    body = response.json()
    assert isinstance(body, list)
    assert body[0]["content"] == "hello world"
    assert body[0]["role"] == "user"


def test_get_session_messages_unknown_returns_404(fake_api: FakeQueryAPI) -> None:
    response = client.get("/api/sessions/host-a/devin/does-not-exist/messages")
    assert response.status_code == 404
    assert "not found" in response.json()["detail"]


def test_search_messages_returns_results(fake_api: FakeQueryAPI) -> None:
    result = MessageSearchResult(
        **make_message_row().model_dump(),
        session_id="s1",
        session_title="Fix the bug",
        session_host="host-a",
        session_tool="devin",
        session_working_directory="/work/proj",
        rank=0.5,
    )
    fake_api.messages = [result]
    response = client.get("/api/search", params={"query": "hello"})
    assert response.status_code == 200
    body = response.json()
    assert isinstance(body, list)
    assert body[0]["session_title"] == "Fix the bug"
    assert body[0]["session_id"] == "s1"
    assert body[0]["rank"] == 0.5


def test_search_sessions_returns_results(fake_api: FakeQueryAPI) -> None:
    fake_api.rows = [make_session_row()]
    response = client.get("/api/search/sessions", params={"query": "bug"})
    assert response.status_code == 200
    assert isinstance(response.json(), list)


def test_search_similar_sessions_returns_results(fake_api: FakeQueryAPI) -> None:
    result = SessionSearchResult(
        **make_session_row().model_dump(),
        similarity=0.9,
        matched_text="Fix the bug",
    )
    fake_api.session_results = [result]
    response = client.get("/api/search/similar/sessions", params={"query": "bug"})
    assert response.status_code == 200
    body = response.json()
    assert body[0]["similarity"] == 0.9


def test_search_similar_messages_returns_results(fake_api: FakeQueryAPI) -> None:
    fake_api.messages = [
        MessageSearchResult(
            **make_message_row().model_dump(),
            session_id="s1",
            session_title="t",
            session_host="h",
            session_tool="d",
            session_working_directory="/w",
            rank=0.8,
        )
    ]
    response = client.get("/api/search/similar/messages", params={"query": "hello"})
    assert response.status_code == 200
    assert response.json()[0]["rank"] == 0.8


def test_stats_returns_json(fake_api: FakeQueryAPI) -> None:
    fake_api.stats = make_stats()
    response = client.get("/api/stats")
    assert response.status_code == 200
    body = response.json()
    assert body["total_sessions"] == 2
    assert body["by_tool"]["devin"]["messages"] == 5


def test_hosts_returns_json(fake_api: FakeQueryAPI) -> None:
    fake_api.hosts = [HostInfo(host="host-a", last_sync=NOW, session_count=2, message_count=5)]
    response = client.get("/api/hosts")
    assert response.status_code == 200
    body = response.json()
    assert body[0]["host"] == "host-a"
    assert body[0]["session_count"] == 2


def test_health_ok(fake_api: FakeQueryAPI) -> None:
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


def test_health_unreachable_returns_503(fake_api: FakeQueryAPI) -> None:
    fake_api.probe_fails = True
    response = client.get("/health")
    assert response.status_code == 503


def test_health_without_db_returns_503() -> None:
    app.dependency_overrides.clear()
    try:
        response = client.get("/health")
        assert response.status_code == 503
        assert response.json() == {"detail": "database not configured"}
    finally:
        app.dependency_overrides.clear()


def test_db_endpoints_without_db_return_503() -> None:
    app.dependency_overrides.clear()
    try:
        response = client.get("/api/sessions")
        assert response.status_code == 503
        assert response.json() == {"detail": "database not configured"}
    finally:
        app.dependency_overrides.clear()


class FakeEmbeddingService:
    def __init__(self, db_config: Any, emb_config: Any) -> None:
        self.db_config = db_config
        self.emb_config = emb_config

    async def backfill_missing(
        self,
        batch_size: int = 100,
        only_sessions: bool = False,
        only_messages: bool = False,
    ) -> int:
        return 7

    async def close(self) -> None:
        pass


def test_backfill_embeddings_without_api_key_returns_503(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    response = client.post("/api/embeddings/backfill")
    assert response.status_code == 503
    assert response.json() == {"detail": "OPENROUTER_API_KEY not configured"}


def test_backfill_embeddings_returns_count(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    import session_hub_api.routes.embeddings as embeddings_routes

    monkeypatch.setenv("OPENROUTER_API_KEY", "test-key")
    monkeypatch.setenv(
        "SESSION_HUB_DSN", "postgresql+asyncpg://user:pass@localhost:5432/session_hub"
    )
    monkeypatch.setattr(embeddings_routes, "EmbeddingService", FakeEmbeddingService)
    response = client.post("/api/embeddings/backfill", params={"only_sessions": "true"})
    assert response.status_code == 200
    assert response.json() == {"backfilled": 7}


def test_contract_paths_match() -> None:
    contract_file = Path(__file__).resolve().parents[3] / "generated/openapi/openapi.json"
    if not contract_file.exists():
        pytest.skip("generated openapi.json not present")
    contract = json.loads(contract_file.read_text())
    app_paths = set(app.openapi()["paths"])
    contract_paths = set(contract["paths"])
    assert app_paths == contract_paths
