"""Unit tests for _compute_momentum_score breakdown refactor."""

from __future__ import annotations

from datetime import date
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

import pytest

from app.services.scoring_service import _compute_momentum_score


def _make_ohlcv_records(n: int, base_close: float = 100.0) -> list:
    """Create n mock OHLCV records with incrementing close prices."""
    records = []
    for i in range(n):
        price = base_close + (i * 0.5)
        records.append(
            SimpleNamespace(
                date=date(2024, 1, 1),
                open=price - 0.5,
                high=price + 1.0,
                low=price - 1.0,
                close=price,
                volume=1000000,
            )
        )
    return records


@pytest.mark.asyncio
async def test_returns_none_none_when_no_records(db_session):
    """When no OHLCV data exists, returns (None, None)."""
    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=[],
    ):
        score, breakdown = await _compute_momentum_score(db_session, "AAPL")
    assert score is None
    assert breakdown is None


@pytest.mark.asyncio
async def test_returns_none_none_when_fewer_than_6_records(db_session):
    """With fewer than 6 records, returns (None, None)."""
    records = _make_ohlcv_records(5)
    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        score, breakdown = await _compute_momentum_score(db_session, "AAPL")
    assert score is None
    assert breakdown is None


@pytest.mark.asyncio
async def test_returns_only_5day_roc_when_fewer_than_21_records(db_session):
    """With 6-20 records, returns only 5-day ROC sub-score; 20-day ROC is unavailable."""
    records = _make_ohlcv_records(10)
    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        score, breakdown = await _compute_momentum_score(db_session, "AAPL")

    assert score is not None
    assert 0.0 <= score <= 100.0

    assert breakdown is not None
    names = [s["name"] for s in breakdown["sub_scores"]]
    assert "5-day ROC" in names
    assert "20-day ROC" not in names

    # 20-day ROC should be unavailable
    unavail_names = [u["name"] for u in breakdown["unavailable"]]
    assert "20-day ROC" in unavail_names


@pytest.mark.asyncio
async def test_returns_both_sub_scores_with_enough_data(db_session):
    """With 21+ records, returns both 5-day and 20-day ROC sub-scores."""
    records = _make_ohlcv_records(30)
    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        score, breakdown = await _compute_momentum_score(db_session, "AAPL")

    assert score is not None
    assert 0.0 <= score <= 100.0

    assert breakdown is not None
    names = [s["name"] for s in breakdown["sub_scores"]]
    assert "5-day ROC" in names
    assert "20-day ROC" in names

    # Verify weights
    weight_map = {s["name"]: s["weight"] for s in breakdown["sub_scores"]}
    assert weight_map["5-day ROC"] == 0.5
    assert weight_map["20-day ROC"] == 0.5

    # Verify raw_value is present and numeric
    for sub in breakdown["sub_scores"]:
        assert sub["raw_value"] is not None
        assert isinstance(sub["raw_value"], (int, float))
        assert sub["description"]

    assert breakdown["unavailable"] == []


@pytest.mark.asyncio
async def test_formula_string_present(db_session):
    """Breakdown always includes the formula description."""
    records = _make_ohlcv_records(30)
    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        _, breakdown = await _compute_momentum_score(db_session, "AAPL")

    assert "formula" in breakdown
    assert "ROC_5" in breakdown["formula"]
    assert "ROC_20" in breakdown["formula"]


@pytest.mark.asyncio
async def test_raw_values_are_roc_percentages(db_session):
    """Raw values should be ROC percentages matching the actual price change."""
    records = _make_ohlcv_records(30, base_close=100.0)
    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        _, breakdown = await _compute_momentum_score(db_session, "AAPL")

    closes = [100.0 + i * 0.5 for i in range(30)]
    latest = closes[-1]
    expected_roc_5 = (latest - closes[-6]) / closes[-6] * 100.0
    expected_roc_20 = (latest - closes[-21]) / closes[-21] * 100.0

    roc_map = {s["name"]: s["raw_value"] for s in breakdown["sub_scores"]}
    assert abs(roc_map["5-day ROC"] - round(expected_roc_5, 4)) < 1e-6
    assert abs(roc_map["20-day ROC"] - round(expected_roc_20, 4)) < 1e-6
