"""Unit tests for read-only get_score composite breakdown wiring."""

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_score

TEST_DATABASE_URL = "sqlite+aiosqlite://"


@pytest.fixture
async def fresh_db():
    """Provide a non-transactional session for persisted score 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 = "AAPL") -> Ticker:
    ticker = Ticker(symbol=symbol)
    session.add(ticker)
    await session.commit()
    return ticker


async def _seed_scores(session: AsyncSession, ticker: Ticker, *, stale: bool = False) -> None:
    now = datetime(2026, 7, 3, tzinfo=timezone.utc)
    session.add_all([
        DimensionScore(
            ticker_id=ticker.id,
            dimension="technical",
            score=70.0,
            is_stale=stale,
            computed_at=now,
        ),
        DimensionScore(
            ticker_id=ticker.id,
            dimension="momentum",
            score=60.0,
            is_stale=False,
            computed_at=now,
        ),
        CompositeScore(
            ticker_id=ticker.id,
            score=66.0,
            is_stale=stale,
            weights_json="{}",
            computed_at=now,
        ),
    ])
    await session.commit()


@pytest.mark.asyncio
async def test_get_score_returns_composite_breakdown_without_recomputing(fresh_db):
    ticker = await _seed_ticker(fresh_db, "AAPL")
    await _seed_scores(fresh_db, ticker)

    with patch(
        "app.services.scoring_service.compute_dimension_score",
        new=AsyncMock(side_effect=AssertionError("GET must not recompute dimensions")),
    ), patch(
        "app.services.scoring_service.compute_composite_score",
        new=AsyncMock(side_effect=AssertionError("GET must not recompute composite")),
    ):
        result = await get_score(fresh_db, "AAPL")

    assert result["composite_score"] == 66.0
    cb = result["composite_breakdown"]
    assert cb is not None
    assert "technical" in cb["available_dimensions"]
    assert "momentum" in cb["available_dimensions"]
    assert "sentiment" in cb["missing_dimensions"]
    assert "fundamental" in cb["missing_dimensions"]
    assert "sr_quality" in cb["missing_dimensions"]
    assert cb["renormalized_weights"]
    assert abs(sum(cb["renormalized_weights"].values()) - 1.0) < 1e-9


@pytest.mark.asyncio
async def test_get_score_dimensions_do_not_recompute_breakdowns(fresh_db):
    ticker = await _seed_ticker(fresh_db, "AAPL")
    await _seed_scores(fresh_db, ticker)

    result = await get_score(fresh_db, "AAPL")

    tech_dim = next((d for d in result["dimensions"] if d["dimension"] == "technical"), None)
    assert tech_dim is not None
    assert tech_dim["breakdown"] is None


@pytest.mark.asyncio
async def test_get_score_all_dimensions_missing(fresh_db):
    await _seed_ticker(fresh_db, "AAPL")

    result = await get_score(fresh_db, "AAPL")

    cb = result["composite_breakdown"]
    assert cb["available_dimensions"] == []
    assert len(cb["missing_dimensions"]) == 5
    assert cb["renormalized_weights"] == {}
    assert result["composite_score"] is None


@pytest.mark.asyncio
async def test_get_score_reports_stale_without_refreshing(fresh_db):
    ticker = await _seed_ticker(fresh_db, "AAPL")
    await _seed_scores(fresh_db, ticker, stale=True)

    result = await get_score(fresh_db, "AAPL")

    assert result["composite_stale"] is True
    assert "technical" in result["missing_dimensions"]
    tech_dim = next((d for d in result["dimensions"] if d["dimension"] == "technical"), None)
    assert tech_dim is not None
    assert tech_dim["is_stale"] is True
