/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gbif.checklistbank.nub;

import org.gbif.api.exception.UnparsableException;
import org.gbif.api.model.Constants;
import org.gbif.api.model.checklistbank.ParsedName;
import org.gbif.api.service.checklistbank.NameParser;
import org.gbif.api.vocabulary.Kingdom;
import org.gbif.api.vocabulary.Origin;
import org.gbif.api.vocabulary.Rank;
import org.gbif.api.vocabulary.TaxonomicStatus;
import org.gbif.checklistbank.authorship.AuthorComparator;
import org.gbif.checklistbank.neo.UsageDao;
import org.gbif.checklistbank.nub.model.NubUsage;
import org.gbif.checklistbank.nub.model.NubUsageMatch;
import org.gbif.checklistbank.nub.model.SrcUsage;
import org.gbif.checklistbank.utils.RankUtils;
import org.gbif.nameparser.NameParserGbifV1;

import java.util.UUID;

import com.google.common.base.Throwables;
import org.junit.jupiter.api.Test;
import org.neo4j.graphdb.Transaction;

import static org.junit.Assert.*;

public class NubDbTest {
  final NameParser parser = new NameParserGbifV1();
  private long counter = 1;

  @Test
  public void testCountTaxa() throws Exception {
    UsageDao dao = UsageDao.temporaryDao(10);
    NubDb nub = NubDb.create(dao, AuthorComparator.createWithoutAuthormap());
    try (Transaction tx = dao.beginTx()) {

      assertEquals(0l, nub.countTaxa());

      NubUsage u = new NubUsage();
      u.parsedName = new ParsedName();
      u.origin = Origin.SOURCE;
      u.rank = Rank.SPECIES;
      nub.addRoot(u);
      assertEquals(1l, nub.countTaxa());

      // we add the same nub usage which already has a neo node, nothing changes
      nub.addRoot(u);
      assertEquals(1l, nub.countTaxa());

      u.node = null;
      nub.addRoot(u);
      assertEquals(2l, nub.countTaxa());
      tx.success();
    }

  }

