import 'package:commons_core/commons_core.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter_test/flutter_test.dart'; import 'package:tane/data/species_catalog.dart'; import 'package:tane/data/species_repository.dart'; import 'package:tane/db/database.dart'; import '../support/test_support.dart'; const _seeds = [ SpeciesSeed( scientificName: 'Solanum lycopersicum', family: 'Solanaceae', commonNames: { 'en': ['Tomato'], 'es': ['Tomate'], }, ), SpeciesSeed( scientificName: 'Phaseolus vulgaris', family: 'Fabaceae', commonNames: { 'en': ['Common bean'], 'es': ['Judía', 'Alubia'], }, ), ]; void main() { late AppDatabase db; late SpeciesRepository repo; setUp(() { db = newTestDatabase(); repo = SpeciesRepository(db, idGen: IdGen()); }); tearDown(() => db.close()); test( 'parseSpeciesCatalog reads scientific name, family and common names', () { const json = ''' {"version":3,"species":[ {"scientific_name":"Zea mays","family":"Poaceae","wikidata_qid":"Q11575","gbif_key":2703077,"common":{"en":["Maize"],"es":["Maíz"]}} ]}'''; final seeds = parseSpeciesCatalog(json); expect(seeds.single.scientificName, 'Zea mays'); expect(seeds.single.family, 'Poaceae'); expect(seeds.single.wikidataQid, 'Q11575'); expect(seeds.single.gbifKey, 2703077); expect(seeds.single.commonNames['es'], ['Maíz']); }, ); test('parseSpeciesCatalog reads gbif and wikidata identifiers', () { const json = ''' {"version":3,"species":[ {"scientific_name":"Zea mays","gbif_key":5290052,"wikidata_qid":"Q11575"}, {"scientific_name":"Unknown sp."} ]}'''; final seeds = parseSpeciesCatalog(json); expect(seeds.first.gbifKey, 5290052); expect(seeds.first.wikidataQid, 'Q11575'); // Missing identifiers stay null, they are optional. expect(seeds.last.gbifKey, isNull); expect(seeds.last.wikidataQid, isNull); }); test('seedBundled persists gbif and wikidata identifiers', () async { await repo.seedBundled(const [ SpeciesSeed( scientificName: 'Zea mays', gbifKey: 5290052, wikidataQid: 'Q11575', ), ]); final row = await db.select(db.species).getSingle(); expect(row.gbifKey, 5290052); expect(row.wikidataQid, 'Q11575'); }); test( 'seedBundled backfills identifiers onto rows seeded before them', () async { // A pre-existing bundled row that predates the identifier fields. await db .into(db.species) .insert( SpeciesCompanion.insert( id: 'legacy', createdAt: 0, updatedAt: '0', lastAuthor: 'bundle', scientificName: 'Zea mays', isBundled: const Value(true), ), ); await repo.seedBundled(const [ SpeciesSeed( scientificName: 'Zea mays', gbifKey: 5290052, wikidataQid: 'Q11575', ), ]); final rows = await db.select(db.species).get(); expect(rows.single.id, 'legacy'); // same row, not a duplicate expect(rows.single.gbifKey, 5290052); expect(rows.single.wikidataQid, 'Q11575'); }, ); test('seedBundled inserts species and is idempotent', () async { await repo.seedBundled(_seeds); await repo.seedBundled(_seeds); // second run must not duplicate final species = await db.select(db.species).get(); expect(species.length, 2); expect(species.every((s) => s.isBundled), isTrue); }); test('search matches by scientific name', () async { await repo.seedBundled(_seeds); final results = await repo.search('phaseolus'); expect(results.single.scientificName, 'Phaseolus vulgaris'); }); test('search matches by common name and returns a localized label', () async { await repo.seedBundled(_seeds); final es = await repo.search('alubia', languageCode: 'es'); expect(es.single.scientificName, 'Phaseolus vulgaris'); expect(es.single.commonName, anyOf('Judía', 'Alubia')); final en = await repo.search('tom', languageCode: 'en'); expect(en.single.commonName, 'Tomato'); expect(en.single.displayLabel, 'Tomato (Solanum lycopersicum)'); }); test('empty query returns nothing', () async { await repo.seedBundled(_seeds); expect(await repo.search(' '), isEmpty); }); test('seedBundled stores wikidata id and gbif key', () async { await repo.seedBundled(const [ SpeciesSeed( scientificName: 'Solanum lycopersicum', family: 'Solanaceae', wikidataQid: 'Q13223', gbifKey: 2930137, commonNames: { 'en': ['Tomato'], }, ), ]); final row = await db.select(db.species).getSingle(); expect(row.wikidataQid, 'Q13223'); expect(row.gbifKey, 2930137); }); test('seedBundled backfills reference fields added later', () async { // First seed lacks the enrichment fields (an older catalog). await repo.seedBundled(const [ SpeciesSeed( scientificName: 'Allium cepa', commonNames: { 'en': ['Onion'], }, ), ]); // A later catalog carries them; identity is preserved, fields are filled. await repo.seedBundled(const [ SpeciesSeed( scientificName: 'Allium cepa', wikidataQid: 'Q23485', gbifKey: 2857697, viabilityYears: 1, commonNames: { 'en': ['Onion'], }, ), ]); final rows = await db.select(db.species).get(); expect(rows.length, 1); expect(rows.single.wikidataQid, 'Q23485'); expect(rows.single.gbifKey, 2857697); expect(rows.single.viabilityYears, 1); }); test('search matches a non-Latin common name', () async { await repo.seedBundled(const [ SpeciesSeed( scientificName: 'Solanum lycopersicum', commonNames: { 'ar': ['طماطم'], 'zh': ['番茄'], }, ), ]); final ar = await repo.search('طماطم', languageCode: 'ar'); expect(ar.single.scientificName, 'Solanum lycopersicum'); expect(ar.single.commonName, 'طماطم'); final zh = await repo.search('番茄', languageCode: 'zh'); expect(zh.single.commonName, '番茄'); }); test('search respects the limit', () async { final many = [ for (var i = 0; i < 20; i++) SpeciesSeed( scientificName: 'Genus species$i', commonNames: const { 'en': ['Plant'], }, ), ]; await repo.seedBundled(many); final results = await repo.search('Genus', limit: 5); expect(results.length, 5); }); test('classifyLabel infers the species named in a free-text label', () async { await repo.seedBundled(_seeds); final match = await repo.classifyLabel('Tomate cherry', languageCode: 'es'); expect(match, isNotNull); expect(match!.scientificName, 'Solanum lycopersicum'); expect(match.family, 'Solanaceae'); expect(match.commonName, 'Tomate'); // best name for the locale // No species named → no suggestion. expect(await repo.classifyLabel('Girasol gigante'), isNull); }); group('seedBundledIfNeeded', () { test('seeds on first run and skips the reload once recorded', () async { String? stored; var loads = 0; Future> load() async { loads++; return _seeds; } await repo.seedBundledIfNeeded( version: 3, readVersion: () async => stored, writeVersion: (v) async => stored = v, loadSeeds: load, ); expect(loads, 1); expect(stored, '3'); expect(await repo.search('Tomato'), isNotEmpty); // Same version already recorded → the ~1.5 MB parse is never loaded again. await repo.seedBundledIfNeeded( version: 3, readVersion: () async => stored, writeVersion: (v) async => stored = v, loadSeeds: load, ); expect(loads, 1); }); test('reseeds when the catalog version changes', () async { var stored = '3'; var loads = 0; await repo.seedBundledIfNeeded( version: 4, readVersion: () async => stored, writeVersion: (v) async => stored = v, loadSeeds: () async { loads++; return _seeds; }, ); expect(loads, 1); expect(stored, '4'); }); }); }