import 'dart:async';
import 'dart:io';

import 'package:drift/drift.dart' show ApplyInterceptor, QueryExecutor, QueryExecutorUser, QueryInterceptor;
import 'package:drift/native.dart';
import 'package:flutter/material.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/database/tvos_database_recovery_store.dart';
import 'package:plezy/main.dart';
import 'package:plezy/media/ids.dart';
import 'package:plezy/models/download_models.dart';
import 'package:plezy/services/base_shared_preferences_service.dart';

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

final class _OpenTrackingInterceptor extends QueryInterceptor {
  _OpenTrackingInterceptor({this.failure});

  final Object? failure;
  var ensureOpenCalls = 0;
  var ensureOpenCompleted = false;
  var closed = false;

  @override
  Future<bool> ensureOpen(QueryExecutor executor, QueryExecutorUser user) async {
    ensureOpenCalls++;
    final failure = this.failure;
    if (failure != null) throw failure;

    final result = await executor.ensureOpen(user);
    ensureOpenCompleted = true;
    return result;
  }

  @override
  Future<void> close(QueryExecutor inner) async {
    await inner.close();
    closed = true;
  }
}

void main() {
  testWidgets('renders a Flutter frame before starting the initialization gate', (tester) async {
    final completion = Completer<int>();
    var bootstrapWasMounted = false;

    await tester.pumpWidget(
      StartupBootstrap<int>(
        initialize: () {
          bootstrapWasMounted = find.byKey(startupBootstrapProgressKey).evaluate().isNotEmpty;
          return completion.future;
        },
        buildApp: (_, value) => MaterialApp(home: Text('ready $value')),
      ),
    );

    expect(bootstrapWasMounted, isTrue);
    expect(find.byKey(startupBootstrapProgressKey), findsOneWidget);

    completion.complete(1);
    await tester.pump();
  });

  testWidgets('replaces bootstrap UI with the initialized app on success', (tester) async {
    final completion = Completer<int>();

    await tester.pumpWidget(
      StartupBootstrap<int>(
        initialize: () => completion.future,
        buildApp: (_, value) => MaterialApp(home: Text('ready $value')),
      ),
    );

    completion.complete(7);
    await tester.pump();

    expect(find.text('ready 7'), findsOneWidget);
    expect(find.byKey(startupBootstrapProgressKey), findsNothing);
  });

  testWidgets('shows a localized recoverable failure instead of removing Flutter UI', (tester) async {
    await tester.pumpWidget(
      StartupBootstrap<int>(
        initialize: () async => throw StateError('database unavailable'),
        buildApp: (_, value) => Text('ready $value'),
      ),
    );
    await tester.pump();

    expect(find.byKey(startupBootstrapFailureKey), findsOneWidget);
    expect(find.text('Error'), findsOneWidget);
    expect(find.text('Retry'), findsOneWidget);
  });

  testWidgets('retry clears the failed generation and can commit a later success', (tester) async {
    final retryCompletion = Completer<int>();
    var attempts = 0;

    await tester.pumpWidget(
      StartupBootstrap<int>(
        initialize: () {
          attempts++;
          if (attempts == 1) return Future<int>.error(StateError('first attempt'));
          return retryCompletion.future;
        },
        buildApp: (_, value) => MaterialApp(home: Text('ready $value')),
      ),
    );
    await tester.pump();

    await tester.tap(find.byKey(startupBootstrapRetryKey));
    await tester.pump();
    expect(attempts, 2);
    expect(find.byKey(startupBootstrapProgressKey), findsOneWidget);

    retryCompletion.complete(42);
    await tester.pump();

    expect(find.text('ready 42'), findsOneWidget);
    expect(find.byKey(startupBootstrapFailureKey), findsNothing);
  });

  testWidgets('discards a completion from a disposed bootstrap generation', (tester) async {
    final completion = Completer<int>();
    final discarded = <int>[];

    await tester.pumpWidget(
      StartupBootstrap<int>(
        initialize: () => completion.future,
        buildApp: (_, value) => Text('ready $value'),
        discard: discarded.add,
      ),
    );

    await tester.pumpWidget(const SizedBox.shrink());
    completion.complete(9);
    await tester.pump();

    expect(discarded, [9]);
    expect(find.text('ready 9'), findsNothing);
  });

  test('storage-full lazy database open discards native work before retrying', () async {
    resetSharedPreferencesForTest();
    final tempDir = await Directory.systemTemp.createTemp('plezy_startup_storage_full_');
    final file = File('${tempDir.path}/plezy_downloads.db');
    final prefs = await BaseSharedPreferencesService.sharedCache();
    final failedOpen = _OpenTrackingInterceptor(
      failure: const FileSystemException('write failed: No space left on device'),
    );
    final successfulOpen = _OpenTrackingInterceptor();
    AppDatabase? seeded;
    AppDatabase? resultDatabase;

    try {
      seeded = AppDatabase.forTesting(NativeDatabase(file));
      await seeded.insertDownload(
        serverId: ServerId('srv'),
        ratingKey: 'active',
        globalKey: 'srv:active',
        type: 'movie',
        status: DownloadStatus.downloading.index,
      );
      await seeded.updateBgTaskId('srv:active', 'native-task');
      await seeded.addToQueue(mediaGlobalKey: 'srv:active');
      await seeded.insertDownload(
        serverId: ServerId('srv'),
        ratingKey: 'complete',
        globalKey: 'srv:complete',
        type: 'movie',
        status: DownloadStatus.completed.index,
      );
      await seeded.close();
      seeded = null;

      var openAttempts = 0;
      var recoveries = 0;
      final bootstrap = await openAppDatabaseWithDownloadRecovery(
        openDatabase: () {
          return AppDatabase.open(
            isTvos: false,
            databaseFile: file,
            preferences: prefs,
            executorFactory: (databaseFile) {
              openAttempts++;
              final interceptor = openAttempts == 1 ? failedOpen : successfulOpen;
              return NativeDatabase(databaseFile).interceptWith(interceptor);
            },
          );
        },
        recoverNativeDownloads: () async {
          expect(failedOpen.closed, isTrue);
          recoveries++;
        },
        storageFullMessage: 'Storage full',
      );
      resultDatabase = bootstrap.database;

      final active = await resultDatabase.getDownloadedMedia('srv:active');
      final complete = await resultDatabase.getDownloadedMedia('srv:complete');
      expect(bootstrap.recoveryOutcome, TvosDatabaseRecoveryOutcome.notApplicable);
      expect(openAttempts, 2);
      expect(recoveries, 1);
      expect(failedOpen.ensureOpenCalls, 1);
      expect(successfulOpen.ensureOpenCalls, greaterThanOrEqualTo(1));
      expect(successfulOpen.ensureOpenCompleted, isTrue);
      expect(active?.status, DownloadStatus.failed.index);
      expect(active?.bgTaskId, isNull);
      expect(active?.errorMessage, 'Storage full');
      expect(complete?.status, DownloadStatus.completed.index);
      expect(await resultDatabase.select(resultDatabase.downloadQueue).get(), isEmpty);
      expect(await resultDatabase.customSelect('SELECT 1').get(), isNotEmpty);
    } finally {
      await resultDatabase?.close();
      await seeded?.close();
      await tempDir.delete(recursive: true);
    }
  });
  test('non-storage lazy database-open errors bypass download recovery', () async {
    resetSharedPreferencesForTest();
    final tempDir = await Directory.systemTemp.createTemp('plezy_startup_open_error_');
    final file = File('${tempDir.path}/plezy_downloads.db');
    final prefs = await BaseSharedPreferencesService.sharedCache();
    final error = StateError('injected database setup failure');
    final failedOpen = _OpenTrackingInterceptor(failure: error);
    var openAttempts = 0;
    var recoveries = 0;

    try {
      final open = openAppDatabaseWithDownloadRecovery(
        openDatabase: () {
          return AppDatabase.open(
            isTvos: false,
            databaseFile: file,
            preferences: prefs,
            executorFactory: (databaseFile) {
              openAttempts++;
              return NativeDatabase(databaseFile).interceptWith(failedOpen);
            },
          );
        },
        recoverNativeDownloads: () async {
          recoveries++;
        },
        storageFullMessage: 'Storage full',
      );

      await expectLater(open, throwsA(same(error)));
      expect(failedOpen.ensureOpenCalls, 1);
      expect(failedOpen.closed, isTrue);
      expect(openAttempts, 1);
      expect(recoveries, 0);
    } finally {
      await tempDir.delete(recursive: true);
    }
  });
}
