"""Tests for polymarket.py - Polymarket prediction market search."""

import json
from unittest.mock import Mock, patch

import pytest

from lib import polymarket

# === Helper Functions ===


def create_mock_event(
    event_id="evt-123",
    title="Test Event",
    slug="test-event",
    volume24hr=100000,
    liquidity=50000,
    closed=False,
    markets=None,
):
    """Create a mock Polymarket event."""
    if markets is None:
        markets = [create_mock_market()]
    
    return {
        "id": event_id,
        "title": title,
        "slug": slug,
        "active": True,
        "closed": closed,
        "volume24hr": volume24hr,
        "liquidity": liquidity,
        "markets": markets,
    }


def create_mock_market(
    market_id="mkt-123",
    question="Will X happen?",
    outcomes='["Yes", "No"]',
    prices='["0.60", "0.40"]',
    volume="100000",
    liquidity="50000",
    closed=False,
):
    """Create a mock Polymarket market."""
    return {
        "id": market_id,
        "question": question,
        "active": True,
        "closed": closed,
        "outcomes": outcomes,
        "outcomePrices": prices,
        "volume": volume,
        "liquidity": liquidity,
    }

# === Tests for _extract_core_subject() ===


def test_extract_core_subject_basic():
    """Test basic subject extraction."""
    result = polymarket._extract_core_subject("AI frameworks")
    assert result == "AI frameworks"


def test_extract_core_subject_with_time_prefix():
    """Test stripping time prefixes."""
    result = polymarket._extract_core_subject("last 7 days AI frameworks")
    assert result == "AI frameworks"


def test_extract_core_subject_with_question_prefix():
    """Test stripping question prefixes."""
    result = polymarket._extract_core_subject("what are people saying about Kanye West")
    assert result == "Kanye West"


def test_extract_core_subject_multiple_prefixes():
    """Test handling multiple prefix patterns."""
    result = polymarket._extract_core_subject("research AI models")
    assert result == "AI models"

# === Tests for _expand_queries() ===


def test_expand_queries_basic():
    """Test basic query expansion."""
    queries = polymarket._expand_queries("AI framework")
    
    # Should include core + individual words
    assert "AI framework" in queries or "ai framework" in queries
    assert len(queries) >= 2


def test_expand_queries_single_word():
    """Test query expansion with single word."""
    queries = polymarket._expand_queries("AI")
    
    # Single word, should just return that word
    assert len(queries) >= 1
    assert any("ai" in q.lower() for q in queries)


def test_expand_queries_filters_noise():
    """Test that low-signal tokens are filtered."""
    queries = polymarket._expand_queries("the AI framework")
    
    # "the" should be filtered from individual word expansions
    # But may appear in the full phrase
    assert len(queries) >= 1
    assert any("ai" in q.lower() or "framework" in q.lower() for q in queries)


def test_expand_queries_deduplication():
    """Test that duplicate queries are removed."""
    queries = polymarket._expand_queries("test test test")
    
    # Should dedupe
    assert len(queries) == len(set(q.lower() for q in queries))


def test_expand_queries_cap_at_six():
    """Test that query list is capped at 6."""
    long_topic = "one two three four five six seven eight"
    queries = polymarket._expand_queries(long_topic)
    
    assert len(queries) <= 6

# === Tests for _passes_topic_filter() ===


def test_passes_topic_filter_match():
    """Test that matching events pass the filter."""
    assert polymarket._passes_topic_filter("AI safety", "AI Safety Conference 2026") is True


def test_passes_topic_filter_no_match():
    """Test that non-matching events are filtered."""
    # "West" is a noise word, so "Kanye West" requires "Kanye" to match
    assert polymarket._passes_topic_filter("Kanye West", "NFC West Championship") is False


def test_passes_topic_filter_partial_match():
    """Test that at least one informative word must match."""
    # "AI" appears in both topic and title
    assert polymarket._passes_topic_filter("AI safety", "New AI Safety Conference") is True
    # "models" doesn't appear, should fail
    assert polymarket._passes_topic_filter("AI models", "New AI prediction") is False


def test_passes_topic_filter_all_noise_words():
    """Test that all-noise-word topics don't filter anything."""
    # "the west" has no informative words, so should pass everything
    result = polymarket._passes_topic_filter("the west", "Any title")
    assert result is True


def test_passes_topic_filter_empty_topic():
    """Test empty topic always passes."""
    assert polymarket._passes_topic_filter("", "Any title") is True


