"""Phase 5 WebSocket migration tests — Sync/Discovery Progress + Scans.

Verifies that:
 - Room-based sync:progress events are delivered only to subscribed clients
 - Room-based discovery:progress events are delivered only to subscribed clients
 - Broadcast scan:watchlist and scan:media events reach all clients
 - Data shapes are correct for each event type
 - HTTP endpoints still work as fallback

IMPORTANT: Do NOT use ``from tests.conftest import …`` — pytest's auto-discovered
conftest is a different module instance. Use the ``shared_state`` fixture instead.
"""

import pytest


# =========================================================================
# Constants
# =========================================================================

DISCOVERY_PLATFORMS = ['tidal', 'youtube']

SYNC_PLAYLIST_IDS = ['test-playlist-1']


# =========================================================================
# Group A — Sync Event Delivery (room-based)
# =========================================================================

class TestSyncEventDelivery:
    """sync:progress socket events are received by subscribed clients."""
    def test_sync_only_subscribed(self, test_app, shared_state):
        """Unsubscribed client does NOT receive sync:progress events."""
        app, socketio = test_app
        subscribed = socketio.test_client(app)
        unsubscribed = socketio.test_client(app)
        build = shared_state['build_sync_status']

        # Only one client subscribes
        subscribed.emit('sync:subscribe', {'playlist_ids': ['test-playlist-1']})

        socketio.emit('sync:progress', {
            'playlist_id': 'test-playlist-1',
            **build('test-playlist-1')
        }, room='sync:test-playlist-1')

        sub_events = [e for e in subscribed.get_received()
                      if e['name'] == 'sync:progress']
        unsub_events = [e for e in unsubscribed.get_received()
                        if e['name'] == 'sync:progress']

        assert len(sub_events) >= 1
        assert len(unsub_events) == 0

        subscribed.disconnect()
        unsubscribed.disconnect()

    def test_sync_subscribe_unsubscribe(self, test_app, shared_state):
        """Client stops receiving events after unsubscribing."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_sync_status']

        # Subscribe
        client.emit('sync:subscribe', {'playlist_ids': ['test-playlist-1']})

        # First emit — should receive
        socketio.emit('sync:progress', {
            'playlist_id': 'test-playlist-1',
            **build('test-playlist-1')
        }, room='sync:test-playlist-1')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'sync:progress']
        assert len(events) >= 1

        # Unsubscribe
        client.emit('sync:unsubscribe', {'playlist_ids': ['test-playlist-1']})

        # Second emit — should NOT receive
        socketio.emit('sync:progress', {
            'playlist_id': 'test-playlist-1',
            **build('test-playlist-1')
        }, room='sync:test-playlist-1')

        received2 = client.get_received()
        events2 = [e for e in received2 if e['name'] == 'sync:progress']
        assert len(events2) == 0

        client.disconnect()


# =========================================================================
# Group B — Sync Data Shape
# =========================================================================

class TestSyncDataShape:
    """sync:progress event data has the expected keys."""

    def test_sync_progress_shape(self, test_app, shared_state):
        """Sync progress has playlist_id, status, progress dict."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_sync_status']

        client.emit('sync:subscribe', {'playlist_ids': ['test-playlist-1']})

        payload = {'playlist_id': 'test-playlist-1', **build('test-playlist-1')}
        socketio.emit('sync:progress', payload, room='sync:test-playlist-1')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'sync:progress']
        assert len(events) >= 1
        data = events[0]['args'][0]

        assert 'playlist_id' in data
        assert data['playlist_id'] == 'test-playlist-1'
        assert 'status' in data
        assert data['status'] == 'syncing'
        assert 'progress' in data
        progress = data['progress']
        assert 'total_tracks' in progress
        assert 'matched_tracks' in progress
        assert 'progress' in progress
        assert isinstance(progress['progress'], (int, float))

        client.disconnect()


# =========================================================================
# Group C — Discovery Event Delivery (room-based)
# =========================================================================

