/*
 * 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.validation;

import org.gbif.api.model.Constants;
import org.gbif.api.model.checklistbank.NameUsage;
import org.gbif.api.model.common.LinneanClassification;
import org.gbif.api.model.common.paging.PagingRequest;
import org.gbif.api.service.checklistbank.NameUsageService;
import org.gbif.api.vocabulary.Kingdom;
import org.gbif.api.vocabulary.Rank;

import java.util.Iterator;
import java.util.List;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;

public class PgAssertionEngine implements AssertionEngine {
  private static final Logger LOG = LoggerFactory.getLogger(PgAssertionEngine.class);

  private final NameUsageService usageService;

  private boolean valid = true;

  public PgAssertionEngine(NameUsageService usageService) {
    this.usageService = usageService;
  }

  @Override
  public boolean isValid() {
    return valid;
  }

  @Override
  public void assertParentsContain(String searchName, Rank searchRank, String parent) {
    try {
      NameUsage start = findUsageByCanonical(searchName, searchRank);
      assertParentsContain(start.getKey(), null, parent);

    } catch (AssertionError e) {
      valid = false;
      LOG.error("Classification for {} {} lacks parent {}", searchRank, searchName, parent, e);
    }
  }

  @Override
  public void assertParentsContain(int usageKey, @Nullable Rank parentRank, String parent) {
    try {
      boolean found = false;
      for (NameUsage p : usageService.listParents(usageKey, null)) {
        if (parent.equalsIgnoreCase(p.getCanonicalOrScientificName()) && (parentRank == null || parentRank.equals(p.getRank()))) {
          found = true;
          break;
        }
      }
      Assert.assertTrue(found);

    } catch (AssertionError e) {
      valid = false;
      LOG.error("Classification for usage {} missing {}", usageKey, parent, e);
    }
  }

  @Override
  public void assertClassification(int usageKey, LinneanClassification classification) {
    try {
      NameUsage u = usageService.get(usageKey, null);
      for (Rank r : Rank.DWC_RANKS) {
        if (!StringUtils.isBlank(classification.getHigherRank(r))) {
          Assert.assertEquals(classification.getHigherRank(r), u.getHigherRank(r));
        }
      }

    } catch (AssertionError e) {
      valid = false;
      LOG.error("Classification assertion failed for {}", usageKey, e);
    }
  }

  @Override
  public void assertClassification(int usageKey, String... classification) {
    Iterator<String> expected = Lists.newArrayList(classification).iterator();
    Iterator<NameUsage> parents = Lists.reverse(usageService.listParents(usageKey, null)).iterator();

    try {
      while (expected.hasNext()) {
        Assert.assertTrue(parents.hasNext());
        Assert.assertEquals(expected.next(), parents.next().getCanonicalName());
      }
      Assert.assertFalse(parents.hasNext());

    } catch (AssertionError e) {
      valid = false;
      LOG.error("Classification for usage {} wrong", usageKey, e);
    }
  }

  @Override
  public void assertSearchMatch(int expectedSearchMatches, String name) {
    assertSearchMatch(expectedSearchMatches, name, null);
  }

  @Override
  public void assertSearchMatch(int expectedSearchMatches, String name, Rank rank) {
    List<NameUsage> matches = Lists.newArrayList();
    try {
      matches = findUsagesByCanonical(name, rank);
      Assert.assertEquals(expectedSearchMatches, matches.size());
    } catch (AssertionError e) {
      valid = false;
      LOG.error("Expected {} matches, but found {} for name {} with rank {}", expectedSearchMatches, matches.size(), name, rank);
    }
  }

  @Override
  public void assertNotExisting(String name, Rank rank) {
    List<NameUsage> matches = Lists.newArrayList();
    try {
      matches = findUsagesByCanonical(name, rank);
      Assert.assertTrue(matches.isEmpty());
    } catch (AssertionError e) {
      valid = false;
      LOG.error("Found {} {} expected to be missing: {}", rank, name, matches.get(0));
    }
  }

  @Override
  public void assertUsage(int usageKey, Rank rank, String name, String accepted, Kingdom kingdom) {
    NameUsage u = null;
    try {
      u = usageService.get(usageKey, null);
      Assert.assertNotNull("Usage "+usageKey+" not existing", u);
      Assert.assertEquals("Wrong rank", rank, u.getRank());
      Assert.assertTrue("Wrong scientific name", u.getScientificName().startsWith(name));

      if (StringUtils.isBlank(accepted)) {
        Assert.assertFalse("Not accepted", u.isSynonym());
      } else {
        Assert.assertTrue("Not a synonym", u.isSynonym());
        Assert.assertTrue("Wrong accepted name", u.getAccepted().startsWith(accepted));
      }

      if (kingdom != null) {
        Assert.assertEquals("Wrong kingdom", kingdom.nubUsageID(), u.getKingdomKey());
        Assert.assertEquals("Wrong kingdom", kingdom.scientificName(), u.getKingdom());
      }

    } catch (AssertionError e) {
      LOG.error("Usage {} {} wrong: {}\n{}", usageKey, name, e.getMessage(), u);
      valid = false;
    }
  }

  private NameUsage findUsageByCanonical(String name, Rank rank) {
    List<NameUsage> matches = findUsagesByCanonical(name, rank);
    if (matches.size() > 1 || matches.isEmpty()) {
      valid = false;
      LOG.error("{} matches when expecting single match for {} {}", matches.size(), rank, name);
      throw new AssertionError("No single match for " + name);
    }
    return matches.get(0);
  }

  private List<NameUsage> findUsagesByCanonical(String name, @Nullable Rank rank) {
    // avoid paging and do a large, single page
    PagingRequest req = new PagingRequest(0, 1000);
    List<NameUsage> matches = Lists.newArrayList();

    for (NameUsage u : usageService.listByCanonicalName(null, name, req, Constants.NUB_DATASET_KEY).getResults()) {
      if (rank == null || rank.equals(u.getRank())) {
        matches.add(u);
      }
    }
    return matches;
  }
}