def test_passes_topic_filter_multi_word_requires_two_matches():
    """Topics with 3+ informative words require at least 2 to match."""
    # "Mill food recycler" has 3 informative words. "Meek Mill YC" only matches "mill".
    assert polymarket._passes_topic_filter(
        "Mill food recycler", "Meek Mill gets Y Combinator funding"
    ) is False


def test_passes_topic_filter_multi_word_passes_with_two_matches():
    """Topics with 3+ informative words pass when 2+ match."""
    assert polymarket._passes_topic_filter(
        "Sam Altman OpenAI", "Sam Altman CEO OpenAI"
    ) is True


def test_passes_topic_filter_two_word_still_needs_one():
    """Topics with 2 informative words still only need 1 match (existing behavior)."""
    assert polymarket._passes_topic_filter(
        "Kanye West", "Kanye divorce settlement"
    ) is True


def test_passes_topic_filter_multi_word_edge_exactly_three():
    """Topic with exactly 3 informative words, 1 match -> rejected."""
    assert polymarket._passes_topic_filter(
        "Tesla stock price", "Tesla quarterly earnings"
    ) is False  # only "tesla" matches, needs 2

# === Tests for _parse_outcome_prices() ===


def test_parse_outcome_prices_basic():
    """Test basic outcome price parsing."""
    market = {
        "outcomes": '["Yes", "No"]',
        "outcomePrices": '["0.65", "0.35"]',
    }
    
    result = polymarket._parse_outcome_prices(market)
    
    assert len(result) == 2
    assert result[0] == ("Yes", 0.65)
    assert result[1] == ("No", 0.35)


def test_parse_outcome_prices_already_parsed():
    """Test handling when outcomes/prices are already lists."""
    market = {
        "outcomes": ["Yes", "No"],
        "outcomePrices": ["0.70", "0.30"],
    }
    
    result = polymarket._parse_outcome_prices(market)
    
    assert len(result) == 2
    assert result[0][0] == "Yes"
    assert result[0][1] == 0.70


def test_parse_outcome_prices_multi_outcome():
    """Test multi-outcome markets."""
    market = {
        "outcomes": '["Team A", "Team B", "Team C", "Team D"]',
        "outcomePrices": '["0.40", "0.30", "0.20", "0.10"]',
    }
    
    result = polymarket._parse_outcome_prices(market)
    
    assert len(result) == 4
    assert result[0] == ("Team A", 0.40)
    assert result[3] == ("Team D", 0.10)


def test_parse_outcome_prices_missing_data():
    """Test handling of missing outcome prices."""
    market = {"outcomes": '["Yes"]'}
    
    result = polymarket._parse_outcome_prices(market)
    
    assert result == []


def test_parse_outcome_prices_invalid_json():
    """Test handling of invalid JSON."""
    market = {
        "outcomes": "not valid json",
        "outcomePrices": "also not valid",
    }
    
    result = polymarket._parse_outcome_prices(market)
    
    assert result == []

# === Tests for _format_price_movement() ===


def test_format_price_movement_one_day():
    """Test formatting one-day price movement."""
    market = {
        "oneDayPriceChange": 0.05,  # Up 5%
        "oneWeekPriceChange": 0.02,
        "oneMonthPriceChange": 0.01,
    }
    
    result = polymarket._format_price_movement(market)
    
    assert "up" in result
    assert "5.0%" in result
    assert "today" in result


def test_format_price_movement_negative():
    """Test formatting negative price movement."""
    market = {
        "oneDayPriceChange": -0.15,  # Down 15%
    }
    
    result = polymarket._format_price_movement(market)
    
    assert "down" in result
    assert "15.0%" in result


def test_format_price_movement_picks_largest():
    """Test that largest change is picked."""
    market = {
        "oneDayPriceChange": 0.02,
        "oneWeekPriceChange": 0.10,  # Largest
        "oneMonthPriceChange": 0.05,
    }
    
    result = polymarket._format_price_movement(market)
    
    assert "10.0%" in result
    assert "this week" in result


def test_format_price_movement_below_threshold():
    """Test that small changes return None."""
    market = {
        "oneDayPriceChange": 0.005,  # 0.5%, below 1% threshold
    }
    
    result = polymarket._format_price_movement(market)
    
    assert result is None


def test_format_price_movement_missing_data():
    """Test handling of missing price change data."""
    market = {}
    
    result = polymarket._format_price_movement(market)
    
    assert result is None

# === Tests for _shorten_question() ===


def test_shorten_question_will_pattern():
    """Test shortening 'Will X...' questions."""
    result = polymarket._shorten_question("Will Arizona win the NCAA Tournament?")
    
    assert result == "Arizona"


