import 'package:drift/drift.dart' hide isNull, isNotNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/database/download_operations.dart';
import 'package:plezy/models/download_models.dart';

void main() {
  late AppDatabase db;

  setUp(() {
    db = AppDatabase.forTesting(NativeDatabase.memory());
  });

  tearDown(() async {
    await db.close();
  });

  // ============================================================
  // insertDownload
  // ============================================================

  group('insertDownload', () {
    test('inserts a movie row with defaults', () async {
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '100',
        globalKey: 'srv:100',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );

      final rows = await db.select(db.downloadedMedia).get();
      expect(rows, hasLength(1));
      final r = rows.first;
      expect(r.serverId, 'srv');
      expect(r.ratingKey, '100');
      expect(r.globalKey, 'srv:100');
      expect(r.type, 'movie');
      expect(r.status, DownloadStatus.queued.index);
      expect(r.parentRatingKey, isNull);
      expect(r.grandparentRatingKey, isNull);
      expect(r.mediaIndex, 0);
    });

    test('inserts an episode with parent and grandparent keys', () async {
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: 'ep1',
        globalKey: 'srv:ep1',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.queued.index,
        mediaIndex: 7,
      );

      final row = (await db.select(db.downloadedMedia).get()).single;
      expect(row.parentRatingKey, 'season1');
      expect(row.grandparentRatingKey, 'show1');
      expect(row.mediaIndex, 7);
    });

    test('insertDownload uses InsertMode.insertOrReplace (re-insert overwrites)', () async {
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '100',
        globalKey: 'srv:100',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
      // Mark progress so we can detect a replace.
      await db.updateDownloadProgress('srv:100', 50, 500, 1000);

      // Re-insert with the same globalKey — should replace, resetting progress to default 0.
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '100',
        globalKey: 'srv:100',
        type: 'movie',
        status: DownloadStatus.failed.index,
      );

      final row = (await db.select(db.downloadedMedia).get()).single;
      expect(row.status, DownloadStatus.failed.index);
      expect(row.progress, 0);
      expect(row.downloadedBytes, 0);
    });
  });

  // ============================================================
  // Download queue + getNextQueueItem
  // ============================================================

  group('queue', () {
    test('addToQueue inserts a row with defaults', () async {
      await db.addToQueue(mediaGlobalKey: 'srv:100');

      final rows = await db.select(db.downloadQueue).get();
      expect(rows, hasLength(1));
      expect(rows.first.mediaGlobalKey, 'srv:100');
      expect(rows.first.priority, 0);
      expect(rows.first.downloadSubtitles, isTrue);
      expect(rows.first.downloadArtwork, isTrue);
    });

    test('addToQueue stores custom priority and toggles', () async {
      await db.addToQueue(mediaGlobalKey: 'srv:100', priority: 5, downloadSubtitles: false, downloadArtwork: false);

      final row = (await db.select(db.downloadQueue).get()).single;
      expect(row.priority, 5);
      expect(row.downloadSubtitles, isFalse);
      expect(row.downloadArtwork, isFalse);
    });

    test('addToQueue replaces row with same mediaGlobalKey (unique)', () async {
      await db.addToQueue(mediaGlobalKey: 'srv:100', priority: 1);
      await db.addToQueue(mediaGlobalKey: 'srv:100', priority: 9);

      final rows = await db.select(db.downloadQueue).get();
      expect(rows, hasLength(1));
      expect(rows.first.priority, 9);
    });

    test('removeFromQueue deletes the matching row', () async {
      await db.addToQueue(mediaGlobalKey: 'srv:1');
      await db.addToQueue(mediaGlobalKey: 'srv:2');

      await db.removeFromQueue('srv:1');

      final rows = await db.select(db.downloadQueue).get();
      expect(rows, hasLength(1));
      expect(rows.first.mediaGlobalKey, 'srv:2');
    });

    test('getNextQueueItem returns null when empty', () async {
      expect(await db.getNextQueueItem(), isNull);
    });

    test('getNextQueueItem only returns items whose media is queued', () async {
      // Two items in queue; one's media is still queued, the other is downloading.
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '1',
        globalKey: 'srv:1',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '2',
        globalKey: 'srv:2',
        type: 'movie',
        status: DownloadStatus.downloading.index,
      );

      await db.addToQueue(mediaGlobalKey: 'srv:1', priority: 1);
      await db.addToQueue(mediaGlobalKey: 'srv:2', priority: 10);

      final next = await db.getNextQueueItem();
      expect(next, isNotNull);
      // Should pick srv:1 since srv:2 is downloading (not queued).
      expect(next!.mediaGlobalKey, 'srv:1');
    });

    test('getNextQueueItem orders by priority desc, then addedAt asc', () async {
      // All have queued status
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '1',
        globalKey: 'srv:1',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '2',
        globalKey: 'srv:2',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '3',
        globalKey: 'srv:3',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );

      // Manually inject deterministic addedAt so the test isn't time-dependent.
      final now = DateTime.now().millisecondsSinceEpoch;
      await db
          .into(db.downloadQueue)
          .insert(DownloadQueueCompanion.insert(mediaGlobalKey: 'srv:1', priority: const Value(1), addedAt: now));
      await db
          .into(db.downloadQueue)
          .insert(DownloadQueueCompanion.insert(mediaGlobalKey: 'srv:2', priority: const Value(5), addedAt: now + 100));
      await db
          .into(db.downloadQueue)
          .insert(DownloadQueueCompanion.insert(mediaGlobalKey: 'srv:3', priority: const Value(5), addedAt: now + 50));

      final next = await db.getNextQueueItem();
      // priority 5 wins; srv:3 added before srv:2.
      expect(next!.mediaGlobalKey, 'srv:3');
    });
  });

  // ============================================================
  // Update helpers
  // ============================================================

  group('update helpers', () {
    Future<void> seed({String key = 'srv:100'}) async {
      await db.insertDownload(
        serverId: key.split(':').first,
        ratingKey: key.split(':').last,
        globalKey: key,
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
    }

    test('updateDownloadStatus changes only status', () async {
      await seed();
      await db.updateDownloadStatus('srv:100', DownloadStatus.downloading.index);

      final r = (await db.select(db.downloadedMedia).get()).single;
      expect(r.status, DownloadStatus.downloading.index);
      expect(r.progress, 0); // untouched
    });

    test('updateDownloadProgress writes progress + bytes', () async {
      await seed();
      await db.updateDownloadProgress('srv:100', 42, 4242, 9999);

      final r = (await db.select(db.downloadedMedia).get()).single;
      expect(r.progress, 42);
      expect(r.downloadedBytes, 4242);
      expect(r.totalBytes, 9999);
    });

    test('updateVideoFilePath sets path and downloadedAt timestamp', () async {
      await seed();
      final before = DateTime.now().millisecondsSinceEpoch;
      await db.updateVideoFilePath('srv:100', '/tmp/file.mkv');
      final after = DateTime.now().millisecondsSinceEpoch;

      final r = (await db.select(db.downloadedMedia).get()).single;
      expect(r.videoFilePath, '/tmp/file.mkv');
      expect(r.downloadedAt, isNotNull);
      expect(r.downloadedAt! >= before, isTrue);
      expect(r.downloadedAt! <= after, isTrue);
    });

    test('updateArtworkPaths sets thumbPath; null clears it', () async {
      await seed();
      await db.updateArtworkPaths(globalKey: 'srv:100', thumbPath: '/tmp/thumb.jpg');
      expect((await db.select(db.downloadedMedia).get()).single.thumbPath, '/tmp/thumb.jpg');

      await db.updateArtworkPaths(globalKey: 'srv:100', thumbPath: null);
      expect((await db.select(db.downloadedMedia).get()).single.thumbPath, isNull);
    });

    test('updateDownloadError stores message and increments retryCount', () async {
      await seed();

      await db.updateDownloadError('srv:100', 'first');
      var r = (await db.select(db.downloadedMedia).get()).single;
      expect(r.errorMessage, 'first');
      expect(r.retryCount, 1);

      await db.updateDownloadError('srv:100', 'second');
      r = (await db.select(db.downloadedMedia).get()).single;
      expect(r.errorMessage, 'second');
      expect(r.retryCount, 2);
    });

    test('clearDownloadError nulls the message and resets retryCount to 0', () async {
      await seed();
      await db.updateDownloadError('srv:100', 'oops');

      await db.clearDownloadError('srv:100');
      final r = (await db.select(db.downloadedMedia).get()).single;
      expect(r.errorMessage, isNull);
      expect(r.retryCount, 0);
    });

    test('updateBgTaskId / getBgTaskId round-trip', () async {
      await seed();
      expect(await db.getBgTaskId('srv:100'), isNull);

      await db.updateBgTaskId('srv:100', 'task-abc');
      expect(await db.getBgTaskId('srv:100'), 'task-abc');

      await db.updateBgTaskId('srv:100', null);
      expect(await db.getBgTaskId('srv:100'), isNull);
    });

    test('getBgTaskId on missing globalKey returns null', () async {
      expect(await db.getBgTaskId('does:not-exist'), isNull);
    });
  });

  // ============================================================
  // Lookup helpers
  // ============================================================

  group('lookup helpers', () {
    Future<void> seedTree() async {
      await db.insertDownload(
        serverId: 'srvA',
        ratingKey: 'ep1',
        globalKey: 'srvA:ep1',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );
      await db.insertDownload(
        serverId: 'srvA',
        ratingKey: 'ep2',
        globalKey: 'srvA:ep2',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );
      await db.insertDownload(
        serverId: 'srvA',
        ratingKey: 'ep3',
        globalKey: 'srvA:ep3',
        type: 'episode',
        parentRatingKey: 'season2',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );
      await db.insertDownload(
        serverId: 'srvB',
        ratingKey: 'movie1',
        globalKey: 'srvB:movie1',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
    }

    test('getDownloadedMedia returns the matching row or null', () async {
      await seedTree();

      final hit = await db.getDownloadedMedia('srvA:ep1');
      expect(hit, isNotNull);
      expect(hit!.ratingKey, 'ep1');

      expect(await db.getDownloadedMedia('nope:nope'), isNull);
    });

    test('getEpisodesBySeason filters by parentRatingKey', () async {
      await seedTree();

      final s1 = await db.getEpisodesBySeason('season1');
      expect(s1.map((e) => e.ratingKey).toSet(), {'ep1', 'ep2'});

      final s2 = await db.getEpisodesBySeason('season2');
      expect(s2.map((e) => e.ratingKey).toSet(), {'ep3'});

      expect(await db.getEpisodesBySeason('seasonZ'), isEmpty);
    });

    test('getEpisodesBySeason can filter by server and client scope', () async {
      await db.insertDownload(
        serverId: 'jf',
        clientScopeId: 'jf/user-a',
        ratingKey: 'ep-a',
        globalKey: 'jf:ep-a',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );
      await db.insertDownload(
        serverId: 'jf',
        clientScopeId: 'jf/user-b',
        ratingKey: 'ep-b',
        globalKey: 'jf:ep-b',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );
      await db.insertDownload(
        serverId: 'other',
        ratingKey: 'ep-other',
        globalKey: 'other:ep-other',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );
      await db.insertDownload(
        serverId: 'other',
        clientScopeId: 'other/user-a',
        ratingKey: 'ep-other-scoped',
        globalKey: 'other:ep-other-scoped',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );

      final userA = await db.getEpisodesBySeason('season1', serverId: 'jf', clientScopeId: 'jf/user-a');
      final unscoped = await db.getEpisodesBySeason('season1', serverId: 'other', filterClientScope: true);

      expect(userA.map((e) => e.ratingKey), ['ep-a']);
      expect(unscoped.map((e) => e.ratingKey), ['ep-other']);
    });

    test('getEpisodesByShow filters by grandparentRatingKey', () async {
      await seedTree();

      final all = await db.getEpisodesByShow('show1');
      expect(all.map((e) => e.ratingKey).toSet(), {'ep1', 'ep2', 'ep3'});

      expect(await db.getEpisodesByShow('show-missing'), isEmpty);
    });

    test('getEpisodesByShow can filter by server and client scope', () async {
      await db.insertDownload(
        serverId: 'jf',
        clientScopeId: 'jf/user-a',
        ratingKey: 'ep-a',
        globalKey: 'jf:ep-a',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );
      await db.insertDownload(
        serverId: 'jf',
        clientScopeId: 'jf/user-b',
        ratingKey: 'ep-b',
        globalKey: 'jf:ep-b',
        type: 'episode',
        parentRatingKey: 'season1',
        grandparentRatingKey: 'show1',
        status: DownloadStatus.completed.index,
      );

      final userB = await db.getEpisodesByShow('show1', serverId: 'jf', clientScopeId: 'jf/user-b');

      expect(userB.map((e) => e.ratingKey), ['ep-b']);
    });

    test('getDownloadsByServerId filters by serverId', () async {
      await seedTree();

      final a = await db.getDownloadsByServerId('srvA');
      expect(a.map((e) => e.ratingKey).toSet(), {'ep1', 'ep2', 'ep3'});

      final b = await db.getDownloadsByServerId('srvB');
      expect(b.map((e) => e.ratingKey).toSet(), {'movie1'});

      expect(await db.getDownloadsByServerId('srvZ'), isEmpty);
    });
  });

  // ============================================================
  // Download owners
  // ============================================================

  group('download owners', () {
    Future<void> insertProfile(String id) async {
      await db
          .into(db.profiles)
          .insert(ProfilesCompanion.insert(id: id, kind: 'local', displayName: id, configJson: '{}', createdAt: 0));
    }

    Future<void> insertPlexConnection(String id) async {
      await db
          .into(db.connections)
          .insert(ConnectionsCompanion.insert(id: id, kind: 'plex', displayName: id, configJson: '{}', createdAt: 0));
    }

    test('owner counts ignore orphan local profiles', () async {
      await insertProfile('profile-a');
      await db.addDownloadOwner(profileId: 'profile-a', globalKey: 'srv:100');
      await db.addDownloadOwner(profileId: 'profile-deleted', globalKey: 'srv:100');

      expect(await db.getDownloadOwnerCount('srv:100'), 1);
      expect(await db.hasDownloadOwner('srv:100', excludingProfileId: 'profile-a'), isFalse);
    });

    test('owner counts preserve virtual Plex Home profile ids', () async {
      const plexHomeProfileId = 'plex-home-account-1-00000000-0000-0000-0000-000000000001';
      await insertPlexConnection('account-1');
      await db.addDownloadOwner(profileId: plexHomeProfileId, globalKey: 'srv:100');

      expect(await db.getDownloadOwnerCount('srv:100'), 1);
      expect(await db.hasDownloadOwner('srv:100'), isTrue);
    });

    test('owner counts ignore Plex Home rows whose parent connection is gone', () async {
      const plexHomeProfileId = 'plex-home-missing-account-00000000-0000-0000-0000-000000000001';
      await db.addDownloadOwner(profileId: plexHomeProfileId, globalKey: 'srv:100');

      expect(await db.getDownloadOwnerCount('srv:100'), 0);
      expect(await db.hasDownloadOwner('srv:100'), isFalse);
    });
  });

  // ============================================================
  // deleteDownload — removes from both tables
  // ============================================================

  group('deleteDownload', () {
    test('removes the row from downloadedMedia AND its queue entry', () async {
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '100',
        globalKey: 'srv:100',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
      await db.addToQueue(mediaGlobalKey: 'srv:100');
      await db.insertDownload(
        serverId: 'srv',
        ratingKey: '200',
        globalKey: 'srv:200',
        type: 'movie',
        status: DownloadStatus.queued.index,
      );
      await db.addToQueue(mediaGlobalKey: 'srv:200');

      await db.deleteDownload('srv:100');

      final media = await db.select(db.downloadedMedia).get();
      expect(media.map((m) => m.globalKey).toList(), ['srv:200']);

      final queue = await db.select(db.downloadQueue).get();
      expect(queue.map((q) => q.mediaGlobalKey).toList(), ['srv:200']);
    });

    test('deleteDownload on a missing globalKey is a no-op', () async {
      // Should not throw.
      await db.deleteDownload('nope:nope');
      expect(await db.select(db.downloadedMedia).get(), isEmpty);
      expect(await db.select(db.downloadQueue).get(), isEmpty);
    });
  });
}
