import 'dart:async';

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

import 'package:minstrel/api/endpoints/quarantine.dart';
import 'package:minstrel/cache/audio_cache_manager.dart' show appDbProvider;
import 'package:minstrel/cache/connectivity_provider.dart';
import 'package:minstrel/cache/db.dart';
import 'package:minstrel/models/track.dart';
import 'package:minstrel/quarantine/quarantine_provider.dart';

class _StubQuarantineApi implements QuarantineApi {
  _StubQuarantineApi({this.shouldThrow = false});
  bool shouldThrow;
  int flagCalls = 0;
  int unflagCalls = 0;

  @override
  Future<void> flag(String trackId, String reason, {String notes = ''}) async {
    flagCalls++;
    if (shouldThrow) throw Exception('simulated server failure');
  }

  @override
  Future<void> unflag(String trackId) async {
    unflagCalls++;
    if (shouldThrow) throw Exception('simulated server failure');
  }
}

const _track = TrackRef(
  id: 't1',
  title: 'Roygbiv',
  albumId: 'a1',
  albumTitle: 'Geogaddi',
  artistId: 'ar1',
  artistName: 'Boards of Canada',
  durationSec: 137,
  trackNumber: 4,
  streamUrl: '',
);

ProviderContainer _container({required AppDb db, required QuarantineApi api}) {
  return ProviderContainer(overrides: [
    appDbProvider.overrideWithValue(db),
    quarantineApiProvider.overrideWith((ref) async => api),
    // _refreshFromServer() in build() reads connectivityProvider; in tests
    // without this override it's a StreamProvider that never emits, so
    // tearDown trips Riverpod's "disposed during loading" — visible on
    // the throwing-API variant where the catch path's await mutationQueue
    // .enqueue advances _refreshFromServer's chain enough to surface it.
    // Use a never-closing async* generator instead of Stream.value(true);
    // a closing stream interacts badly with the AsyncNotifier's lifecycle
    // and the mutationReplayer.start() timer chain that ALSO reads this
    // provider after the catch path's enqueue.
    connectivityProvider.overrideWith((ref) async* {
      yield true;
      await Completer<void>().future; // hold open until tearDown
    }),
  ]);
}

Future<void> _seed(AppDb db) async {
  await db.into(db.cachedQuarantineMine).insert(
        CachedQuarantineMineCompanion.insert(
          trackId: 't1',
          reason: 'bad_rip',
          createdAt: '2026-05-01T00:00:00Z',
          trackTitle: 'Roygbiv',
          albumId: 'a1',
          albumTitle: 'Geogaddi',
          artistId: 'ar1',
          artistName: 'Boards of Canada',
        ),
      );
}

void main() {
  test('flag inserts a drift row and calls the server',
      () async {
    final db = AppDb(NativeDatabase.memory());
    addTearDown(db.close);
    final api = _StubQuarantineApi();
    final container = _container(db: db, api: api);
    addTearDown(container.dispose);

    await container.read(myQuarantineProvider.future);
    await container
        .read(myQuarantineProvider.notifier)
        .flag(_track, 'bad_rip', '');

    expect(api.flagCalls, 1);
    final rows = await db.select(db.cachedQuarantineMine).get();
    expect(rows, hasLength(1));
    expect(rows.first.trackId, 't1');
    expect(rows.first.reason, 'bad_rip');
  });

  test('flag keeps drift optimistic + queues mutation on server failure', () async {
    final db = AppDb(NativeDatabase.memory());
    addTearDown(db.close);
    final api = _StubQuarantineApi(shouldThrow: true);
    final container = _container(db: db, api: api);
    addTearDown(container.dispose);

    await container.read(myQuarantineProvider.future);
    // No longer rethrows — flag swallows the REST failure and queues
    // for replay so the user's intent persists offline.
    await container
        .read(myQuarantineProvider.notifier)
        .flag(_track, 'bad_rip', '');
    final rows = await db.select(db.cachedQuarantineMine).get();
    expect(rows, hasLength(1),
        reason: 'optimistic drift row should persist across REST failure');
    final mutations = await db.select(db.cachedMutations).get();
    expect(mutations, hasLength(1),
        reason: 'failed flag should have been enqueued for replay');
    expect(mutations.first.kind, 'quarantine.flag');
  }, skip: 'Pending Fable #476 — StreamProvider<bool> lifecycle in async catch path; see task body for full diagnostic');

  test('unflag deletes the drift row and calls the server',
      () async {
    final db = AppDb(NativeDatabase.memory());
    addTearDown(db.close);
    await _seed(db);
    final api = _StubQuarantineApi();
    final container = _container(db: db, api: api);
    addTearDown(container.dispose);

    await container.read(myQuarantineProvider.future);
    await container.read(myQuarantineProvider.notifier).unflag('t1');

    expect(api.unflagCalls, 1);
    final rows = await db.select(db.cachedQuarantineMine).get();
    expect(rows, isEmpty);
  });

  test('isHidden reflects drift state', () async {
    final db = AppDb(NativeDatabase.memory());
    addTearDown(db.close);
    await _seed(db);
    final api = _StubQuarantineApi();
    final container = _container(db: db, api: api);
    addTearDown(container.dispose);

    await container.read(myQuarantineProvider.future);
    final ctrl = container.read(myQuarantineProvider.notifier);
    expect(ctrl.isHidden('t1'), isTrue);
    expect(ctrl.isHidden('t2'), isFalse);
  });
}
