import 'dart:io' show Platform;

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/mpv/player/platform/player_android.dart';
import 'package:plezy/mpv/player/player_native.dart';
import 'package:plezy/services/settings_service.dart';

import '../test_helpers/prefs.dart';

/// Guards the channel contract: every property [PlayerBase.handlePropertyChange]
/// depends on for core state must be registered by each backend at init.
/// The Android ExoPlayer plugin replays exactly these registrations into a
/// fallback MPV core, so a missing registration here silently breaks the
/// event stream after a backend switch.
void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  setUp(() async {
    resetSharedPreferencesForTest();
    SettingsService.resetForTesting();
    await SettingsService.getInstance();
  });

  const coreNames = {
    'time-pos',
    'duration',
    'seekable',
    'pause',
    'paused-for-cache',
    'eof-reached',
    'volume',
    'speed',
    'aid',
    'sid',
    'track-list',
  };

  Future<List<MethodCall>> capturedObservations({
    required String channelName,
    required Future<void> Function() initialize,
    required Future<void> Function() dispose,
  }) async {
    final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
    final methodChannel = MethodChannel(channelName);
    final observations = <MethodCall>[];

    messenger.setMockMethodCallHandler(methodChannel, (call) async {
      if (call.method == 'observeProperty') observations.add(call);
      if (call.method == 'initialize') return true;
      return null;
    });
    try {
      await initialize();
    } finally {
      await dispose();
      messenger.setMockMethodCallHandler(methodChannel, null);
    }
    return observations;
  }

  Set<String> names(List<MethodCall> calls) => calls.map((c) => (c.arguments as Map)['name'] as String).toSet();

  test('ExoPlayer registers the core properties (plus its cache extra)', () async {
    final player = PlayerAndroid();
    final observations = await capturedObservations(
      channelName: 'com.plezy/exo_player',
      initialize: () => player.requestAudioFocus(), // forces _ensureInitialized
      dispose: () => player.dispose(),
    );

    final registered = names(observations);
    expect(registered, containsAll(coreNames));
    expect(registered, contains('demuxer-cache-time'));
    for (final call in observations) {
      final args = call.arguments as Map;
      expect(args['format'], isNotNull);
      expect(args['id'], isA<int>());
    }
  });

  test('PlayerAndroid forwards only explicit playback restart events', () async {
    final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
    const channel = MethodChannel('com.plezy/exo_player');
    messenger.setMockMethodCallHandler(channel, (_) async => null);
    addTearDown(() => messenger.setMockMethodCallHandler(channel, null));

    final player = PlayerAndroid();
    var restartCount = 0;
    final subscription = player.streams.playbackRestart.listen((_) => restartCount++);
    addTearDown(() async {
      await subscription.cancel();
      await player.dispose();
    });

    player.handlePropertyChange('paused-for-cache', false);
    player.handlePropertyChange('time-pos', 12.0);
    await Future<void>.delayed(Duration.zero);
    expect(restartCount, 0);

    player.handlePlayerEvent('playback-restart', null);
    await Future<void>.delayed(Duration.zero);
    expect(restartCount, 1);
  });

  test('mpv registers the core properties (plus its track/device extras)', () async {
    final player = PlayerNative();
    final observations = await capturedObservations(
      channelName: 'com.plezy/mpv_player',
      initialize: () => player.setLogLevel('warn'), // forces _ensureInitialized
      dispose: () => player.dispose(),
    );

    final registered = names(observations);
    expect(registered, containsAll(coreNames));
    expect(registered, containsAll({'secondary-sid', 'demuxer-cache-state', 'audio-device-list', 'audio-device'}));
    final structuredFormat = Platform.isAndroid ? 'string' : 'node';
    for (final call in observations.where((call) {
      final name = (call.arguments as Map)['name'];
      return name == 'track-list' || name == 'demuxer-cache-state' || name == 'audio-device-list';
    })) {
      expect((call.arguments as Map)['format'], structuredFormat);
    }
  });
}
