"""Tests for GET /api/search/async (fire-and-forget search + SSE streaming).

Verifies:
  - Returns HTTP 202 with session_id and status: "queued"
  - session_id is registered in _update_queues immediately
  - Actual scraping is not performed (mocked out)
  - Empty query path returns a completed session with done event
"""
from __future__ import annotations

import os
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient

# ── Fixtures ──────────────────────────────────────────────────────────────────

@pytest.fixture
def client(tmp_path):
    """TestClient with a fresh tmp DB.  Must set SNIPE_DB *before* importing app."""
    os.environ["SNIPE_DB"] = str(tmp_path / "snipe.db")
    from api.main import app
    return TestClient(app, raise_server_exceptions=False)


def _make_mock_listing():
    """Return a minimal mock listing object that satisfies the search pipeline."""
    m = MagicMock()
    m.platform_listing_id = "123456789"
    m.seller_platform_id = "test_seller"
    m.title = "Test GPU"
    m.price = 100.0
    m.currency = "USD"
    m.condition = "Used"
    m.url = "https://www.ebay.com/itm/123456789"
    m.photo_urls = []
    m.listing_age_days = 5
    m.buying_format = "fixed_price"
    m.ends_at = None
    m.fetched_at = None
    m.trust_score_id = None
    m.id = 1
    m.category_name = None
    return m


# ── Core contract tests ───────────────────────────────────────────────────────

def test_async_search_returns_202(client):
    """GET /api/search/async?q=... returns HTTP 202 with session_id and status."""
    with (
        patch("api.main._make_adapter") as mock_adapter_factory,
        patch("api.main._trigger_scraper_enrichment"),
        patch("api.main.TrustScorer") as mock_scorer_cls,
    ):
        mock_adapter = MagicMock()
        mock_adapter.search.return_value = []
        mock_adapter.get_completed_sales.return_value = None
        mock_adapter_factory.return_value = mock_adapter

        mock_scorer = MagicMock()
        mock_scorer.score_batch.return_value = []
        mock_scorer_cls.return_value = mock_scorer

        resp = client.get("/api/search/async?q=test+gpu")

    assert resp.status_code == 202
    data = resp.json()
    assert "session_id" in data
    assert data["status"] == "queued"
    assert isinstance(data["session_id"], str)
    assert len(data["session_id"]) > 0


def test_async_search_registers_session_id(client):
    """session_id returned by 202 response must appear in _update_queues immediately."""
    with (
        patch("api.main._make_adapter") as mock_adapter_factory,
        patch("api.main._trigger_scraper_enrichment"),
        patch("api.main.TrustScorer") as mock_scorer_cls,
    ):
        mock_adapter = MagicMock()
        mock_adapter.search.return_value = []
        mock_adapter.get_completed_sales.return_value = None
        mock_adapter_factory.return_value = mock_adapter

        mock_scorer = MagicMock()
        mock_scorer.score_batch.return_value = []
        mock_scorer_cls.return_value = mock_scorer

        resp = client.get("/api/search/async?q=test+gpu")

    assert resp.status_code == 202
    session_id = resp.json()["session_id"]

    # The queue must be registered so the SSE endpoint can open it.
    from api.main import _update_queues
    assert session_id in _update_queues


def test_async_search_empty_query(client):
    """Empty query returns 202 with a pre-loaded done sentinel, no scraping needed."""
    resp = client.get("/api/search/async?q=")
    assert resp.status_code == 202
    data = resp.json()
    assert data["status"] == "queued"
    assert "session_id" in data


    from api.main import _update_queues
    sid = data["session_id"]
    assert sid in _update_queues
    q = _update_queues[sid]
    # First item should be the empty listings event
    first = q.get_nowait()
    assert first is not None
    assert first["type"] == "listings"
    assert first["listings"] == []
    # Second item should be the sentinel
    sentinel = q.get_nowait()
    assert sentinel is None


def test_async_search_no_real_chromium(client):
    """Async search endpoint must not launch real Chromium in tests.

    Verifies that the background scraper is submitted to the executor but the
    adapter factory is patched — no real Playwright/Xvfb process is spawned.
    Uses a broad patch on Store to avoid sqlite3 DB path issues in the thread pool.
    """
    import threading
    scrape_called = threading.Event()

    def _fake_search(query, filters):
        scrape_called.set()
        return []

    with (
        patch("api.main._make_adapter") as mock_adapter_factory,
        patch("api.main._trigger_scraper_enrichment"),
        patch("api.main.TrustScorer") as mock_scorer_cls,
        patch("api.main.Store") as mock_store_cls,
    ):
        mock_adapter = MagicMock()
        mock_adapter.search.side_effect = _fake_search
        mock_adapter.get_completed_sales.return_value = None
        mock_adapter_factory.return_value = mock_adapter

        mock_scorer = MagicMock()
        mock_scorer.score_batch.return_value = []
        mock_scorer_cls.return_value = mock_scorer

        mock_store = MagicMock()
        mock_store.get_listings_staged.return_value = {}
        mock_store.refresh_seller_categories.return_value = 0
        mock_store.save_listings.return_value = None
        mock_store.save_trust_scores.return_value = None
        mock_store.get_market_comp.return_value = None
        mock_store.get_seller.return_value = None
        mock_store.get_user_preference.return_value = None
        mock_store_cls.return_value = mock_store

        resp = client.get("/api/search/async?q=rtx+3080")

    assert resp.status_code == 202
    # Give the background worker a moment to run (it's in a thread pool)
    scrape_called.wait(timeout=5.0)
    # If we get here without a real Playwright process, the test passes.
    assert scrape_called.is_set(), "Background search worker never ran"


def test_async_search_query_params_forwarded(client):
    """All filter params accepted by /api/search are also accepted here."""
    with (
        patch("api.main._make_adapter") as mock_adapter_factory,
        patch("api.main._trigger_scraper_enrichment"),
        patch("api.main.TrustScorer") as mock_scorer_cls,
    ):
        mock_adapter = MagicMock()
        mock_adapter.search.return_value = []
        mock_adapter.get_completed_sales.return_value = None
        mock_adapter_factory.return_value = mock_adapter

        mock_scorer = MagicMock()
        mock_scorer.score_batch.return_value = []
        mock_scorer_cls.return_value = mock_scorer

        resp = client.get(
            "/api/search/async"
            "?q=rtx+3080"
            "&max_price=400"
            "&min_price=100"
            "&pages=2"
            "&must_include=rtx,3080"
            "&must_include_mode=all"
            "&must_exclude=mining"
            "&category_id=27386"
            "&adapter=auto"
        )

    assert resp.status_code == 202


def test_async_search_session_id_is_uuid(client):
    """session_id must be a valid UUID v4 string."""
    import uuid as _uuid

    with (
        patch("api.main._make_adapter") as mock_adapter_factory,
        patch("api.main._trigger_scraper_enrichment"),
        patch("api.main.TrustScorer") as mock_scorer_cls,
    ):
        mock_adapter = MagicMock()
        mock_adapter.search.return_value = []
        mock_adapter.get_completed_sales.return_value = None
        mock_adapter_factory.return_value = mock_adapter

        mock_scorer = MagicMock()
        mock_scorer.score_batch.return_value = []
        mock_scorer_cls.return_value = mock_scorer

        resp = client.get("/api/search/async?q=test")

    assert resp.status_code == 202
    sid = resp.json()["session_id"]
    # Should not raise if it's a valid UUID
    parsed = _uuid.UUID(sid)
    assert str(parsed) == sid
