"""Tests for X-Wines enrichment service."""

import io
import os
import uuid
from unittest.mock import AsyncMock, patch

import pytest
from httpx import AsyncClient

from winebox.config import settings
from winebox.models import XWinesWine, Wine
from winebox.services.xwines_enrichment import (
    _score_candidate,
    _tokenize,
    enrich_parsed_with_xwines,
    parse_xwines_grapes,
)

has_anthropic_key = bool(
    settings.anthropic_api_key
    or os.getenv("WINEBOX_ANTHROPIC_API_KEY")
    or os.getenv("ANTHROPIC_API_KEY")
)
skip_no_key = pytest.mark.skipif(
    not has_anthropic_key,
    reason="No Anthropic API key available",
)


# ---------------------------------------------------------------------------
# parse_xwines_grapes
# ---------------------------------------------------------------------------


def test_parse_xwines_grapes_python_list() -> None:
    """Python-style single-quoted list is parsed correctly."""
    result = parse_xwines_grapes("['Merlot', 'Cabernet Sauvignon']")
    assert result == "Merlot, Cabernet Sauvignon"


def test_parse_xwines_grapes_json_list() -> None:
    """JSON-style double-quoted list is parsed correctly."""
    result = parse_xwines_grapes('["Chardonnay", "Pinot Grigio"]')
    assert result == "Chardonnay, Pinot Grigio"


def test_parse_xwines_grapes_single_item() -> None:
    """Single-element list is parsed correctly."""
    result = parse_xwines_grapes("['Syrah']")
    assert result == "Syrah"


def test_parse_xwines_grapes_empty_string() -> None:
    """Empty string returns None."""
    assert parse_xwines_grapes("") is None


def test_parse_xwines_grapes_none() -> None:
    """None returns None."""
    assert parse_xwines_grapes(None) is None


def test_parse_xwines_grapes_plain_string() -> None:
    """Plain string (not a list) is returned as-is."""
    result = parse_xwines_grapes("Tempranillo")
    assert result == "Tempranillo"


def test_parse_xwines_grapes_empty_list() -> None:
    """Empty list returns None."""
    assert parse_xwines_grapes("[]") is None


# ---------------------------------------------------------------------------
# _tokenize
# ---------------------------------------------------------------------------


def test_tokenize_composite_name() -> None:
    """Composite CSV-style name is split into clean tokens."""
    result = _tokenize("Chateau Lynch-Bages, Pauillac, Bordeaux")
    assert result == ["Chateau", "Lynch-Bages", "Pauillac", "Bordeaux"]


def test_tokenize_strips_trailing_punctuation() -> None:
    """Trailing punctuation is removed from each token."""
    result = _tokenize("Lynch-Bages, Pauillac;")
    assert result == ["Lynch-Bages", "Pauillac"]


def test_tokenize_simple_name() -> None:
    """Simple space-separated name returns individual words."""
    result = _tokenize("Chateau Margaux")
    assert result == ["Chateau", "Margaux"]


def test_tokenize_empty() -> None:
    """Empty string returns empty list."""
    assert _tokenize("") == []


# ---------------------------------------------------------------------------
# _score_candidate
# ---------------------------------------------------------------------------


def _make_xwines_wine(**kwargs: object) -> XWinesWine:
    """Create an XWinesWine without requiring Beanie DB initialization."""
    defaults = {
        "xwines_id": 0,
        "name": "",
        "wine_type": "Red",
        "winery_name": "",
        "avg_rating": 0.0,
        "rating_count": 0,
    }
    defaults.update(kwargs)
    return XWinesWine.model_construct(**defaults)


def test_score_composite_name_matches_winery() -> None:
    """Composite name scores >= 10 when winery appears in query."""
    candidate = _make_xwines_wine(
        xwines_id=999,
        name="Pauillac (Grand Cru Classe)",
        winery_name="Chateau Lynch-Bages",
        avg_rating=4.0,
        rating_count=7000,
    )
    score = _score_candidate("Chateau Lynch-Bages, Pauillac, Bordeaux", candidate)
    assert score >= 10, f"Expected score >= 10 but got {score}"


def test_score_exact_name_still_high() -> None:
    """Exact name match still produces a high score."""
    candidate = _make_xwines_wine(
        xwines_id=100,
        name="Chateau Margaux",
        winery_name="Chateau Margaux",
        avg_rating=4.5,
        rating_count=1000,
    )
    score = _score_candidate("Chateau Margaux", candidate)
    assert score >= 100, f"Expected score >= 100 but got {score}"


