import 'package:drift/native.dart';
import 'package:plezy/media/ids.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/models/external_player_models.dart';
import 'package:plezy/services/external_player_service.dart';
import 'package:plezy/services/jellyfin_api_cache.dart';
import 'package:plezy/services/multi_server_manager.dart';
import 'package:plezy/services/offline_watch_sync_service.dart';
import 'package:plezy/utils/active_client_scope.dart';
import 'package:plezy/utils/watch_state_notifier.dart';
import '../test_helpers/media_items.dart';
import '../test_helpers/playback_report_fakes.dart';

class _RecordingClient with PlaybackReportRecorder implements MediaServerClient, ScopedMediaServerClient {
  _RecordingClient({this.backend = MediaBackend.plex, String? scopedServerId})
    : scopedServerId =
          scopedServerId ??
          (backend == MediaBackend.plex
              ? buildPlexProfileScopeId(serverId: ServerId('srv'), profileId: 'profile-a')
              : 'srv/user-a');

  bool failStart = false;
  bool failStop = false;
  final started = <({int positionMs, int? durationMs})>[];
  final stopped = <({int positionMs, int? durationMs})>[];
  final watched = <String>[];

  @override
  ServerId get serverId => ServerId('srv');

  @override
  final MediaBackend backend;
  @override
  final String scopedServerId;

  @override
  double get watchedThreshold => 0.9;

  // Mirror the real clients: Jellyfin marks played from the stopped report, so
  // the external-player completion path emits only the local watch event
  // (#1287); Plex needs the explicit markWatched.
  @override
  bool get marksWatchedOnPlaybackStopped => backend == MediaBackend.jellyfin;

  @override
  Future<void> onPlaybackReport(PlaybackReportCall call) async {
    final entry = (positionMs: call.position.inMilliseconds, durationMs: call.duration?.inMilliseconds);
    switch (call.kind) {
      case PlaybackReportKind.started:
        started.add(entry);
        if (failStart) throw StateError('start failed');
      case PlaybackReportKind.progress:
        throw UnimplementedError();
      case PlaybackReportKind.stopped:
        stopped.add(entry);
        if (failStop) throw StateError('stop failed');
    }
  }

  @override
  Future<void> markWatched(MediaItem item) async {
    watched.add(item.id);
  }

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

MediaItem _item({int? durationMs}) {
  return testMediaItem(
    id: 'item-1',
    backend: MediaBackend.plex,
    kind: MediaKind.movie,
    serverId: 'srv',
    durationMs: durationMs,
  );
}

void main() {
  test('MX Player Android package candidates include free and Pro variants', () {
    final mxPlayer = KnownPlayers.findById('mx_player');

    expect(mxPlayer, isNotNull);
    expect(KnownPlayers.androidPackageCandidates(mxPlayer!), [
      'com.mxtech.videoplayer.ad',
      'com.mxtech.videoplayer.pro',
    ]);
  });

  test('Android external progress preserves null duration and still stops after start failure', () async {
    final client = _RecordingClient()..failStart = true;

    await ExternalPlayerService.reportAndroidExternalProgressForTesting(
      positionMs: 5000,
      durationMs: null,
      metadata: _item(),
      client: client,
    );

    expect(client.started, [(positionMs: 5000, durationMs: null)]);
    expect(client.stopped, [(positionMs: 5000, durationMs: null)]);
  });

  test('Android external progress queues unknown-duration resume when no client is available', () async {
    final db = AppDatabase.forTesting(NativeDatabase.memory());
    JellyfinApiCache.initialize(db);
    final manager = MultiServerManager();
    final service = OfflineWatchSyncService(database: db, serverManager: manager);
    addTearDown(() async {
      service.dispose();
      manager.dispose();
      await db.close();
    });

    await ExternalPlayerService.reportAndroidExternalProgressForTesting(
      positionMs: 5000,
      durationMs: null,
      metadata: _item(),
      client: null,
      offlineWatchService: service,
    );

    final action = await db.getLatestWatchAction('srv:item-1');
    expect(action, isNotNull);
    expect(action!.viewOffset, 5000);
    expect(action.duration, isNull);
    expect(action.shouldMarkWatched, isFalse);
  });

  test('Android external progress emits the exact client cache scope', () async {
    final scope = buildPlexProfileScopeId(serverId: ServerId('srv'), profileId: 'profile-a');
    final client = _RecordingClient(scopedServerId: scope);
    final events = <WatchStateEvent>[];
    final subscription = WatchStateNotifier()
        .forItem('item-1')
        .where((event) => event.changeType == WatchStateChangeType.progressUpdate)
        .listen(events.add);
    addTearDown(subscription.cancel);

    await ExternalPlayerService.reportAndroidExternalProgressForTesting(
      positionMs: 5000,
      durationMs: 100000,
      metadata: _item(durationMs: 100000),
      client: client,
    );
    await Future<void>.delayed(Duration.zero);

    expect(events, hasLength(1));
    expect(events.single.cacheServerId, scope);
  });

  test('Android external progress ignores missing position without explicit completion', () async {
    final client = _RecordingClient();

    await ExternalPlayerService.reportAndroidExternalProgressForTesting(
      positionMs: null,
      durationMs: 100000,
      playbackCompleted: false,
      metadata: _item(durationMs: 100000),
      client: client,
    );

    expect(client.started, isEmpty);
    expect(client.stopped, isEmpty);
  });

  test('Android external progress reports full duration for explicit completion', () async {
    final client = _RecordingClient();

    await ExternalPlayerService.reportAndroidExternalProgressForTesting(
      positionMs: null,
      durationMs: 100000,
      playbackCompleted: true,
      metadata: _item(durationMs: 100000),
      client: client,
    );

    expect(client.started, [(positionMs: 100000, durationMs: 100000)]);
    expect(client.stopped, [(positionMs: 100000, durationMs: 100000)]);
    expect(client.watched, ['item-1']);
  });

  test('Android external completion on Jellyfin marks watched via the stop report, not markWatched (#1287)', () async {
    final client = _RecordingClient(backend: MediaBackend.jellyfin);

    await ExternalPlayerService.reportAndroidExternalProgressForTesting(
      positionMs: null,
      durationMs: 100000,
      playbackCompleted: true,
      metadata: _item(durationMs: 100000),
      client: client,
    );

    // The stopped report at full duration marks it played server-side…
    expect(client.stopped, [(positionMs: 100000, durationMs: 100000)]);
    // …so the explicit markWatched is skipped — issuing it would double-scrobble
    // through the Jellyfin Trakt plugin.
    expect(client.watched, isEmpty);
  });
}