def test_shorten_question_complex_will():
    """Test shortening complex Will questions."""
    result = polymarket._shorten_question("Will Duke be a number 1 seed?")
    
    assert result == "Duke"


def test_shorten_question_no_pattern():
    """Test questions that don't match patterns."""
    result = polymarket._shorten_question("Arizona wins championship")
    
    # Should return original or truncated
    assert len(result) > 0


def test_shorten_question_long():
    """Test truncation of very long questions."""
    long_q = "A" * 100
    result = polymarket._shorten_question(long_q)

    assert len(result) <= 40


def test_shorten_question_fallback_strips_leading_article():
    """The truncation fallback must not keep a leading article like 'an' or 'the'.

    Without stripping, a question like 'an Anthropic Claude model scores...' yields
    a lead name of just 'an', which renders as the mangled 'an 19%' footer fragment.
    """
    result = polymarket._shorten_question(
        "an Anthropic Claude model scores at the top of the leaderboard this month"
    )
    lower = result.lower()
    assert not lower.startswith("a ")
    assert not lower.startswith("an ")
    assert not lower.startswith("the ")


def test_shorten_question_fallback_keeps_non_article_lead():
    """Stripping only removes a leading article, not the first informative word."""
    result = polymarket._shorten_question(
        "Anthropic ships a major Claude model update before the end of this month"
    )
    assert result.lower().startswith("anthropic")

# === Tests for search_polymarket() ===


def test_search_polymarket_result_cap():
    """Test that result cap configuration exists."""
    assert "quick" in polymarket.RESULT_CAP
    assert "default" in polymarket.RESULT_CAP
    assert "deep" in polymarket.RESULT_CAP
    
    # Deep should return more results
    assert polymarket.RESULT_CAP["deep"] >= polymarket.RESULT_CAP["quick"]


def test_search_polymarket_depth_config():
    """Test that depth configuration exists."""
    # Verify depth config
    assert "quick" in polymarket.DEPTH_CONFIG
    assert "default" in polymarket.DEPTH_CONFIG
    assert "deep" in polymarket.DEPTH_CONFIG
    
    # Quick should be least pages
    assert polymarket.DEPTH_CONFIG["quick"] <= polymarket.DEPTH_CONFIG["default"]


def test_search_polymarket_query_expansion():
    """Test that _expand_queries creates multiple queries."""
    queries = polymarket._expand_queries("AI framework")
    
    # Should expand to multiple queries
    assert len(queries) >= 2

@patch('lib.polymarket.http.post')


def test_search_polymarket_http_error_handling(mock_post):
    """Test graceful handling of HTTP errors."""
    from lib.http import HTTPError
    mock_post.side_effect = HTTPError("HTTP 429: Rate limit")
    
    result = polymarket.search_polymarket("test", "2026-01-01", "2026-01-31")
    
    # Should return structure with error
    assert "events" in result or "error" in result

# === Tests for parse_polymarket_response() ===


def test_parse_polymarket_response_basic():
    """Test basic response parsing."""
    response = {
        "events": [create_mock_event(
            title="Will AI surpass humans?",
            volume24hr=500000,
        )]
    }
    
    items = polymarket.parse_polymarket_response(response, topic="AI")
    
    assert len(items) >= 0
    # Items may be filtered by topic filter


def test_parse_polymarket_response_filters_closed():
    """Test that closed events are filtered."""
    response = {
        "events": [
            create_mock_event(closed=False),
            create_mock_event(closed=True),
        ]
    }
    
    items = polymarket.parse_polymarket_response(response)
    
    # Closed events should be filtered
    # (exact count depends on market filtering)
    assert isinstance(items, list)


def test_parse_polymarket_response_empty():
    """Test handling of empty response."""
    response = {"events": []}
    
    items = polymarket.parse_polymarket_response(response)
    
    assert items == []


def test_parse_polymarket_response_market_url():
    """Test that Polymarket URLs are generated."""
    response = {
        "events": [create_mock_event(
            slug="test-event-slug",
            title="Test Event"
        )]
    }
    
    items = polymarket.parse_polymarket_response(response, topic="test")
    
    if items:  # If not filtered
        assert "url" in items[0]
        assert "polymarket.com" in items[0]["url"]


def test_parse_polymarket_response_engagement():
    """Test that engagement/volume metrics are captured."""
    response = {
        "events": [create_mock_event(
            title="AI Event",
            volume24hr=250000,
            liquidity=100000,
        )]
    }
    
    items = polymarket.parse_polymarket_response(response, topic="AI")

    if items:  # If not filtered
        # Check for volume or liquidity fields
        assert "volume24hr" in items[0] or "liquidity" in items[0] or isinstance(items[0], dict)