class TestDiscoveryEventDelivery:
    """discovery:progress socket events are received by subscribed clients."""
    def test_discovery_only_subscribed(self, test_app, shared_state):
        """Unsubscribed client does NOT receive discovery:progress events."""
        app, socketio = test_app
        subscribed = socketio.test_client(app)
        unsubscribed = socketio.test_client(app)
        build = shared_state['build_discovery_status']

        subscribed.emit('discovery:subscribe', {'ids': ['test-tidal-1']})

        payload = build('tidal', 'test-tidal-1')
        payload['platform'] = 'tidal'
        payload['id'] = 'test-tidal-1'
        socketio.emit('discovery:progress', payload, room='discovery:test-tidal-1')

        sub_events = [e for e in subscribed.get_received()
                      if e['name'] == 'discovery:progress']
        unsub_events = [e for e in unsubscribed.get_received()
                        if e['name'] == 'discovery:progress']

        assert len(sub_events) >= 1
        assert len(unsub_events) == 0

        subscribed.disconnect()
        unsubscribed.disconnect()

    def test_discovery_subscribe_unsubscribe(self, test_app, shared_state):
        """Client stops receiving discovery events after unsubscribing."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_discovery_status']

        client.emit('discovery:subscribe', {'ids': ['test-yt-hash']})

        payload = build('youtube', 'test-yt-hash')
        payload['platform'] = 'youtube'
        payload['id'] = 'test-yt-hash'
        socketio.emit('discovery:progress', payload, room='discovery:test-yt-hash')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'discovery:progress']
        assert len(events) >= 1

        client.emit('discovery:unsubscribe', {'ids': ['test-yt-hash']})

        socketio.emit('discovery:progress', payload, room='discovery:test-yt-hash')
        received2 = client.get_received()
        events2 = [e for e in received2 if e['name'] == 'discovery:progress']
        assert len(events2) == 0

        client.disconnect()


# =========================================================================
# Group D — Discovery Data Shape
# =========================================================================

class TestDiscoveryDataShape:
    """discovery:progress event data has the expected keys."""

    @pytest.mark.parametrize('platform,pid', [
        ('tidal', 'test-tidal-1'),
        ('youtube', 'test-yt-hash'),
    ])
    def test_discovery_progress_shape(self, test_app, shared_state, platform, pid):
        """Discovery progress has platform, id, phase, status, complete, results."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_discovery_status']

        client.emit('discovery:subscribe', {'ids': [pid]})

        payload = build(platform, pid)
        payload['platform'] = platform
        payload['id'] = pid
        socketio.emit('discovery:progress', payload, room=f'discovery:{pid}')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'discovery:progress']
        assert len(events) >= 1
        data = events[0]['args'][0]

        assert data['platform'] == platform
        assert data['id'] == pid
        assert 'phase' in data
        assert 'status' in data
        assert 'progress' in data
        assert 'spotify_matches' in data
        assert 'spotify_total' in data
        assert 'results' in data
        assert 'complete' in data
        assert isinstance(data['results'], list)
        assert isinstance(data['complete'], bool)

        client.disconnect()


# =========================================================================
# Group E — Scan Events (broadcast)
# =========================================================================

class TestScanEventDelivery:
    """Broadcast scan events are received by all connected clients."""
    def test_watchlist_scan_shape(self, test_app, shared_state):
        """scan:watchlist data has success, status, current_artist_name."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_watchlist_scan_status']

        socketio.emit('scan:watchlist', build())

        received = client.get_received()
        events = [e for e in received if e['name'] == 'scan:watchlist']
        assert len(events) >= 1
        data = events[0]['args'][0]

        assert data['success'] is True
        assert 'status' in data
        assert data['status'] == 'scanning'
        assert 'current_artist_name' in data
        assert 'current_album' in data
        assert 'current_track_name' in data

        client.disconnect()

    def test_media_scan_shape(self, test_app, shared_state):
        """scan:media data has success and status with is_scanning."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_media_scan_status']

        socketio.emit('scan:media', build())

        received = client.get_received()
        events = [e for e in received if e['name'] == 'scan:media']
        assert len(events) >= 1
        data = events[0]['args'][0]

        assert data['success'] is True
        assert 'status' in data
        status = data['status']
        assert 'is_scanning' in status
        assert 'status' in status
        assert 'progress_message' in status

        client.disconnect()


