"""Single-ticker activation ranks: manual refresh must attach residual ranks.

A daily ``scan_all`` path ranks the universe and stamps each setup. Manual
``/ingestion/fetch`` re-scans one symbol; without the same stamps the new
setup has ``momentum_percentile=None`` and fails the activation gate.
"""

from __future__ import annotations

from datetime import datetime, timezone

import pytest

from app.models.ticker import Ticker
from app.models.trade_setup import TradeSetup
from app.services import rr_scanner_service
from tests.conftest import _test_session_factory  # type: ignore


@pytest.fixture
async def session():
    async with _test_session_factory() as s:
        yield s


async def test_resolve_ranks_uses_fresh_universe_rank(session, monkeypatch):
    session.add(Ticker(symbol="AAA"))
    await session.commit()

    async def _fake_ranks(db):
        return {
            "AAA": {
                "momentum_percentile": 91.0,
                "strategy_rank": 88.5,
                "volatility_percentile": 70.0,
            }
        }

    monkeypatch.setattr(
        "app.services.momentum_service.compute_activation_ranks",
        _fake_ranks,
    )

    ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "aaa")
    assert ranks["momentum_percentile"] == 91.0
    assert ranks["strategy_rank"] == 88.5
    assert ranks["volatility_percentile"] == 70.0


async def test_resolve_ranks_falls_back_to_previous_setup(session, monkeypatch):
    ticker = Ticker(symbol="BBB")
    session.add(ticker)
    await session.flush()
    session.add(
        TradeSetup(
            ticker_id=ticker.id,
            direction="long",
            entry_price=100.0,
            stop_loss=95.0,
            target=110.0,
            rr_ratio=2.0,
            composite_score=60.0,
            momentum_percentile=84.0,
            strategy_rank=80.0,
            volatility_percentile=55.0,
            detected_at=datetime.now(timezone.utc),
        )
    )
    await session.commit()

    async def _empty_ranks(db):
        return {}

    monkeypatch.setattr(
        "app.services.momentum_service.compute_activation_ranks",
        _empty_ranks,
    )

    ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "BBB")
    assert ranks["momentum_percentile"] == 84.0
    assert ranks["strategy_rank"] == 80.0
    assert ranks["volatility_percentile"] == 55.0


async def test_resolve_ranks_skips_null_prior_setups(session, monkeypatch):
    """Broken prior setups (null percentile) must not block a still-older ranked row."""
    ticker = Ticker(symbol="CCC")
    session.add(ticker)
    await session.flush()
    now = datetime.now(timezone.utc)
    session.add_all([
        TradeSetup(
            ticker_id=ticker.id,
            direction="long",
            entry_price=100.0,
            stop_loss=95.0,
            target=110.0,
            rr_ratio=2.0,
            composite_score=60.0,
            momentum_percentile=93.0,
            strategy_rank=90.0,
            volatility_percentile=40.0,
            detected_at=now.replace(year=now.year - 1) if now.year > 2000 else now,
        ),
        TradeSetup(
            ticker_id=ticker.id,
            direction="long",
            entry_price=101.0,
            stop_loss=96.0,
            target=111.0,
            rr_ratio=2.1,
            composite_score=61.0,
            momentum_percentile=None,
            strategy_rank=None,
            volatility_percentile=None,
            detected_at=now,
        ),
    ])
    await session.commit()

    async def _empty_ranks(db):
        return {}

    monkeypatch.setattr(
        "app.services.momentum_service.compute_activation_ranks",
        _empty_ranks,
    )

    ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "CCC")
    assert ranks["momentum_percentile"] == 93.0
    assert ranks["strategy_rank"] == 90.0


async def test_resolve_ranks_returns_empty_when_unavailable(session, monkeypatch):
    session.add(Ticker(symbol="DDD"))
    await session.commit()

    async def _boom(db):
        raise RuntimeError("ranker down")

    monkeypatch.setattr(
        "app.services.momentum_service.compute_activation_ranks",
        _boom,
    )

    ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "DDD")
    assert ranks == {
        "momentum_percentile": None,
        "strategy_rank": None,
        "volatility_percentile": None,
    }
