"""Tests for scan_all_tickers orchestration: error isolation between phases."""

from __future__ import annotations

import pytest
from datetime import datetime, timezone
from sqlalchemy import select

from app.models.score import CompositeScore, DimensionScore
from app.models.ticker import Ticker
from app.services import rr_scanner_service, scoring_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_scan_proceeds_when_score_refresh_fails(session, monkeypatch):
    """A scoring failure must not skip setup detection for the ticker.

    Qualification re-gates on live scores at alert time, so a stale score is
    recoverable — a skipped scan is not.
    """
    session.add(Ticker(symbol="AAA"))
    await session.commit()

    async def _boom(db, symbol):
        raise RuntimeError("scoring unavailable")

    scanned: list[str] = []
    primary_floors: list[float] = []

    async def _fake_scan_ticker(db, symbol, *args, **kwargs):
        scanned.append(symbol)
        primary_floors.append(kwargs["primary_min_rr"])
        return []

    monkeypatch.setattr(scoring_service, "compute_all_dimensions", _boom)
    monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker)

    setups = await rr_scanner_service.scan_all_tickers(session)

    assert scanned == ["AAA"]
    assert primary_floors == [rr_scanner_service.PRIMARY_TARGET_MIN_RR]
    assert setups == []


async def test_scan_marks_scores_stale_when_refresh_fails(session, monkeypatch):
    ticker = Ticker(symbol="AAA")
    session.add(ticker)
    await session.flush()
    now = datetime.now(timezone.utc)
    session.add_all([
        DimensionScore(
            ticker_id=ticker.id,
            dimension="technical",
            score=70.0,
            is_stale=False,
            computed_at=now,
        ),
        CompositeScore(
            ticker_id=ticker.id,
            score=70.0,
            is_stale=False,
            weights_json="{}",
            computed_at=now,
        ),
    ])
    await session.commit()

    async def _boom(db, symbol):
        raise RuntimeError("scoring unavailable")

    async def _fake_scan_ticker(db, symbol, *args, **kwargs):
        comp = (
            await db.execute(select(CompositeScore).where(CompositeScore.ticker_id == ticker.id))
        ).scalar_one()
        dimensions = (
            await db.execute(select(DimensionScore).where(DimensionScore.ticker_id == ticker.id))
        ).scalars().all()
        assert comp.is_stale is True
        assert all(score.is_stale for score in dimensions)
        return []

    monkeypatch.setattr(scoring_service, "compute_all_dimensions", _boom)
    monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker)

    await rr_scanner_service.scan_all_tickers(session)


async def test_scan_error_does_not_stop_later_tickers(session, monkeypatch):
    session.add_all([Ticker(symbol="AAA"), Ticker(symbol="BBB")])
    await session.commit()

    scanned: list[str] = []

    async def _fake_scan_ticker(db, symbol, *args, **kwargs):
        scanned.append(symbol)
        if symbol == "AAA":
            raise RuntimeError("scan blew up")
        return []

    monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker)

    await rr_scanner_service.scan_all_tickers(session)

    assert scanned == ["AAA", "BBB"]
