"""Unit tests for _compute_sentiment_score breakdown refactor."""

from __future__ import annotations

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

import pytest

from app.services.scoring_service import _compute_sentiment_score


def _make_sentiment_records(n: int, classification: str = "bullish", confidence: int = 80) -> list:
    """Create n mock sentiment records with recent timestamps."""
    now = datetime.now(timezone.utc)
    records = []
    for i in range(n):
        records.append(
            SimpleNamespace(
                classification=classification,
                confidence=confidence,
                source="test",
                timestamp=now,
                reasoning="",
                citations_json="[]",
            )
        )
    return records


@pytest.mark.asyncio
async def test_returns_none_with_breakdown_when_no_records(db_session):
    """When no sentiment records exist, returns (None, breakdown) with unavailable entry."""
    with patch(
        "app.services.sentiment_service.get_sentiment_scores",
        new_callable=AsyncMock,
        return_value=[],
    ):
        score, breakdown = await _compute_sentiment_score(db_session, "AAPL")

    assert score is None
    assert breakdown is not None
    assert breakdown["sub_scores"] == []
    assert len(breakdown["unavailable"]) == 1
    assert breakdown["unavailable"][0]["name"] == "sentiment_records"
    assert "formula" in breakdown


@pytest.mark.asyncio
async def test_returns_none_none_when_get_scores_raises(db_session):
    """When get_sentiment_scores raises, returns (None, None)."""
    with patch(
        "app.services.sentiment_service.get_sentiment_scores",
        new_callable=AsyncMock,
        side_effect=Exception("DB error"),
    ):
        score, breakdown = await _compute_sentiment_score(db_session, "AAPL")

    assert score is None
    assert breakdown is None


@pytest.mark.asyncio
async def test_returns_breakdown_with_sub_scores(db_session):
    """With sentiment records, returns score and breakdown with expected sub-scores."""
    records = _make_sentiment_records(3)

    with patch(
        "app.services.sentiment_service.get_sentiment_scores",
        new_callable=AsyncMock,
        return_value=records,
    ), patch(
        "app.services.sentiment_service.compute_sentiment_dimension_score",
        new_callable=AsyncMock,
        return_value=75.0,
    ):
        score, breakdown = await _compute_sentiment_score(db_session, "AAPL")

    assert score == 75.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 "record_count" in names
    assert "decay_rate" in names
    assert "lookback_window" in names

    # Verify raw values
    raw_map = {s["name"]: s["raw_value"] for s in breakdown["sub_scores"]}
    assert raw_map["record_count"] == 3
    assert raw_map["decay_rate"] == 0.1
    assert raw_map["lookback_window"] == 168

    assert breakdown["unavailable"] == []


@pytest.mark.asyncio
async def test_formula_contains_decay_info(db_session):
    """Breakdown formula describes the time-decay weighted average."""
    records = _make_sentiment_records(2)

    with patch(
        "app.services.sentiment_service.get_sentiment_scores",
        new_callable=AsyncMock,
        return_value=records,
    ), patch(
        "app.services.sentiment_service.compute_sentiment_dimension_score",
        new_callable=AsyncMock,
        return_value=60.0,
    ):
        _, breakdown = await _compute_sentiment_score(db_session, "AAPL")

    assert "Time-decay" in breakdown["formula"]
    assert "decay_rate" in breakdown["formula"]
    assert "168" in breakdown["formula"]


@pytest.mark.asyncio
async def test_sub_scores_have_descriptions(db_session):
    """Each sub-score has a non-empty description."""
    records = _make_sentiment_records(1)

    with patch(
        "app.services.sentiment_service.get_sentiment_scores",
        new_callable=AsyncMock,
        return_value=records,
    ), patch(
        "app.services.sentiment_service.compute_sentiment_dimension_score",
        new_callable=AsyncMock,
        return_value=50.0,
    ):
        _, breakdown = await _compute_sentiment_score(db_session, "AAPL")

    for sub in breakdown["sub_scores"]:
        assert sub["description"], f"Sub-score {sub['name']} missing description"