# =========================================================================
# Group F — HTTP Parity
# =========================================================================

class TestSyncHttpParity:
    """Socket event data matches HTTP endpoint response for sync."""

    def test_sync_matches_http(self, test_app, shared_state):
        """Sync socket data matches GET /api/sync/status/:id."""
        app, socketio = test_app
        flask_client = app.test_client()
        ws_client = socketio.test_client(app)
        build = shared_state['build_sync_status']

        endpoint = shared_state['sync_endpoints']['sync']
        http_data = flask_client.get(endpoint).get_json()

        ws_client.emit('sync:subscribe', {'playlist_ids': ['test-playlist-1']})

        payload = {'playlist_id': 'test-playlist-1', **build('test-playlist-1')}
        socketio.emit('sync:progress', payload, room='sync:test-playlist-1')

        received = ws_client.get_received()
        events = [e for e in received if e['name'] == 'sync:progress']
        assert len(events) >= 1
        ws_data = events[0]['args'][0]

        assert ws_data['status'] == http_data['status']
        assert ws_data['playlist_id'] == http_data['playlist_id']
        assert ws_data['progress']['total_tracks'] == http_data['progress']['total_tracks']
        assert ws_data['progress']['matched_tracks'] == http_data['progress']['matched_tracks']

        ws_client.disconnect()


class TestDiscoveryHttpParity:
    """Socket event data matches HTTP endpoint response for discovery."""

    @pytest.mark.parametrize('platform,pid,endpoint_key', [
        ('tidal', 'test-tidal-1', 'tidal'),
        ('youtube', 'test-yt-hash', 'youtube'),
    ])
    def test_discovery_matches_http(self, test_app, shared_state,
                                    platform, pid, endpoint_key):
        """Discovery socket data matches GET /api/<platform>/discovery/status/:id."""
        app, socketio = test_app
        flask_client = app.test_client()
        ws_client = socketio.test_client(app)
        build = shared_state['build_discovery_status']

        endpoint = shared_state['discovery_endpoints'][endpoint_key]
        http_data = flask_client.get(endpoint).get_json()

        ws_client.emit('discovery:subscribe', {'ids': [pid]})

        payload = build(platform, pid)
        payload['platform'] = platform
        payload['id'] = pid
        socketio.emit('discovery:progress', payload, room=f'discovery:{pid}')

        received = ws_client.get_received()
        events = [e for e in received if e['name'] == 'discovery:progress']
        assert len(events) >= 1
        ws_data = events[0]['args'][0]

        assert ws_data['phase'] == http_data['phase']
        assert ws_data['status'] == http_data['status']
        assert ws_data['spotify_matches'] == http_data['spotify_matches']
        assert ws_data['spotify_total'] == http_data['spotify_total']
        assert ws_data['complete'] == http_data['complete']

        ws_client.disconnect()


class TestScanHttpParity:
    """Socket event data matches HTTP endpoint response for scans."""

    def test_watchlist_scan_matches_http(self, test_app, shared_state):
        """scan:watchlist socket data matches GET /api/watchlist/scan/status."""
        app, socketio = test_app
        flask_client = app.test_client()
        ws_client = socketio.test_client(app)
        build = shared_state['build_watchlist_scan_status']

        endpoint = shared_state['scan_endpoints']['watchlist']
        http_data = flask_client.get(endpoint).get_json()

        socketio.emit('scan:watchlist', build())

        received = ws_client.get_received()
        events = [e for e in received if e['name'] == 'scan:watchlist']
        assert len(events) >= 1
        ws_data = events[0]['args'][0]

        assert ws_data['success'] == http_data['success']
        assert ws_data['status'] == http_data['status']
        assert ws_data['current_artist_name'] == http_data['current_artist_name']

        ws_client.disconnect()

    def test_media_scan_matches_http(self, test_app, shared_state):
        """scan:media socket data matches GET /api/scan/status."""
        app, socketio = test_app
        flask_client = app.test_client()
        ws_client = socketio.test_client(app)
        build = shared_state['build_media_scan_status']

        endpoint = shared_state['scan_endpoints']['media']
        http_data = flask_client.get(endpoint).get_json()

        socketio.emit('scan:media', build())

        received = ws_client.get_received()
        events = [e for e in received if e['name'] == 'scan:media']
        assert len(events) >= 1
        ws_data = events[0]['args'][0]

        assert ws_data['success'] == http_data['success']
        assert ws_data['status']['is_scanning'] == http_data['status']['is_scanning']
        assert ws_data['status']['status'] == http_data['status']['status']

        ws_client.disconnect()