def _claude_downtime_response():
    """An off-topic Polymarket event that mentions only the generic word 'Claude'."""
    return {
        "events": [
            create_mock_event(
                event_id="evt-noise",
                title="Will Claude go down 3-5 times in June?",
                slug="claude-downtime",
            ),
        ]
    }


def test_parse_polymarket_response_filters_noise_on_full_subquery():
    """A multi-word subquery filters off-topic 'Claude downtime' noise.

    'Claude Code subagents workflow' carries 3 informative words; the downtime
    title matches only one ('claude'), so the min-2 rule drops it.
    """
    items = polymarket.parse_polymarket_response(
        _claude_downtime_response(), topic="Claude Code subagents workflow"
    )
    assert items == []


def test_parse_polymarket_response_narrow_subquery_leaks_noise():
    """The SAME off-topic market leaks through a single-word subquery.

    'claude' has one informative word, so the min-match threshold drops to 1 and
    the downtime market passes. Because the pipeline previously fed the per-subquery
    search_query, filtering swung between these two outcomes across the fanout;
    keying off the stable original topic makes it consistent. This pair pins that
    threshold-by-word-count behavior the wiring fix depends on.
    """
    items = polymarket.parse_polymarket_response(
        _claude_downtime_response(), topic="claude"
    )
    assert len(items) == 1

# === Tests for engagement scoring ===


def test_engagement_with_volume():
    """Test engagement calculation with volume."""
    response = {
        "events": [create_mock_event(
            title="Test",
            volume24hr=500000,
        )]
    }
    
    items = polymarket.parse_polymarket_response(response, topic="test")
    
    if items:
        engagement = items[0].get("engagement", {})
        # volume24hr should be captured
        assert "volume24hr" in engagement or isinstance(engagement, dict)

# === Tests for noise-word query skipping ===


def test_expand_queries_skips_noise_words():
    """Noise words like 'west' should not become standalone queries."""
    queries = polymarket._expand_queries("kanye west")
    lowered = [q.lower() for q in queries]
    assert "kanye west" in lowered  # full phrase kept
    assert "kanye" in lowered       # informative word kept
    assert "west" not in lowered    # noise word skipped


def test_expand_queries_keeps_informative_words():
    """Non-noise words should still be expanded as standalone queries."""
    queries = polymarket._expand_queries("arizona basketball")
    lowered = [q.lower() for q in queries]
    assert "arizona" in lowered
    assert "basketball" in lowered


def test_expand_queries_all_noise_words_keeps_phrase():
    """If all words are noise, the full phrase is still searched."""
    queries = polymarket._expand_queries("north west")
    assert len(queries) >= 1
    assert any("north west" in q.lower() for q in queries)
    # Neither individual word should be a standalone query
    lowered = [q.lower() for q in queries]
    assert "north" not in lowered or "north west" in lowered  # only as part of phrase

# === Tests for per-item relevance floor ===


def test_per_item_relevance_floor_drops_zero_items():
    """Items with relevance 0.0 should be dropped even if best item is high."""
    # Simulate the filtering logic directly
    items = [
        {"relevance": 0.85, "title": "Kanye market"},
        {"relevance": 0.45, "title": "Related market"},
        {"relevance": 0.0, "title": "Golf noise"},
        {"relevance": 0.0, "title": "Cycling noise"},
    ]
    filtered = [i for i in items if i["relevance"] >= 0.10]
    assert len(filtered) == 2
    assert all(i["title"] != "Golf noise" for i in filtered)


def test_per_item_relevance_floor_keeps_borderline():
    """Items at exactly 0.10 should be kept."""
    items = [
        {"relevance": 0.85, "title": "Main market"},
        {"relevance": 0.10, "title": "Borderline market"},
        {"relevance": 0.09, "title": "Below floor"},
    ]
    filtered = [i for i in items if i["relevance"] >= 0.10]
    assert len(filtered) == 2
    assert filtered[1]["title"] == "Borderline market"


def test_per_item_relevance_floor_no_drops_when_all_high():
    """Nothing dropped when all items are above the floor."""
    items = [
        {"relevance": 0.85, "title": "A"},
        {"relevance": 0.50, "title": "B"},
        {"relevance": 0.30, "title": "C"},
    ]
    filtered = [i for i in items if i["relevance"] >= 0.10]
    assert len(filtered) == 3

if __name__ == "__main__":
    pytest.main([__file__, "-v"])