  @Test
  public void testFindAcceptedCanonical() throws Exception {
    UsageDao dao = UsageDao.temporaryDao(10);
    NubDb db = NubDb.create(dao, AuthorComparator.createWithoutAuthormap());
    try (Transaction tx = dao.beginTx()) {

      final NubUsage plantae = db.addRoot(buildNub(Kingdom.PLANTAE, "Plantae", Rank.KINGDOM, TaxonomicStatus.ACCEPTED));
      final NubUsage oenanteP = db.addUsage(plantae, buildNub("Oenanthe Vieillot, 1816", Rank.GENUS, TaxonomicStatus.ACCEPTED));
      db.addUsage(oenanteP, buildNub("Oenanthe aquatica Poir.", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      NubUsage acc = db.addUsage(plantae, buildNub("Palma aquatica (Senser.)", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      db.addUsage(acc, buildNub("Oenanthe aquatica Senser.", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      db.addUsage(oenanteP, buildNub("Oenanthe carolina (Mill.)", Rank.SPECIES, TaxonomicStatus.DOUBTFUL));
      acc = db.addUsage(oenanteP, buildNub("Oenanthe arida", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      db.addUsage(acc, buildNub("Onatus horrida", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      db.addUsage(acc, buildNub("Onatus horrida alpina", Rank.SUBSPECIES, TaxonomicStatus.SYNONYM));

      final NubUsage animalia = db.addRoot(buildNub(Kingdom.ANIMALIA, "Animalia", Rank.KINGDOM, TaxonomicStatus.ACCEPTED));
      final NubUsage oenanteA = db.addUsage(animalia, buildNub("Oenanthe Linnaeus, 1753", Rank.GENUS, TaxonomicStatus.ACCEPTED));
      db.addUsage(animalia, buildNub("Geotrupes stercorarius (Linnaeus, 1758)", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      acc = db.addUsage(animalia, buildNub("Geotrupes spiniger (Marsham, 1802)", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      db.addUsage(acc, buildNub("Geotrupes stercorarius Erichson, 1847", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      db.addUsage(oenanteA, buildNub("Oenanthe aquatica", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      tx.success();

      assertTrue(db.findAcceptedNubUsage(Kingdom.ANIMALIA, "Oenanthe", Rank.GENUS).isMatch());
      assertTrue(db.findAcceptedNubUsage(Kingdom.ANIMALIA, "Oenanthe aquatica", Rank.SPECIES).isMatch());
      assertFalse(db.findAcceptedNubUsage(Kingdom.ANIMALIA, "Oenanthe aquatica", Rank.SUBSPECIES).isMatch());

      assertFalse(db.findAcceptedNubUsage(Kingdom.PLANTAE, "Onatus horrida alpina", Rank.SUBSPECIES).isMatch());
      assertFalse(db.findAcceptedNubUsage(Kingdom.PLANTAE, "Onatus horrida", Rank.SPECIES).isMatch());
      assertTrue(db.findAcceptedNubUsage(Kingdom.PLANTAE, "Oenanthe", Rank.GENUS).isMatch());
      assertTrue(db.findAcceptedNubUsage(Kingdom.PLANTAE, "Oenanthe aquatica", Rank.SPECIES).isMatch());
      assertFalse(db.findAcceptedNubUsage(Kingdom.PLANTAE, "Oenanthe aquatica", Rank.SUBSPECIES).isMatch());
    }
  }

  @Test
  public void testGetParent() throws Exception {
    UsageDao dao = UsageDao.temporaryDao(10);
    NubDb db = NubDb.create(dao, AuthorComparator.createWithoutAuthormap());
    try (Transaction tx = dao.beginTx()) {
      final NubUsage plantae = db.addRoot(buildNub(Kingdom.PLANTAE, "Plantae", Rank.KINGDOM, TaxonomicStatus.ACCEPTED));
      final NubUsage oenanteP = db.addUsage(plantae, buildNub("Oenanthe Vieillot, 1816", Rank.GENUS, TaxonomicStatus.ACCEPTED));
      db.addUsage(oenanteP, buildNub("Oenanthe aquatica Poir.", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      NubUsage acc = db.addUsage(plantae, buildNub("Palma aquatica (Senser.)", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      db.addUsage(acc, buildNub("Oenanthe aquatica Senser.", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      db.addUsage(oenanteP, buildNub("Oenanthe carolina (Mill.)", Rank.SPECIES, TaxonomicStatus.DOUBTFUL));

      assertNull(db.parent((NubUsage) null));
      assertNull(db.parent(new NubUsage()));
      assertNull(db.parent(plantae));
      assertNotNull(db.parent(oenanteP));
    }
  }

  @Test
  public void testFindTaxa() throws Exception {
    UsageDao dao = UsageDao.temporaryDao(25);
    NubDb db = NubDb.create(dao, AuthorComparator.createWithoutAuthormap());
    try (Transaction tx = dao.beginTx()) {

      final NubUsage plantae = db.addRoot(buildNub(Kingdom.PLANTAE, "Plantae", Rank.KINGDOM, TaxonomicStatus.ACCEPTED));
      final NubUsage animalia = db.addRoot(buildNub(Kingdom.ANIMALIA, "Animalia", Rank.KINGDOM, TaxonomicStatus.ACCEPTED));
      final NubUsage fungi = db.addRoot(buildNub(Kingdom.FUNGI, "Fungi", Rank.KINGDOM, TaxonomicStatus.ACCEPTED));

      final NubUsage sagartiidae = addHierarchy(db, animalia, Rank.PHYLUM, "Cnidaria", "Anthozoa", "Actiniaria", "Sagartiidae");
      final NubUsage verrillactis = db.addUsage(sagartiidae, buildNub("Verrillactis", Rank.GENUS, TaxonomicStatus.ACCEPTED));
      final NubUsage aarum = db.addUsage(sagartiidae, buildNub("Aarum", Rank.GENUS, TaxonomicStatus.ACCEPTED));
      final NubUsage barum = db.addUsage(sagartiidae, buildNub("Barum", Rank.GENUS, TaxonomicStatus.ACCEPTED));
      final NubUsage carum = db.addUsage(sagartiidae, buildNub("Carum", Rank.GENUS, TaxonomicStatus.ACCEPTED));
      final NubUsage darum = db.addUsage(sagartiidae, buildNub("Darum", Rank.GENUS, TaxonomicStatus.ACCEPTED));

      final NubUsage vp1870 = db.addUsage(verrillactis, buildNub("Verrillactis paguri (Verrill, 1870)", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      final NubUsage vp = db.addUsage(vp1870, buildNub("Verrillactis paguri (Verrill)", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      final NubUsage vp1869 = db.addUsage(vp1870, buildNub("Verrillactis paguri (Verrill, 1869)", Rank.SPECIES, TaxonomicStatus.HETEROTYPIC_SYNONYM));
      final NubUsage vp1896 = db.addUsage(vp1870, buildNub("Verrillactis paguri (Verrill, 1896a)", Rank.SPECIES, TaxonomicStatus.SYNONYM));

      final NubUsage vacc = db.addUsage(verrillactis, buildNub("Verrillactis accepturi (Verrill, 1870)", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      final NubUsage vv = db.addUsage(vacc, buildNub("Verrillactis vagguri (Verrill)", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      db.addUsage(vacc, buildNub("Verrillactis vagguri (Verrill, 1869)", Rank.SPECIES, TaxonomicStatus.HETEROTYPIC_SYNONYM));
      db.addUsage(vacc, buildNub("Verrillactis vagguri (Verrill, 1896)", Rank.SPECIES, TaxonomicStatus.SYNONYM));

      final NubUsage vacc2 = db.addUsage(verrillactis, buildNub("Verrillactis accepturissimo (Verrill, 1870)", Rank.SPECIES, TaxonomicStatus.ACCEPTED));
      db.addUsage(vacc, buildNub("Verrillactis laguri (Verrill)", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      final NubUsage vl = db.addUsage(vacc2, buildNub("Verrillactis laguri (Verrill, 1869)", Rank.SPECIES, TaxonomicStatus.HETEROTYPIC_SYNONYM));
      db.addUsage(vacc2, buildNub("Verrillactis laguri (Verrill, 1896a)", Rank.SPECIES, TaxonomicStatus.SYNONYM));
      tx.success();


      // full name matches
      assertMatch(vp1869,
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis paguri (Verrill, 1869)", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.ANIMALIA, animalia)
      );
      assertMatch(vv,
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis vagguri (Verrill)", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.ANIMALIA, animalia)
      );
      assertMatch(vl,
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis laguri (Verrill, 1869)", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.ANIMALIA, animalia)
      );

      // wrong kingdom, but matching authorship
      assertMatch(vp1869,
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis paguri (Verrill, 1869)", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.VIRUSES, animalia)
      );
      assertMatch(vl,
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis laguri (Verrill, 1869)", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.VIRUSES, animalia)
      );

      // wrong rank
      assertNoMatch(
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis paguri (Verrill, 1869)", Rank.GENUS, TaxonomicStatus.ACCEPTED), Kingdom.ANIMALIA, animalia)
      );

      // single accepted
      assertSnap(vp1870,
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis paguri", Rank.SPECIES, TaxonomicStatus.SYNONYM), Kingdom.ANIMALIA, animalia)
      );

      // single accepted, but wrong kingdom
      assertNoMatch(
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis paguri", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.PLANTAE, animalia)
      );

      // 3 synonyms, all pointing to the same accepted. Pick first
      assertSnap(vv,
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis vagguri", Rank.SPECIES, TaxonomicStatus.SYNONYM), Kingdom.ANIMALIA, animalia)
      );

      // same usage source as the synonyms and authorless accepted
      assertNoMatch(
          db.findNubUsage(Constants.COL_DATASET_KEY, buildSrc("Verrillactis vagguri", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.ANIMALIA, animalia)
      );

      // different usage source than the synonyms, pick first
      assertSnap(vv,
          db.findNubUsage(Constants.NUB_NETWORK_KEY, buildSrc("Verrillactis vagguri", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.ANIMALIA, animalia)
      );

      // 2 different accepted parents for these synonyms, ignore
      assertIgnoreSource(db, Constants.NUB_NETWORK_KEY, buildSrc("Verrillactis laguri", Rank.SPECIES, TaxonomicStatus.ACCEPTED), Kingdom.ANIMALIA, animalia);
    }
  }

  private void assertMatch(NubUsage expected, NubUsageMatch m) {
    assertEquals(expected.node, m.usage.node);
    assertFalse(m.ignore);
  }

  private void assertSnap(NubUsage expected, NubUsageMatch m) {
    assertEquals(expected.node, m.usage.node);
    assertTrue(m.ignore);
  }

  private void assertNoMatch(NubUsageMatch m) {
    assertNull(m.usage);
  }

  private void assertIgnoreSource(NubDb db, UUID currSource, SrcUsage u, Kingdom uKingdom, NubUsage currNubParent) {
    try {
      db.findNubUsage(currSource, u, uKingdom, currNubParent);
      fail("IgnoreSourceUsageException  expected");
    } catch (IgnoreSourceUsageException e) {
      return;
    }
  }

  private NubUsage addHierarchy(NubDb db, NubUsage parent, Rank startRank, String... taxa) throws IgnoreSourceUsageException {
    Rank rank = startRank;
    for (String name : taxa) {
      parent = db.addUsage(parent, buildNub(name, rank, TaxonomicStatus.ACCEPTED));
      rank = RankUtils.nextLowerLinneanRank(rank);
    }
    return parent;
  }

  private NubUsage buildNub(String sciname, Rank rank, TaxonomicStatus status) {
    return buildNub(null, sciname, rank, status);
  }

  private NubUsage buildNub(Kingdom k, String sciname, Rank rank, TaxonomicStatus status) {
    NubUsage nu = new NubUsage();
    nu.datasetKey = Constants.COL_DATASET_KEY;
    try {
      nu.parsedName = parser.parse(sciname, rank);
      nu.rank = rank;
      nu.status = status;
      nu.kingdom = k;
    } catch (UnparsableException e) {
      Throwables.propagate(e);
    }
    return nu;
  }

  private SrcUsage buildSrc(String sciname, Rank rank, TaxonomicStatus status) {
    SrcUsage u = new SrcUsage();
    try {
      u.scientificName = sciname;
      u.parsedName = parser.parse(sciname, rank);
      u.rank = rank;
      u.status = status;
    } catch (UnparsableException e) {
      Throwables.propagate(e);
    }
    return u;
  }
}