import pytest
from unittest.mock import Mock, AsyncMock, MagicMock
from datetime import datetime
from app.services.playlist_service import PlaylistService
from app.models import PlaylistConfig, Track


@pytest.fixture
def mock_db():
    session = MagicMock()
    # Mock query return values
    session.query.return_value.filter.return_value.first.return_value = None
    return session


@pytest.fixture
def mock_spotify():
    client = AsyncMock()
    client.create_playlist.return_value = {"id": "new_playlist_id"}
    client.get_tracks.return_value = []
    return client


@pytest.fixture
def mock_recco():
    client = AsyncMock()
    return client


@pytest.fixture
def mock_narrative():
    service = Mock()
    service.generate_playlist_theme.return_value = {
        "theme_name": "Test Theme",
        "description": "Test Description",
        "curated_tracks": [],
    }
    return service


@pytest.fixture
def playlist_service(mock_db, mock_spotify, mock_recco, mock_narrative):
    return PlaylistService(mock_db, mock_spotify, mock_recco, mock_narrative)


@pytest.mark.asyncio
async def test_ensure_playlists_exist_creates_new(
    playlist_service, mock_db, mock_spotify
):
    # Setup: DB empty, Env vars assumed empty (or mocked)
    mock_db.query.return_value.filter.return_value.first.return_value = None

    result = await playlist_service.ensure_playlists_exist("user123")

    assert result["six_hour_id"] == "new_playlist_id"
    assert result["daily_id"] == "new_playlist_id"
    assert mock_spotify.create_playlist.call_count == 2
    # Verify persistence call
    assert mock_db.add.call_count == 2  # Once for each
    assert mock_db.commit.call_count == 2


@pytest.mark.asyncio
async def test_ensure_playlists_exist_loads_from_db(
    playlist_service, mock_db, mock_spotify
):
    # Setup: DB has configs
    mock_six = PlaylistConfig(key="six_hour", spotify_id="db_six_id")
    mock_daily = PlaylistConfig(key="daily", spotify_id="db_daily_id")

    # Mock return values for separate queries
    # This is tricky with MagicMock chains.

    # Simpler approach: Assuming the service calls query(PlaylistConfig).filter(...)
    # We can just check the result logic without complex DB mocking if we abstract the DB access.
    # But let's try to mock the specific return values based on call order if possible.
    mock_query = mock_db.query.return_value
    mock_filter = mock_query.filter

    # Configure filter().first() to return mock_six then mock_daily
    # But ensure_playlists_exist calls filter twice.
    # mock_filter.return_value is the same object.
    # mock_filter.return_value.first.side_effect = [mock_six, mock_daily]
    # This assumes sequential execution order which is fragile but works for unit test.
    # IMPORTANT: Ensure filter side_effect is cleared if set previously
    mock_filter.side_effect = None
    mock_filter.return_value.first.side_effect = [mock_six, mock_daily]

    result = await playlist_service.ensure_playlists_exist("user123")

    assert result["six_hour_id"] == "db_six_id"
    assert result["daily_id"] == "db_daily_id"
    mock_spotify.create_playlist.assert_not_called()


def test_optimize_playlist_flow(playlist_service):
    tracks = [
        {"id": "1", "energy": 0.8},  # High
        {"id": "2", "energy": 0.2},  # Low
        {"id": "3", "energy": 0.5},  # Medium
        {"id": "4", "energy": 0.9},  # High
        {"id": "5", "energy": 0.3},  # Low
    ]

    # Expected sort: Low, Low, Medium, High, High
    # Then split:
    # Sorted: 2(0.2), 5(0.3), 3(0.5), 1(0.8), 4(0.9)
    # Len 5.
    # Low end: 5 * 0.3 = 1.5 -> 1. (Index 1) -> [2]
    # High start: 5 * 0.7 = 3.5 -> 3. (Index 3) -> [1, 4]
    # Medium: [5, 3]
    # Result: Low + High + Medium = [2] + [1, 4] + [5, 3]
    # Order: 2, 1, 4, 5, 3
    # Energies: 0.2, 0.8, 0.9, 0.3, 0.5

    optimized = playlist_service._optimize_playlist_flow(tracks)

    ids = [t["id"] for t in optimized]
    # Check if High energy tracks are in the middle/early part (Ramp Up)
    # The current logic is Low -> High -> Medium.
    # So we expect High energy block (1, 4) to be in the middle?
    # Wait, code was: low_energy + high_energy + medium_energy

    assert ids == ["2", "1", "4", "5", "3"]
    assert optimized[0]["energy"] == 0.2
    assert optimized[1]["energy"] == 0.8
