"""Unit tests for _compute_technical_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_technical_score


def _make_ohlcv_records(n: int, base_close: float = 100.0) -> list:
    """Create n mock OHLCV records with realistic price data."""
    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_tuple_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_technical_score(db_session, "AAPL")
    assert score is None
    assert breakdown is None


@pytest.mark.asyncio
async def test_returns_breakdown_with_all_sub_scores(db_session):
    """With enough data, returns score and breakdown with all six sub-scores."""
    # 60 bars: enough for every indicator incl. EMA Cross (needs 51)
    records = _make_ohlcv_records(60)

    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        score, breakdown = await _compute_technical_score(db_session, "AAPL")

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

    assert breakdown is not None
    assert "sub_scores" in breakdown
    assert "formula" in breakdown
    assert "unavailable" in breakdown

    names = [s["name"] for s in breakdown["sub_scores"]]
    assert "ADX" in names
    assert "EMA" in names
    assert "RSI" in names
    assert "EMA_Cross" in names
    assert "Volume_Profile" in names
    assert "Pivot_Points" in names

    # Verify weights sum to 1 and match the formula
    weight_map = {s["name"]: s["weight"] for s in breakdown["sub_scores"]}
    assert weight_map["ADX"] == 0.30
    assert weight_map["EMA"] == 0.20
    assert weight_map["RSI"] == 0.20
    assert weight_map["EMA_Cross"] == 0.15
    assert weight_map["Volume_Profile"] == 0.10
    assert weight_map["Pivot_Points"] == 0.05
    assert sum(weight_map.values()) == pytest.approx(1.0)

    # Verify raw_value is present (numeric except EMA_Cross's signal string)
    for sub in breakdown["sub_scores"]:
        assert sub["raw_value"] is not None
        if sub["name"] == "EMA_Cross":
            assert sub["raw_value"] in ("bullish", "neutral", "bearish")
        else:
            assert isinstance(sub["raw_value"], (int, float))
        assert sub["description"]

    assert breakdown["unavailable"] == []


@pytest.mark.asyncio
async def test_partial_sub_scores_with_insufficient_data(db_session):
    """With limited data (enough for EMA/RSI but not ADX), returns partial breakdown."""
    # 22 bars: enough for EMA(20) and RSI(14) but not ADX (needs 28)
    records = _make_ohlcv_records(22)

    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        score, breakdown = await _compute_technical_score(db_session, "AAPL")

    assert score is not None
    assert breakdown is not None

    names = [s["name"] for s in breakdown["sub_scores"]]
    assert "EMA" in names
    assert "RSI" in names
    assert "ADX" not in names

    # ADX should be in unavailable
    unavail_names = [u["name"] for u in breakdown["unavailable"]]
    assert "ADX" in unavail_names
    assert any(u["reason"] for u in breakdown["unavailable"])


@pytest.mark.asyncio
async def test_all_sub_scores_unavailable(db_session):
    """With very few bars (not enough for any indicator), returns None score with breakdown."""
    # 4 bars: not enough for any indicator (Pivot Points needs 5+)
    records = _make_ohlcv_records(4)

    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        score, breakdown = await _compute_technical_score(db_session, "AAPL")

    assert score is None
    assert breakdown is not None
    assert breakdown["sub_scores"] == []
    assert len(breakdown["unavailable"]) == 6

    unavail_names = [u["name"] for u in breakdown["unavailable"]]
    assert "ADX" in unavail_names
    assert "EMA" in unavail_names
    assert "RSI" in unavail_names
    assert "EMA_Cross" in unavail_names
    assert "Volume_Profile" in unavail_names
    assert "Pivot_Points" in unavail_names


@pytest.mark.asyncio
async def test_formula_string_present(db_session):
    """Breakdown always includes the formula description."""
    records = _make_ohlcv_records(50)

    with patch(
        "app.services.price_service.query_ohlcv",
        new_callable=AsyncMock,
        return_value=records,
    ):
        _, breakdown = await _compute_technical_score(db_session, "AAPL")

    assert "formula" in breakdown
    assert "ADX" in breakdown["formula"]
    assert "EMA" in breakdown["formula"]
    assert "RSI" in breakdown["formula"]