# =========================================================================
# Group G — HTTP Still Works
# =========================================================================

class TestHttpStillWorks:
    """HTTP endpoints return 200 with expected structure."""
    def test_sync_http_404_unknown(self, flask_client):
        """GET /api/sync/status/:id returns 404 for unknown playlist."""
        resp = flask_client.get('/api/sync/status/nonexistent')
        assert resp.status_code == 404

    def test_discovery_http_not_found(self, flask_client):
        """GET /api/tidal/discovery/status/:id returns error for unknown ID."""
        resp = flask_client.get('/api/tidal/discovery/status/nonexistent')
        assert resp.status_code == 200
        data = resp.get_json()
        assert 'error' in data

# =========================================================================
# Group H — Broadcast Updates
# =========================================================================

class TestBackwardCompat:
    """Broadcast scan events still reach multiple clients."""

    def test_multiple_clients_get_scan_updates(self, test_app, shared_state):
        """Multiple WebSocket clients each receive scan events."""
        app, socketio = test_app
        client1 = socketio.test_client(app)
        client2 = socketio.test_client(app)
        build = shared_state['build_watchlist_scan_status']

        socketio.emit('scan:watchlist', build())

        for client in [client1, client2]:
            received = client.get_received()
            events = [e for e in received if e['name'] == 'scan:watchlist']
            assert len(events) >= 1

        client1.disconnect()
        client2.disconnect()

    def test_sync_reflects_state_change(self, test_app, shared_state):
        """When sync state changes, the next emit reflects it."""
        app, socketio = test_app
        client = socketio.test_client(app)
        ss = shared_state['sync_states']
        build = shared_state['build_sync_status']

        client.emit('sync:subscribe', {'playlist_ids': ['test-playlist-1']})

        # Mutate state
        ss['test-playlist-1']['status'] = 'completed'
        ss['test-playlist-1']['progress']['progress'] = 100
        ss['test-playlist-1']['progress']['matched_tracks'] = 11

        payload = {'playlist_id': 'test-playlist-1', **build('test-playlist-1')}
        socketio.emit('sync:progress', payload, room='sync:test-playlist-1')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'sync:progress']
        data = events[-1]['args'][0]
        assert data['status'] == 'completed'
        assert data['progress']['progress'] == 100
        assert data['progress']['matched_tracks'] == 11

        client.disconnect()

    def test_discovery_reflects_state_change(self, test_app, shared_state):
        """When discovery state changes, the next emit reflects it."""
        app, socketio = test_app
        client = socketio.test_client(app)
        ds = shared_state['discovery_states']
        build = shared_state['build_discovery_status']

        client.emit('discovery:subscribe', {'ids': ['test-tidal-1']})

        # Mutate state
        ds['tidal']['test-tidal-1']['phase'] = 'discovered'
        ds['tidal']['test-tidal-1']['spotify_matches'] = 10

        payload = build('tidal', 'test-tidal-1')
        payload['platform'] = 'tidal'
        payload['id'] = 'test-tidal-1'
        socketio.emit('discovery:progress', payload, room='discovery:test-tidal-1')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'discovery:progress']
        data = events[-1]['args'][0]
        assert data['phase'] == 'discovered'
        assert data['complete'] is True
        assert data['spotify_matches'] == 10

        client.disconnect()

    def test_scan_reflects_state_change(self, test_app, shared_state):
        """When scan state changes, the next emit reflects it."""
        app, socketio = test_app
        client = socketio.test_client(app)
        wss = shared_state['watchlist_scan_state']
        build = shared_state['build_watchlist_scan_status']

        # Mutate state
        wss['status'] = 'completed'
        wss['current_artist_name'] = 'Led Zeppelin'

        socketio.emit('scan:watchlist', build())

        received = client.get_received()
        events = [e for e in received if e['name'] == 'scan:watchlist']
        data = events[-1]['args'][0]
        assert data['status'] == 'completed'
        assert data['current_artist_name'] == 'Led Zeppelin'

        client.disconnect()


