"""Unit tests for read-only get_rankings: bulk-load, sorting, and staleness."""

from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import AsyncMock, patch

import pytest
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.database import Base
from app.models.score import CompositeScore, DimensionScore
from app.models.ticker import Ticker
from app.services.scoring_service import get_rankings

TEST_DATABASE_URL = "sqlite+aiosqlite://"


@pytest.fixture
async def fresh_db():
    """Non-transactional session for persisted ranking reads."""
    engine = create_async_engine(TEST_DATABASE_URL, echo=False)
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    async with session_factory() as session:
        yield session
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    await engine.dispose()


async def _seed_ticker(session: AsyncSession, symbol: str) -> Ticker:
    ticker = Ticker(symbol=symbol)
    session.add(ticker)
    await session.commit()
    await session.refresh(ticker)
    return ticker


def _composite(ticker_id: int, score: float, *, stale: bool = False) -> CompositeScore:
    return CompositeScore(
        ticker_id=ticker_id, score=score, is_stale=stale,
        weights_json="{}", computed_at=datetime.now(timezone.utc),
    )


def _dimension(ticker_id: int, dimension: str, score: float) -> DimensionScore:
    return DimensionScore(
        ticker_id=ticker_id, dimension=dimension, score=score,
        is_stale=False, computed_at=datetime.now(timezone.utc),
    )


@pytest.mark.asyncio
async def test_fast_path_sorts_and_does_not_recompute(fresh_db: AsyncSession):
    """All composites fresh: result is sorted desc and no recompute is triggered
    (the common steady-state path after the daily scan)."""
    low = await _seed_ticker(fresh_db, "LOW")
    high = await _seed_ticker(fresh_db, "HIGH")

    fresh_db.add_all([
        _composite(low.id, 40.0),
        _composite(high.id, 90.0),
        _dimension(high.id, "technical", 88.0),
        _dimension(low.id, "technical", 42.0),
    ])
    await fresh_db.commit()

    # If the fast path tries to recompute, these blow up.
    with patch("app.services.scoring_service.compute_dimension_score",
               new=AsyncMock(side_effect=AssertionError("should not recompute"))), \
         patch("app.services.scoring_service.compute_composite_score",
               new=AsyncMock(side_effect=AssertionError("should not recompute"))):
        result = await get_rankings(fresh_db)

    symbols = [r["symbol"] for r in result["rankings"]]
    assert symbols == ["HIGH", "LOW"]  # sorted desc
    assert result["rankings"][0]["composite_score"] == 90.0
    assert result["rankings"][0]["dimensions"][0]["dimension"] == "technical"


@pytest.mark.asyncio
async def test_ticker_without_computable_composite_is_excluded(fresh_db: AsyncSession):
    """A ticker without a persisted composite is omitted from rankings."""
    fresh = await _seed_ticker(fresh_db, "OK")
    await _seed_ticker(fresh_db, "NONE")
    fresh_db.add_all([_composite(fresh.id, 50.0), _dimension(fresh.id, "technical", 50.0)])
    await fresh_db.commit()

    with patch("app.services.scoring_service.compute_dimension_score",
               new=AsyncMock(side_effect=AssertionError("should not recompute"))), \
         patch("app.services.scoring_service.compute_composite_score",
               new=AsyncMock(side_effect=AssertionError("should not recompute"))):
        result = await get_rankings(fresh_db)

    assert [r["symbol"] for r in result["rankings"]] == ["OK"]


@pytest.mark.asyncio
async def test_stale_composite_is_reported_without_recompute(fresh_db: AsyncSession):
    """A stale composite appears with its stale flag and is not recomputed."""
    ticker = await _seed_ticker(fresh_db, "STALE")
    fresh_db.add(_composite(ticker.id, 10.0, stale=True))
    await fresh_db.commit()

    with patch("app.services.scoring_service.compute_dimension_score",
               new=AsyncMock(side_effect=AssertionError("should not recompute"))), \
         patch("app.services.scoring_service.compute_composite_score",
               new=AsyncMock(side_effect=AssertionError("should not recompute"))):
        result = await get_rankings(fresh_db)

    assert [r["symbol"] for r in result["rankings"]] == ["STALE"]
    assert result["rankings"][0]["composite_score"] == 10.0
    assert result["rankings"][0]["composite_stale"] is True
