import 'package:commons_core/commons_core.dart';
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/variety_detail_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<VarietyDetailState> waitFor(
  VarietyDetailCubit cubit,
  bool Function(VarietyDetailState) 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;

  setUp(() {
    db = newTestDatabase();
    repo = newTestRepository(db);
  });
  tearDown(() => db.close());

  Future<(VarietyDetailCubit, String)> newCubit() async {
    final id = await repo.addQuickVariety(label: 'Maize');
    return (VarietyDetailCubit(repo, id), id);
  }

  test('streams the detail and leaves loading', () async {
    final (cubit, _) = await newCubit();
    final state = await waitFor(cubit, (s) => s.detail != null);
    expect(state.detail!.label, 'Maize');
    await cubit.close();
  });

  test('updateFields re-emits the edited detail', () async {
    final (cubit, _) = await newCubit();
    await waitFor(cubit, (s) => s.detail != null);

    await cubit.updateFields(label: 'Corn', notes: 'From the fair');
    final state = await waitFor(cubit, (s) => s.detail?.label == 'Corn');
    expect(state.detail!.notes, 'From the fair');
    await cubit.close();
  });

  test('addLot, updateLot and deleteLot flow through the stream', () async {
    final (cubit, _) = await newCubit();
    await waitFor(cubit, (s) => s.detail != null);

    await cubit.addLot(
      year: 2024,
      quantity: const Quantity(kind: QuantityKind.cob, count: 2),
    );
    var state = await waitFor(cubit, (s) => s.detail?.lots.isNotEmpty == true);
    final lot = state.detail!.lots.single;
    expect(lot.harvestYear, 2024);

    await cubit.updateLot(lotId: lot.id, type: LotType.seed, year: 2023);
    state = await waitFor(
      cubit,
      (s) => s.detail?.lots.single.harvestYear == 2023,
    );

    await cubit.deleteLot(lot.id);
    state = await waitFor(cubit, (s) => s.detail?.lots.isEmpty == true);
    expect(state.detail!.lots, isEmpty);
    await cubit.close();
  });

  test('vernacular names can be added and removed', () async {
    final (cubit, _) = await newCubit();
    await waitFor(cubit, (s) => s.detail != null);

    await cubit.addVernacularName('Millo');
    var state = await waitFor(
      cubit,
      (s) => s.detail?.vernacularNames.isNotEmpty == true,
    );
    expect(state.detail!.vernacularNames.single.name, 'Millo');

    await cubit.removeVernacularName(state.detail!.vernacularNames.single.id);
    state = await waitFor(
      cubit,
      (s) => s.detail?.vernacularNames.isEmpty == true,
    );
    expect(state.detail!.vernacularNames, isEmpty);
    await cubit.close();
  });

  test('deleteVariety flags deleted and the stream then yields null', () async {
    final (cubit, _) = await newCubit();
    await waitFor(cubit, (s) => s.detail != null);

    await cubit.deleteVariety();
    expect(cubit.state.deleted, isTrue);

    // The reactive watch notices the tombstone and re-emits null.
    final state = await waitFor(cubit, (s) => s.detail == null);
    expect(state.deleted, isTrue);
    await cubit.close();
  });
}