def test_score_no_overlap_is_low() -> None:
    """Completely unrelated candidate scores near zero."""
    candidate = _make_xwines_wine(
        xwines_id=1,
        name="Yellow Tail Shiraz",
        winery_name="Yellow Tail",
        avg_rating=3.0,
        rating_count=50,
    )
    score = _score_candidate("Chateau Lynch-Bages, Pauillac, Bordeaux", candidate)
    assert score < 10, f"Expected score < 10 but got {score}"


# ---------------------------------------------------------------------------
# _match_with_claude_or_score — Claude integration in batch matching
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
@patch("winebox.services.xwines_enrichment.settings")
async def test_batch_match_falls_back_when_claude_disabled(mock_settings) -> None:
    """When use_claude_matching is False, _match_with_claude_or_score returns None."""
    from winebox.services.xwines_enrichment import _match_with_claude_or_score

    mock_settings.use_claude_matching = False
    result = await _match_with_claude_or_score(
        ["Wine A"],
        [_make_xwines_wine(name="Wine A", xwines_id=1)],
    )
    assert result is None


@skip_no_key
@pytest.mark.asyncio
async def test_batch_match_uses_claude_when_enabled() -> None:
    """When Claude matching is enabled, it picks the right candidate."""
    from winebox.services.xwines_enrichment import _match_with_claude_or_score

    correct = _make_xwines_wine(
        name="Chateau Lynch-Bages",
        winery_name="Chateau Lynch-Bages",
        xwines_id=999,
        rating_count=5000,
    )
    wrong = _make_xwines_wine(
        name="Yellow Tail Shiraz",
        winery_name="Yellow Tail",
        xwines_id=1,
        rating_count=100,
    )

    result = await _match_with_claude_or_score(
        ["Lynch-Bages, Pauillac"],
        [correct, wrong],
    )

    # Should return a result (not None = not disabled)
    # and pick the correct candidate
    assert result is not None
    assert result["Lynch-Bages, Pauillac"] == correct


# ---------------------------------------------------------------------------
# enrich_parsed_with_xwines — integration tests (require MongoDB)
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_enrich_fills_gaps(init_test_db) -> None:
    """Empty parsed fields are filled from X-Wines match."""
    uid = uuid.uuid4().hex[:8]
    wine_name = f"Chateau Margaux {uid}"
    xwines_id = hash(uid) % 1_000_000

    # Insert an X-Wines wine with unique name to avoid shared-DB collisions
    xwine = XWinesWine(
        xwines_id=xwines_id,
        name=wine_name,
        wine_type="Red",
        winery_name=wine_name,
        country="France",
        country_code="FR",
        region_name="Bordeaux",
        grapes="['Cabernet Sauvignon', 'Merlot']",
        abv=13.5,
        avg_rating=4.5,
        rating_count=1000,
    )
    await xwine.insert()

    # Parsed data with only name detected
    parsed = {"name": wine_name}

    result = await enrich_parsed_with_xwines(parsed)

    assert result["name"] == wine_name
    assert result["winery"] == wine_name
    assert result["grape_variety"] == "Cabernet Sauvignon, Merlot"
    assert result["region"] == "Bordeaux"
    assert result["country"] == "France"
    assert result["alcohol_percentage"] == 13.5
    assert result["wine_type"] == "red"  # Lowercased
    assert result["xwines_id"] == xwines_id


@pytest.mark.asyncio
async def test_enrich_preserves_existing(init_test_db) -> None:
    """Non-empty parsed fields are NOT overwritten by X-Wines data."""
    uid = uuid.uuid4().hex[:8]
    wine_name = f"Opus One {uid}"
    xwines_id = hash(uid) % 1_000_000 + 1_000_000

    xwine = XWinesWine(
        xwines_id=xwines_id,
        name=wine_name,
        wine_type="Red",
        winery_name=f"Opus One Winery {uid}",
        country="United States",
        country_code="US",
        region_name="Napa Valley",
        grapes="['Cabernet Sauvignon']",
        abv=14.5,
        avg_rating=4.7,
        rating_count=500,
    )
    await xwine.insert()

    # Parsed data with some fields already filled by OCR
    parsed = {
        "name": wine_name,
        "winery": "My Custom Winery",  # Should NOT be overwritten
        "country": "USA",  # Should NOT be overwritten
        "grape_variety": "Pinot Noir",  # Should NOT be overwritten
    }

    result = await enrich_parsed_with_xwines(parsed)

    assert result["winery"] == "My Custom Winery"
    assert result["country"] == "USA"
    assert result["grape_variety"] == "Pinot Noir"
    # But empty fields should be filled
    assert result["region"] == "Napa Valley"
    assert result["alcohol_percentage"] == 14.5
    assert result["wine_type"] == "red"
    assert result["xwines_id"] == xwines_id