# =========================================================================
# Group I — Phase 6: Platform Sync via WebSocket Rooms
# =========================================================================

PLATFORM_SYNC_IDS = [
    ('tidal', 'tidal_test-tidal-1'),
    ('youtube', 'youtube_test-yt-hash'),
    ('beatport', 'beatport_sync_test-bp-hash_1234'),
    ('listenbrainz', 'listenbrainz_test-lb-mbid'),
]


class TestPlatformSyncEventDelivery:
    """Platform sync pollers receive sync:progress via WS rooms."""
    @pytest.mark.parametrize('platform,sync_id', PLATFORM_SYNC_IDS)
    def test_platform_sync_not_received_when_unsubscribed(
            self, test_app, shared_state, platform, sync_id):
        """Unsubscribed client does NOT receive platform sync events."""
        app, socketio = test_app
        subscribed = socketio.test_client(app)
        unsubscribed = socketio.test_client(app)
        build = shared_state['build_sync_status']

        subscribed.emit('sync:subscribe', {'playlist_ids': [sync_id]})

        socketio.emit('sync:progress', {
            'playlist_id': sync_id, **build(sync_id)
        }, room=f'sync:{sync_id}')

        sub_events = [e for e in subscribed.get_received()
                      if e['name'] == 'sync:progress']
        unsub_events = [e for e in unsubscribed.get_received()
                        if e['name'] == 'sync:progress']

        assert len(sub_events) >= 1
        assert len(unsub_events) == 0

        subscribed.disconnect()
        unsubscribed.disconnect()

    @pytest.mark.parametrize('platform,sync_id', PLATFORM_SYNC_IDS)
    def test_platform_sync_progress_shape(self, test_app, shared_state, platform, sync_id):
        """Platform sync progress data has expected keys."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_sync_status']

        client.emit('sync:subscribe', {'playlist_ids': [sync_id]})

        payload = {'playlist_id': sync_id, **build(sync_id)}
        socketio.emit('sync:progress', payload, room=f'sync:{sync_id}')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'sync:progress']
        assert len(events) >= 1
        data = events[0]['args'][0]

        assert 'playlist_id' in data
        assert 'status' in data
        assert 'progress' in data
        progress = data['progress']
        assert 'total_tracks' in progress
        assert 'matched_tracks' in progress
        assert 'failed_tracks' in progress
        assert 'progress' in progress

        client.disconnect()

    def test_platform_sync_completion_detection(self, test_app, shared_state):
        """When status changes to 'finished', client detects completion."""
        app, socketio = test_app
        client = socketio.test_client(app)
        ss = shared_state['sync_states']

        sync_id = 'tidal_test-tidal-1'
        client.emit('sync:subscribe', {'playlist_ids': [sync_id]})

        # Mutate to finished
        ss[sync_id]['status'] = 'finished'
        ss[sync_id]['progress']['progress'] = 100
        ss[sync_id]['progress']['matched_tracks'] = 8

        build = shared_state['build_sync_status']
        socketio.emit('sync:progress', {
            'playlist_id': sync_id, **build(sync_id)
        }, room=f'sync:{sync_id}')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'sync:progress']
        data = events[-1]['args'][0]
        assert data['status'] == 'finished'
        assert data['progress']['progress'] == 100

        client.disconnect()

    def test_platform_sync_error_detection(self, test_app, shared_state):
        """When status changes to 'error', client detects the error."""
        app, socketio = test_app
        client = socketio.test_client(app)
        ss = shared_state['sync_states']

        sync_id = 'youtube_test-yt-hash'
        client.emit('sync:subscribe', {'playlist_ids': [sync_id]})

        ss[sync_id]['status'] = 'error'
        ss[sync_id]['error'] = 'Connection lost'

        build = shared_state['build_sync_status']
        socketio.emit('sync:progress', {
            'playlist_id': sync_id, **build(sync_id)
        }, room=f'sync:{sync_id}')

        received = client.get_received()
        events = [e for e in received if e['name'] == 'sync:progress']
        data = events[-1]['args'][0]
        assert data['status'] == 'error'

        client.disconnect()

# =========================================================================
# Group H — Wishlist Stats (broadcast)
# =========================================================================

class TestWishlistStatsEventDelivery:
    """wishlist:stats broadcast events are received by the client."""
    def test_wishlist_stats_data_shape(self, test_app, shared_state):
        """wishlist:stats has is_auto_processing and next_run_in_seconds."""
        app, socketio = test_app
        client = socketio.test_client(app)
        build = shared_state['build_wishlist_stats']

        socketio.emit('wishlist:stats', build())
        received = client.get_received()
        events = [e for e in received if e['name'] == 'wishlist:stats']
        assert len(events) >= 1
        data = events[0]['args'][0]

        assert 'is_auto_processing' in data
        assert 'next_run_in_seconds' in data
        assert isinstance(data['is_auto_processing'], bool)
        assert isinstance(data['next_run_in_seconds'], (int, float))
        client.disconnect()

    def test_wishlist_stats_auto_processing_detection(self, test_app, shared_state):
        """When is_auto_processing is True, client detects it."""
        app, socketio = test_app
        client = socketio.test_client(app)
        ws = shared_state['wishlist_stats_state']

        ws['is_auto_processing'] = True
        ws['next_run_in_seconds'] = 0
        build = shared_state['build_wishlist_stats']

        socketio.emit('wishlist:stats', build())
        received = client.get_received()
        events = [e for e in received if e['name'] == 'wishlist:stats']
        data = events[-1]['args'][0]
        assert data['is_auto_processing'] is True
        client.disconnect()

    def test_wishlist_stats_countdown_value(self, test_app, shared_state):
        """next_run_in_seconds reflects the mutable state."""
        app, socketio = test_app
        client = socketio.test_client(app)
        ws = shared_state['wishlist_stats_state']

        ws['next_run_in_seconds'] = 42
        build = shared_state['build_wishlist_stats']

        socketio.emit('wishlist:stats', build())
        received = client.get_received()
        events = [e for e in received if e['name'] == 'wishlist:stats']
        data = events[-1]['args'][0]
        assert data['next_run_in_seconds'] == 42
        client.disconnect()

    def test_wishlist_stats_multiple_clients(self, test_app, shared_state):
        """Multiple clients each receive wishlist:stats broadcast."""
        app, socketio = test_app
        client1 = socketio.test_client(app)
        client2 = socketio.test_client(app)
        build = shared_state['build_wishlist_stats']

        socketio.emit('wishlist:stats', build())

        for client in [client1, client2]:
            received = client.get_received()
            events = [e for e in received if e['name'] == 'wishlist:stats']
            assert len(events) >= 1

        client1.disconnect()
        client2.disconnect()

    def test_wishlist_stats_matches_http(self, test_app, shared_state):
        """Socket event data matches HTTP endpoint response."""
        app, socketio = test_app
        flask_client = app.test_client()
        ws_client = socketio.test_client(app)
        build = shared_state['build_wishlist_stats']

        http_data = flask_client.get('/api/wishlist/stats').get_json()

        socketio.emit('wishlist:stats', build())
        received = ws_client.get_received()
        events = [e for e in received if e['name'] == 'wishlist:stats']
        ws_data = events[0]['args'][0]

        assert ws_data['is_auto_processing'] == http_data['is_auto_processing']
        assert ws_data['next_run_in_seconds'] == http_data['next_run_in_seconds']
        ws_client.disconnect()
