"""Tests for resolution sniping (python/horizon/sniper.py)."""

import time
from unittest.mock import patch, MagicMock

import pytest


# ---------------------------------------------------------------------------
# Config defaults
# ---------------------------------------------------------------------------


def test_sniper_config_defaults():
    from horizon.sniper import SniperConfig

    cfg = SniperConfig()
    assert cfg.provider == "anthropic"
    assert cfg.model == ""
    assert cfg.news_sources == []
    assert cfg.exa_query == ""
    assert cfg.tavily_query == ""
    assert cfg.confidence_threshold == 0.85
    assert cfg.max_position_size == 50.0
    assert cfg.price_offset == 0.02
    assert cfg.scan_interval_cycles == 5
    assert cfg.cache_ttl == 60.0


def test_sniper_config_custom():
    from horizon.sniper import SniperConfig

    cfg = SniperConfig(
        provider="openai",
        confidence_threshold=0.90,
        max_position_size=100.0,
    )
    assert cfg.provider == "openai"
    assert cfg.confidence_threshold == 0.90
    assert cfg.max_position_size == 100.0


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


def test_resolution_signal_fields():
    from horizon.sniper import ResolutionSignal

    signal = ResolutionSignal(
        market_id="m1",
        market_title="Will X happen?",
        resolved=True,
        confidence=0.95,
        resolution_side="yes",
        evidence=["Breaking: X happened"],
        reasoning="Multiple sources confirm",
        timestamp=1700000000.0,
    )
    assert signal.market_id == "m1"
    assert signal.resolved is True
    assert signal.confidence == 0.95
    assert signal.resolution_side == "yes"
    assert len(signal.evidence) == 1


def test_resolution_signal_frozen():
    from horizon.sniper import ResolutionSignal

    signal = ResolutionSignal(
        market_id="m1", market_title="Test", resolved=False,
        confidence=0.0, resolution_side="yes", evidence=[],
        reasoning="", timestamp=0.0,
    )
    with pytest.raises(AttributeError):
        signal.resolved = True


# ---------------------------------------------------------------------------
# Prompt building
# ---------------------------------------------------------------------------


def test_build_sniper_prompt():
    from horizon.sniper import _build_sniper_prompt
    from horizon.llm import NewsItem

    news = [
        NewsItem("Event happened!", "Details here", "https://a.com", 1700000000.0, "rss"),
    ]
    prompt = _build_sniper_prompt("Will X happen?", news)
    assert "Will X happen?" in prompt
    assert "Event happened!" in prompt
    assert "resolved" in prompt.lower()


def test_build_sniper_prompt_no_news():
    from horizon.sniper import _build_sniper_prompt

    prompt = _build_sniper_prompt("Test market", [])
    assert "No recent news" in prompt


# ---------------------------------------------------------------------------
# Response parsing
# ---------------------------------------------------------------------------


def test_parse_sniper_response_resolved():
    from horizon.sniper import _parse_sniper_response

    text = '''{
        "resolved": true,
        "confidence": 0.95,
        "resolution_side": "yes",
        "evidence": ["Source confirms event"],
        "reasoning": "Official confirmation"
    }'''
    signal = _parse_sniper_response(text, "m1", "Test")
    assert signal.resolved is True
    assert signal.confidence == 0.95
    assert signal.resolution_side == "yes"
    assert len(signal.evidence) == 1


def test_parse_sniper_response_not_resolved():
    from horizon.sniper import _parse_sniper_response

    text = '{"resolved": false, "confidence": 0.3, "resolution_side": "no", "evidence": [], "reasoning": "No evidence"}'
    signal = _parse_sniper_response(text, "m1", "Test")
    assert signal.resolved is False
    assert signal.confidence == 0.3


def test_parse_sniper_response_invalid():
    from horizon.sniper import _parse_sniper_response

    signal = _parse_sniper_response("garbage", "m1", "Test")
    assert signal.resolved is False
    assert signal.confidence == 0.0


def test_parse_sniper_response_clamps_confidence():
    from horizon.sniper import _parse_sniper_response

    text = '{"resolved": true, "confidence": 1.5, "resolution_side": "yes"}'
    signal = _parse_sniper_response(text, "m1", "Test")
    assert signal.confidence == 1.0


# ---------------------------------------------------------------------------
# Pipeline factory
# ---------------------------------------------------------------------------


def test_resolution_sniper_returns_callable():
    from horizon.sniper import resolution_sniper

    fn = resolution_sniper()
    assert callable(fn)
    assert fn.__name__ == "resolution_sniper"


def test_resolution_sniper_returns_none_below_threshold():
    from horizon.sniper import resolution_sniper, SniperConfig, _signal_cache, ResolutionSignal

    config = SniperConfig(
        confidence_threshold=0.85,
        scan_interval_cycles=1,
    )
    fn = resolution_sniper(config=config)

    # Pre-populate cache with low confidence signal
    _signal_cache["test-market"] = (time.time(), ResolutionSignal(
        market_id="test-market",
        market_title="Test",
        resolved=True,
        confidence=0.5,  # Below threshold
        resolution_side="yes",
        evidence=[],
        reasoning="Low confidence",
        timestamp=time.time(),
    ))

    # Mock LLM to return low confidence
    with patch("horizon.sniper._call_sniper_llm") as mock_llm, \
         patch("horizon.llm._fetch_news_rss", return_value=[]):
        mock_llm.return_value = '{"resolved": true, "confidence": 0.5, "resolution_side": "yes", "evidence": [], "reasoning": "maybe"}'

        ctx = MagicMock()
        ctx.market.id = "test-market"
        ctx.market.name = "Test"
        ctx.params = {}

        result = fn(ctx)
        # Should return None (below threshold)
        assert result is None

    # Clean up
    _signal_cache.pop("test-market", None)


def test_resolution_sniper_respects_interval():
    from horizon.sniper import resolution_sniper, SniperConfig

    config = SniperConfig(scan_interval_cycles=5)
    fn = resolution_sniper(config=config)

    ctx = MagicMock()
    ctx.market.id = "interval-test"
    ctx.market.name = "Test"
    ctx.params = {}

    # Cycles 1-4 should return None (not at interval)
    for _ in range(4):
        result = fn(ctx)
        assert result is None


# ---------------------------------------------------------------------------
# Config exa/tavily fields
# ---------------------------------------------------------------------------


def test_sniper_config_exa_tavily():
    from horizon.sniper import SniperConfig

    cfg = SniperConfig(exa_query="election results", tavily_query="vote count")
    assert cfg.exa_query == "election results"
    assert cfg.tavily_query == "vote count"


# ---------------------------------------------------------------------------
# LLM delegation
# ---------------------------------------------------------------------------


def test_call_sniper_llm_delegates():
    """_call_sniper_llm delegates to _call_llm_raw from horizon.llm."""
    from horizon.sniper import _call_sniper_llm, SniperConfig

    config = SniperConfig(provider="openai", model="gpt-4o")

    with patch("horizon.llm._call_llm_raw", return_value="sniper response") as mock_raw:
        result = _call_sniper_llm("test prompt", config)

    assert result == "sniper response"
    mock_raw.assert_called_once_with(
        "test prompt", "openai", "gpt-4o",
        max_tokens=512, temperature=0.1,
    )


# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------


def test_exports():
    import horizon

    assert hasattr(horizon, "ResolutionSignal")
    assert hasattr(horizon, "SniperConfig")
    assert hasattr(horizon, "scan_resolutions")
    assert hasattr(horizon, "resolution_sniper")