@pytest.mark.asyncio
async def test_enrich_no_match(init_test_db) -> None:
    """Returns parsed unchanged when no X-Wines match found."""
    parsed = {
        "name": "Completely Unknown Wine XYZZY",
        "winery": "Some Winery",
    }

    result = await enrich_parsed_with_xwines(parsed)

    assert result == parsed
    assert "xwines_id" not in result


@pytest.mark.asyncio
async def test_enrich_adds_xwines_id(init_test_db) -> None:
    """xwines_id is added when a match is found."""
    uid = uuid.uuid4().hex[:8]
    wine_name = f"Merlot Reserve {uid}"
    xwines_id = hash(uid) % 1_000_000 + 2_000_000

    xwine = XWinesWine(
        xwines_id=xwines_id,
        name=wine_name,
        wine_type="Red",
        winery_name=f"Test Winery {uid}",
        avg_rating=3.5,
        rating_count=100,
    )
    await xwine.insert()

    parsed = {"name": wine_name}
    result = await enrich_parsed_with_xwines(parsed)

    assert result["xwines_id"] == xwines_id


@pytest.mark.asyncio
async def test_enrich_short_name_skipped(init_test_db) -> None:
    """Names shorter than 2 chars are skipped."""
    parsed = {"name": "A"}
    result = await enrich_parsed_with_xwines(parsed)
    assert result == parsed
    assert "xwines_id" not in result


@pytest.mark.asyncio
async def test_enrich_no_name_skipped(init_test_db) -> None:
    """Missing name is skipped."""
    parsed = {"winery": "Some Winery"}
    result = await enrich_parsed_with_xwines(parsed)
    assert result == parsed


# ---------------------------------------------------------------------------
# Scan endpoint enrichment — integration tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_scan_returns_xwines_fields(client: AsyncClient, init_test_db, sample_image_bytes: bytes) -> None:
    """Scan endpoint includes wine_type and xwines_id from enrichment."""
    uid = uuid.uuid4().hex[:8]
    wine_name = f"Test Scan Wine {uid}"
    xwines_id = hash(uid) % 1_000_000 + 3_000_000

    # Insert an X-Wines wine that will match the OCR result
    xwine = XWinesWine(
        xwines_id=xwines_id,
        name=wine_name,
        wine_type="White",
        winery_name=f"Scan Winery {uid}",
        country="Italy",
        country_code="IT",
        region_name="Tuscany",
        grapes="['Trebbiano']",
        abv=12.0,
        avg_rating=4.0,
        rating_count=50,
    )
    await xwine.insert()

    # Mock OCR to return a name that matches our X-Wines entry
    with patch("winebox.routers.wines.scan.vision_service") as mock_vision, \
         patch("winebox.routers.wines.scan.ocr_service") as mock_ocr, \
         patch("winebox.routers.wines.scan.wine_parser") as mock_parser:
        mock_vision.is_available.return_value = False
        mock_ocr.extract_text_from_bytes = AsyncMock(return_value=wine_name)
        mock_parser.parse.return_value = {"name": wine_name}

        response = await client.post(
            "/api/wines/scan",
            files={"front_label": ("label.png", io.BytesIO(sample_image_bytes), "image/png")},
        )

    assert response.status_code == 200
    data = response.json()
    parsed = data["parsed"]

    assert parsed["wine_type"] == "white"
    assert parsed["xwines_id"] == xwines_id
    assert parsed["region"] == "Tuscany"
    assert parsed["grape_variety"] == "Trebbiano"


# ---------------------------------------------------------------------------
# Check-in endpoint wine_type_id — integration tests
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_checkin_accepts_wine_type_id(client: AsyncClient, init_test_db, sample_image_bytes: bytes) -> None:
    """wine_type_id form field is saved on the Wine document."""
    # Mock vision to not be available, and OCR to return minimal data
    with patch("winebox.routers.wines.checkin.vision_service") as mock_vision, \
         patch("winebox.routers.wines.checkin.ocr_service") as mock_ocr, \
         patch("winebox.routers.wines.checkin.wine_parser") as mock_parser:
        mock_vision.is_available.return_value = False
        mock_ocr.extract_text.return_value = "Test Wine"
        mock_parser.parse.return_value = {"name": "Test Wine"}

        response = await client.post(
            "/api/wines/checkin",
            data={
                "name": "Checkin Wine Type Test",
                "wine_type_id": "red",
                "quantity": "1",
            },
            files={"front_label": ("label.png", io.BytesIO(sample_image_bytes), "image/png")},
        )

    assert response.status_code == 201
    wine_data = response.json()
    assert wine_data["name"] == "Checkin Wine Type Test"
    assert wine_data["wine_type_id"] == "red"

    # Verify in database
    wine = await Wine.find_one({"name": "Checkin Wine Type Test"})
    assert wine is not None
    assert wine.wine_type_id == "red"
