import 'dart:io';

import 'package:dio/dio.dart';
import 'package:drift/drift.dart' show Value;
import 'package:drift/native.dart' show NativeDatabase;
import 'package:flutter_test/flutter_test.dart';

import 'package:minstrel/cache/audio_cache_manager.dart';
import 'package:minstrel/cache/db.dart';

AppDb _testDb() => AppDb(NativeDatabase.memory());

void main() {
  test('isCached returns false when no row exists', () async {
    final db = _testDb();
    addTearDown(db.close);
    final mgr = AudioCacheManager(
      db: db,
      dioFactory: () async => Dio(),
      cacheDirFactory: () async => Directory.systemTemp.createTempSync(),
    );
    expect(await mgr.isCached('nonexistent'), false);
    expect(await mgr.pathFor('nonexistent'), null);
  });

  test('bucketUsage sums drift sizeBytes across rows', () async {
    final db = _testDb();
    addTearDown(db.close);
    final tmp = Directory.systemTemp.createTempSync();
    final mgr = AudioCacheManager(
      db: db,
      dioFactory: () async => Dio(),
      cacheDirFactory: () async => tmp,
    );
    // Each row needs a distinct `path` because path is not the primary
    // key, but mapping by trackId only works if rows are distinct.
    await db.batch((b) {
      b.insertAll(db.audioCacheIndex, [
        AudioCacheIndexCompanion.insert(
            trackId: 'a',
            path: 'p_a',
            sizeBytes: 100,
            source: CacheSource.manual),
        AudioCacheIndexCompanion.insert(
            trackId: 'b',
            path: 'p_b',
            sizeBytes: 250,
            source: CacheSource.incidental),
      ]);
    });
    // usageBytes() is a directory walk (authoritative on-disk total,
    // catches orphan partials); for a drift-sizeBytes sum, bucketUsage
    // is the correct API. With empty liked set, both rows go to rolling.
    final usage = await mgr.bucketUsage(const <String>{});
    expect(usage.liked + usage.rolling, 350);
  });

  test('rolling cap evicts non-liked LRU; liked protected', () async {
    final db = _testDb();
    addTearDown(db.close);
    final tmp = Directory.systemTemp.createTempSync();
    final mgr = AudioCacheManager(
      db: db,
      dioFactory: () async => Dio(),
      cacheDirFactory: () async => tmp,
    );
    Future<void> mk(String id) async {
      final f = File('${tmp.path}/audio_cache/$id.mp3');
      await f.create(recursive: true);
      await f.writeAsBytes(List.filled(100, 0));
    }
    await mk('old');
    await mk('new');
    await mk('lik');
    await db.batch((b) {
      b.insertAll(db.audioCacheIndex, [
        // 'old' has the older lastPlayedAt → evicted first.
        AudioCacheIndexCompanion.insert(
            trackId: 'old',
            path: '${tmp.path}/audio_cache/old.mp3',
            sizeBytes: 100,
            source: CacheSource.incidental,
            lastPlayedAt: Value(DateTime(2020))),
        AudioCacheIndexCompanion.insert(
            trackId: 'new',
            path: '${tmp.path}/audio_cache/new.mp3',
            sizeBytes: 100,
            source: CacheSource.incidental,
            lastPlayedAt: Value(DateTime(2024))),
        AudioCacheIndexCompanion.insert(
            trackId: 'lik',
            path: '${tmp.path}/audio_cache/lik.mp3',
            sizeBytes: 100,
            source: CacheSource.incidental,
            lastPlayedAt: Value(DateTime(2019))),
      ]);
    });
    final liked = {'lik'};
    final u = await mgr.bucketUsage(liked);
    expect(u.liked, 100);
    expect(u.rolling, 200);
    // Rolling cap fits one 100-byte file; Liked cap huge.
    await mgr.evictBuckets(
        likedCap: 1 << 30, rollingCap: 100, liked: liked);
    expect(await mgr.isCached('old'), false); // oldest rolling, evicted
    expect(await mgr.isCached('new'), true); // newer rolling, kept
    expect(await mgr.isCached('lik'), true); // liked, protected
  });

  test('clearAll removes everything including manual', () async {
    final db = _testDb();
    addTearDown(db.close);
    final tmp = Directory.systemTemp.createTempSync();
    final mgr = AudioCacheManager(
      db: db,
      dioFactory: () async => Dio(),
      cacheDirFactory: () async => tmp,
    );
    final f = File('${tmp.path}/audio_cache/man.mp3');
    await f.create(recursive: true);
    await f.writeAsBytes(List.filled(50, 0));
    await db.into(db.audioCacheIndex).insertOnConflictUpdate(
          AudioCacheIndexCompanion.insert(
              trackId: 'man',
              path: f.path,
              sizeBytes: 50,
              source: CacheSource.manual),
        );
    expect(await mgr.usageBytes(), 50);
    await mgr.clearAll();
    expect(await mgr.usageBytes(), 0);
    expect(await mgr.isCached('man'), false);
  });

  test('unpin removes index row + deletes file', () async {
    final db = _testDb();
    addTearDown(db.close);
    final tmp = Directory.systemTemp.createTempSync();
    final mgr = AudioCacheManager(
      db: db,
      dioFactory: () async => Dio(),
      cacheDirFactory: () async => tmp,
    );
    final f = File('${tmp.path}/audio_cache/x.mp3');
    await f.create(recursive: true);
    await f.writeAsBytes(List.filled(10, 0));
    await db.into(db.audioCacheIndex).insertOnConflictUpdate(
          AudioCacheIndexCompanion.insert(
              trackId: 'x',
              path: f.path,
              sizeBytes: 10,
              source: CacheSource.autoPrefetch),
        );
    expect(await mgr.isCached('x'), true);
    await mgr.unpin('x');
    expect(await mgr.isCached('x'), false);
    expect(f.existsSync(), false);
  });
}
