import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import 'package:tane/state/inventory_cubit.dart';

import '../support/test_support.dart';

/// Waits until the cubit's state satisfies [predicate], whether it already
/// does or a later stream emission gets it there.
Future<InventoryState> waitFor(
  InventoryCubit cubit,
  bool Function(InventoryState) predicate,
) async {
  if (predicate(cubit.state)) return cubit.state;
  return cubit.stream.firstWhere(predicate).timeout(const Duration(seconds: 5));
}

void main() {
  late AppDatabase db;
  late VarietyRepository repo;
  late InventoryCubit cubit;

  setUp(() {
    db = newTestDatabase();
    repo = newTestRepository(db);
    cubit = InventoryCubit(repo);
  });
  tearDown(() async {
    await cubit.close();
    await db.close();
  });

  test('subscribes to the inventory and leaves loading', () async {
    await repo.addQuickVariety(label: 'Maize');
    final state = await waitFor(cubit, (s) => !s.loading && s.items.isNotEmpty);
    expect(state.items.single.label, 'Maize');
  });

  test('drafts stay in the tray and never mix into the list', () async {
    await repo.addDraftVariety(Uint8List.fromList([1, 2, 3]));
    var state = await waitFor(cubit, (s) => s.drafts.isNotEmpty);
    expect(state.items, isEmpty);

    // Naming the draft promotes it: tray empties, list gains the item.
    await repo.nameDraft(state.drafts.single.id, 'Named at last');
    state = await waitFor(cubit, (s) => s.items.isNotEmpty && s.drafts.isEmpty);
    expect(state.items.single.label, 'Named at last');
  });

  test('search narrows visibleItems by label, case-insensitively', () async {
    await repo.addQuickVariety(label: 'Tomate rosa');
    await repo.addQuickVariety(label: 'Lechuga');
    await waitFor(cubit, (s) => s.items.length == 2);

    cubit.search('TOMA');
    expect(cubit.state.visibleItems.single.label, 'Tomate rosa');

    cubit.search('');
    expect(cubit.state.visibleItems, hasLength(2));
  });

  test('toggleCategory adds then removes the filter', () async {
    await repo.addQuickVariety(label: 'Maize', category: 'Poaceae');
    await repo.addQuickVariety(label: 'Bean', category: 'Fabaceae');
    await waitFor(cubit, (s) => s.items.length == 2);

    cubit.toggleCategory('Poaceae');
    expect(cubit.state.visibleItems.single.label, 'Maize');

    cubit.toggleCategory('Poaceae');
    expect(cubit.state.visibleItems, hasLength(2));
  });

  test('toggleType keeps only varieties holding a lot of that form', () async {
    final seedId = await repo.addQuickVariety(label: 'Seedy');
    await repo.addLot(varietyId: seedId, type: LotType.seed);
    final plantId = await repo.addQuickVariety(label: 'Planty');
    await repo.addLot(varietyId: plantId, type: LotType.plant);
    await waitFor(
      cubit,
      (s) => s.items.length == 2 && s.items.every((i) => i.lotTypes.isNotEmpty),
    );

    cubit.toggleType(LotType.plant);
    expect(cubit.state.visibleItems.single.label, 'Planty');
  });

  test('organic and needs-reproduction filters', () async {
    final ecoId = await repo.addQuickVariety(label: 'Eco');
    await repo.updateVariety(id: ecoId, isOrganic: true);
    final tiredId = await repo.addQuickVariety(label: 'Tired');
    await repo.updateVariety(id: tiredId, needsReproduction: true);
    await waitFor(cubit, (s) => s.hasOrganic && s.hasNeedsReproduction);

    cubit.toggleOrganicOnly();
    expect(cubit.state.visibleItems.single.label, 'Eco');
    cubit.toggleOrganicOnly();

    cubit.toggleNeedsReproductionOnly();
    expect(cubit.state.visibleItems.single.label, 'Tired');
  });

  test('sharing filter keeps only varieties with an offered lot', () async {
    final sharedId = await repo.addQuickVariety(label: 'Shared');
    await repo.addLot(varietyId: sharedId, offerStatus: OfferStatus.shared);
    final privateId = await repo.addQuickVariety(label: 'Private');
    await repo.addLot(varietyId: privateId);
    await waitFor(cubit, (s) => s.hasShared && s.items.length == 2);

    cubit.toggleSharingOnly();
    expect(cubit.state.visibleItems.single.label, 'Shared');

    cubit.toggleSharingOnly();
    expect(cubit.state.visibleItems, hasLength(2));
  });

  test('clearFilters resets every filter but keeps the search query', () async {
    await repo.addQuickVariety(label: 'Maize', category: 'Poaceae');
    await waitFor(cubit, (s) => s.items.isNotEmpty);

    cubit
      ..search('mai')
      ..toggleCategory('Poaceae')
      ..toggleType(LotType.seed)
      ..toggleOrganicOnly()
      ..toggleNeedsReproductionOnly()
      ..toggleSharingOnly();
    cubit.clearFilters();

    expect(cubit.state.categoryFilter, isEmpty);
    expect(cubit.state.typeFilter, isEmpty);
    expect(cubit.state.organicOnly, isFalse);
    expect(cubit.state.needsReproductionOnly, isFalse);
    expect(cubit.state.sharingOnly, isFalse);
    expect(cubit.state.query, 'mai');
  });

  test('categories dedupes and preserves first-seen order', () async {
    await repo.addQuickVariety(label: 'A', category: 'Poaceae');
    await repo.addQuickVariety(label: 'B', category: 'Fabaceae');
    await repo.addQuickVariety(label: 'C', category: 'Poaceae');
    final state = await waitFor(cubit, (s) => s.items.length == 3);

    expect(state.categories, hasLength(2));
    expect(state.categories.toSet(), {'Poaceae', 'Fabaceae'});
  });

  group('selection mode', () {
    test('enterSelection starts the mode with one id', () async {
      await repo.addQuickVariety(label: 'Bean');
      final state = await waitFor(cubit, (s) => s.items.isNotEmpty);
      final id = state.items.single.id;

      cubit.enterSelection(id);
      expect(cubit.state.selectionMode, isTrue);
      expect(cubit.state.selectedIds, {id});
    });

    test('toggleSelection adds then removes, staying in mode', () async {
      await repo.addQuickVariety(label: 'Bean');
      final state = await waitFor(cubit, (s) => s.items.isNotEmpty);
      final id = state.items.single.id;

      cubit.toggleSelection(id);
      expect(cubit.state.selectedIds, {id});

      cubit.toggleSelection(id);
      expect(cubit.state.selectedIds, isEmpty);
      expect(cubit.state.selectionMode, isTrue);
    });

    test('selectAllVisible selects only the filtered subset', () async {
      await repo.addQuickVariety(label: 'Bean', category: 'Fabaceae');
      await repo.addQuickVariety(label: 'Tomato', category: 'Solanaceae');
      final state = await waitFor(cubit, (s) => s.items.length == 2);
      final beanId = state.items.firstWhere((i) => i.label == 'Bean').id;

      cubit.toggleCategory('Fabaceae'); // hide the tomato
      cubit.selectAllVisible();
      expect(cubit.state.selectedIds, {beanId});
    });

    test('exitSelection leaves the mode and clears the selection', () async {
      await repo.addQuickVariety(label: 'Bean');
      final state = await waitFor(cubit, (s) => s.items.isNotEmpty);
      cubit.enterSelection(state.items.single.id);

      cubit.exitSelection();
      expect(cubit.state.selectionMode, isFalse);
      expect(cubit.state.selectedIds, isEmpty);
    });
  });
}
