/*
 * Jalview - A Sequence Alignment Editor and Viewer (2.11.2.6)
 * Copyright (C) 2023 The Jalview Authors
 * 
 * This file is part of Jalview.
 * 
 * Jalview is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *  
 * Jalview is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty 
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
 * PURPOSE.  See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 * The Jalview Authors are detailed in the 'AUTHORS' file.
 */
package jalview.datamodel.features;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.testng.annotations.Test;

import jalview.datamodel.SequenceFeature;
import junit.extensions.PA;

public class SequenceFeaturesTest
{
  @Test(groups = "Functional")
  public void testConstructor()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    assertFalse(store.hasFeatures());

    store = new SequenceFeatures((List<SequenceFeature>) null);
    assertFalse(store.hasFeatures());

    List<SequenceFeature> features = new ArrayList<>();
    store = new SequenceFeatures(features);
    assertFalse(store.hasFeatures());

    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    features.add(sf1);
    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 15, 18,
            Float.NaN, null);
    features.add(sf2); // nested
    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc2", 0, 0,
            Float.NaN, null); // non-positional
    features.add(sf3);
    store = new SequenceFeatures(features);
    assertTrue(store.hasFeatures());
    assertEquals(2, store.getFeatureCount(true)); // positional
    assertEquals(1, store.getFeatureCount(false)); // non-positional
    assertFalse(store.add(sf1)); // already contained
    assertFalse(store.add(sf2)); // already contained
    assertFalse(store.add(sf3)); // already contained
  }

  @Test(groups = "Functional")
  public void testGetPositionalFeatures()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf1);
    // same range, different description
    SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20,
            Float.NaN, null);
    store.add(sf2);
    // discontiguous range
    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 30, 40,
            Float.NaN, null);
    store.add(sf3);
    // overlapping range
    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 15, 35,
            Float.NaN, null);
    store.add(sf4);
    // enclosing range
    SequenceFeature sf5 = new SequenceFeature("Metal", "desc", 5, 50,
            Float.NaN, null);
    store.add(sf5);
    // non-positional feature
    SequenceFeature sf6 = new SequenceFeature("Metal", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf6);
    // contact feature
    SequenceFeature sf7 = new SequenceFeature("Disulphide bond", "desc", 18,
            45, Float.NaN, null);
    store.add(sf7);
    // different feature type
    SequenceFeature sf8 = new SequenceFeature("Pfam", "desc", 30, 40,
            Float.NaN, null);
    store.add(sf8);
    SequenceFeature sf9 = new SequenceFeature("Pfam", "desc", 15, 35,
            Float.NaN, null);
    store.add(sf9);

    /*
     * get all positional features
     */
    List<SequenceFeature> features = store.getPositionalFeatures();
    assertEquals(features.size(), 8);
    assertTrue(features.contains(sf1));
    assertTrue(features.contains(sf2));
    assertTrue(features.contains(sf3));
    assertTrue(features.contains(sf4));
    assertTrue(features.contains(sf5));
    assertFalse(features.contains(sf6)); // non-positional
    assertTrue(features.contains(sf7));
    assertTrue(features.contains(sf8));
    assertTrue(features.contains(sf9));

    /*
     * get features by type
     */
    assertTrue(store.getPositionalFeatures((String) null).isEmpty());
    assertTrue(store.getPositionalFeatures("Cath").isEmpty());
    assertTrue(store.getPositionalFeatures("METAL").isEmpty());

    features = store.getPositionalFeatures("Metal");
    assertEquals(features.size(), 5);
    assertTrue(features.contains(sf1));
    assertTrue(features.contains(sf2));
    assertTrue(features.contains(sf3));
    assertTrue(features.contains(sf4));
    assertTrue(features.contains(sf5));
    assertFalse(features.contains(sf6));

    features = store.getPositionalFeatures("Disulphide bond");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf7));

    features = store.getPositionalFeatures("Pfam");
    assertEquals(features.size(), 2);
    assertTrue(features.contains(sf8));
    assertTrue(features.contains(sf9));
  }

  @Test(groups = "Functional")
  public void testGetContactFeatures()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    // non-contact
    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf1);
    // non-positional
    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf2);
    // contact feature
    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc", 18,
            45, Float.NaN, null);
    store.add(sf3);
    // repeat for different feature type
    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf4);
    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf5);
    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18,
            45, Float.NaN, null);
    store.add(sf6);

    /*
     * get all contact features
     */
    List<SequenceFeature> features = store.getContactFeatures();
    assertEquals(features.size(), 2);
    assertTrue(features.contains(sf3));
    assertTrue(features.contains(sf6));

    /*
     * get contact features by type
     */
    assertTrue(store.getContactFeatures((String) null).isEmpty());
    assertTrue(store.getContactFeatures("Cath").isEmpty());
    assertTrue(store.getContactFeatures("Pfam").isEmpty());
    assertTrue(store.getContactFeatures("DISULPHIDE BOND").isEmpty());

    features = store.getContactFeatures("Disulphide bond");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf3));

    features = store.getContactFeatures("Disulfide bond");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf6));
  }

  @Test(groups = "Functional")
  public void testGetNonPositionalFeatures()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    // positional
    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf1);
    // non-positional
    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf2);
    // contact feature
    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc", 18,
            45, Float.NaN, null);
    store.add(sf3);
    // repeat for different feature type
    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf4);
    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf5);
    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18,
            45, Float.NaN, null);
    store.add(sf6);
    // one more non-positional, different description
    SequenceFeature sf7 = new SequenceFeature("Pfam", "desc2", 0, 0,
            Float.NaN, null);
    store.add(sf7);

    /*
     * get all non-positional features
     */
    List<SequenceFeature> features = store.getNonPositionalFeatures();
    assertEquals(features.size(), 3);
    assertTrue(features.contains(sf2));
    assertTrue(features.contains(sf5));
    assertTrue(features.contains(sf7));

    /*
     * get non-positional features by type
     */
    assertTrue(store.getNonPositionalFeatures((String) null).isEmpty());
    assertTrue(store.getNonPositionalFeatures("Cath").isEmpty());
    assertTrue(store.getNonPositionalFeatures("PFAM").isEmpty());

    features = store.getNonPositionalFeatures("Metal");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf2));

    features = store.getNonPositionalFeatures("Pfam");
    assertEquals(features.size(), 2);
    assertTrue(features.contains(sf5));
    assertTrue(features.contains(sf7));
  }

  /**
   * Helper method to add a feature of no particular type
   * 
   * @param sf
   * @param type
   * @param from
   * @param to
   * @return
   */
  SequenceFeature addFeature(SequenceFeaturesI sf, String type, int from,
          int to)
  {
    SequenceFeature sf1 = new SequenceFeature(type, "", from, to, Float.NaN,
            null);
    sf.add(sf1);
    return sf1;
  }

  @Test(groups = "Functional")
  public void testFindFeatures()
  {
    SequenceFeaturesI sf = new SequenceFeatures();
    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
    SequenceFeature sf2 = addFeature(sf, "Pfam", 1, 15);
    SequenceFeature sf3 = addFeature(sf, "Pfam", 20, 30);
    SequenceFeature sf4 = addFeature(sf, "Pfam", 40, 100);
    SequenceFeature sf5 = addFeature(sf, "Pfam", 60, 100);
    SequenceFeature sf6 = addFeature(sf, "Pfam", 70, 70);
    SequenceFeature sf7 = addFeature(sf, "Cath", 10, 50);
    SequenceFeature sf8 = addFeature(sf, "Cath", 1, 15);
    SequenceFeature sf9 = addFeature(sf, "Cath", 20, 30);
    SequenceFeature sf10 = addFeature(sf, "Cath", 40, 100);
    SequenceFeature sf11 = addFeature(sf, "Cath", 60, 100);
    SequenceFeature sf12 = addFeature(sf, "Cath", 70, 70);

    List<SequenceFeature> overlaps = sf.findFeatures(200, 200, "Pfam");
    assertTrue(overlaps.isEmpty());

    overlaps = sf.findFeatures(1, 9, "Pfam");
    assertEquals(overlaps.size(), 1);
    assertTrue(overlaps.contains(sf2));

    overlaps = sf.findFeatures(5, 18, "Pfam");
    assertEquals(overlaps.size(), 2);
    assertTrue(overlaps.contains(sf1));
    assertTrue(overlaps.contains(sf2));

    overlaps = sf.findFeatures(30, 40, "Pfam");
    assertEquals(overlaps.size(), 3);
    assertTrue(overlaps.contains(sf1));
    assertTrue(overlaps.contains(sf3));
    assertTrue(overlaps.contains(sf4));

    overlaps = sf.findFeatures(80, 90, "Pfam");
    assertEquals(overlaps.size(), 2);
    assertTrue(overlaps.contains(sf4));
    assertTrue(overlaps.contains(sf5));

    overlaps = sf.findFeatures(68, 70, "Pfam");
    assertEquals(overlaps.size(), 3);
    assertTrue(overlaps.contains(sf4));
    assertTrue(overlaps.contains(sf5));
    assertTrue(overlaps.contains(sf6));

    overlaps = sf.findFeatures(16, 69, "Cath");
    assertEquals(overlaps.size(), 4);
    assertTrue(overlaps.contains(sf7));
    assertFalse(overlaps.contains(sf8));
    assertTrue(overlaps.contains(sf9));
    assertTrue(overlaps.contains(sf10));
    assertTrue(overlaps.contains(sf11));
    assertFalse(overlaps.contains(sf12));

    assertTrue(sf.findFeatures(0, 1000, "Metal").isEmpty());

    overlaps = sf.findFeatures(7, 7, (String) null);
    assertTrue(overlaps.isEmpty());
  }

  @Test(groups = "Functional")
  public void testDelete()
  {
    SequenceFeaturesI sf = new SequenceFeatures();
    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
    assertTrue(sf.getPositionalFeatures().contains(sf1));

    assertFalse(sf.delete(null));
    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 15, 0f, null);
    assertFalse(sf.delete(sf2)); // not added, can't delete it
    assertTrue(sf.delete(sf1));
    assertTrue(sf.getPositionalFeatures().isEmpty());
  }

  @Test(groups = "Functional")
  public void testHasFeatures()
  {
    SequenceFeaturesI sf = new SequenceFeatures();
    assertFalse(sf.hasFeatures());

    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
    assertTrue(sf.hasFeatures());

    sf.delete(sf1);
    assertFalse(sf.hasFeatures());
  }

  /**
   * Tests for the method that gets feature groups for positional or
   * non-positional features
   */
  @Test(groups = "Functional")
  public void testGetFeatureGroups()
  {
    SequenceFeaturesI sf = new SequenceFeatures();
    assertTrue(sf.getFeatureGroups(true).isEmpty());
    assertTrue(sf.getFeatureGroups(false).isEmpty());

    /*
     * add a non-positional feature (begin/end = 0/0)
     */
    SequenceFeature sfx = new SequenceFeature("AType", "Desc", 0, 0, 0f,
            "AGroup");
    sf.add(sfx);
    Set<String> groups = sf.getFeatureGroups(true); // for positional
    assertTrue(groups.isEmpty());
    groups = sf.getFeatureGroups(false); // for non-positional
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("AGroup"));
    groups = sf.getFeatureGroups(false, "AType");
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("AGroup"));
    groups = sf.getFeatureGroups(true, "AnotherType");
    assertTrue(groups.isEmpty());

    /*
     * add, then delete, more non-positional features of different types
     */
    SequenceFeature sfy = new SequenceFeature("AnotherType", "Desc", 0, 0,
            0f, "AnotherGroup");
    sf.add(sfy);
    SequenceFeature sfz = new SequenceFeature("AThirdType", "Desc", 0, 0,
            0f, null);
    sf.add(sfz);
    groups = sf.getFeatureGroups(false);
    assertEquals(groups.size(), 3);
    assertTrue(groups.contains("AGroup"));
    assertTrue(groups.contains("AnotherGroup"));
    assertTrue(groups.contains(null)); // null is a possible group
    sf.delete(sfz);
    sf.delete(sfy);
    groups = sf.getFeatureGroups(false);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("AGroup"));

    /*
     * add positional features
     */
    SequenceFeature sf1 = new SequenceFeature("Pfam", "Desc", 10, 50, 0f,
            "PfamGroup");
    sf.add(sf1);
    groups = sf.getFeatureGroups(true);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("PfamGroup"));
    groups = sf.getFeatureGroups(false); // non-positional unchanged
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("AGroup"));

    SequenceFeature sf2 = new SequenceFeature("Cath", "Desc", 10, 50, 0f,
            null);
    sf.add(sf2);
    groups = sf.getFeatureGroups(true);
    assertEquals(groups.size(), 2);
    assertTrue(groups.contains("PfamGroup"));
    assertTrue(groups.contains(null));

    sf.delete(sf1);
    sf.delete(sf2);
    assertTrue(sf.getFeatureGroups(true).isEmpty());

    SequenceFeature sf3 = new SequenceFeature("CDS", "", 10, 50, 0f,
            "Ensembl");
    sf.add(sf3);
    SequenceFeature sf4 = new SequenceFeature("exon", "", 10, 50, 0f,
            "Ensembl");
    sf.add(sf4);
    groups = sf.getFeatureGroups(true);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("Ensembl"));

    /*
     * delete last Ensembl group feature from CDS features
     * but still have one in exon features
     */
    sf.delete(sf3);
    groups = sf.getFeatureGroups(true);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("Ensembl"));

    /*
     * delete the last non-positional feature
     */
    sf.delete(sfx);
    groups = sf.getFeatureGroups(false);
    assertTrue(groups.isEmpty());
  }

  @Test(groups = "Functional")
  public void testGetFeatureTypesForGroups()
  {
    SequenceFeaturesI sf = new SequenceFeatures();
    assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty());

    /*
     * add feature with group = "Uniprot", type = "helix"
     */
    String groupUniprot = "Uniprot";
    SequenceFeature sf1 = new SequenceFeature("helix", "Desc", 10, 50, 0f,
            groupUniprot);
    sf.add(sf1);
    Set<String> groups = sf.getFeatureTypesForGroups(true, groupUniprot);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("helix"));
    assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty());

    /*
     * add feature with group = "Uniprot", type = "strand"
     */
    SequenceFeature sf2 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
            groupUniprot);
    sf.add(sf2);
    groups = sf.getFeatureTypesForGroups(true, groupUniprot);
    assertEquals(groups.size(), 2);
    assertTrue(groups.contains("helix"));
    assertTrue(groups.contains("strand"));

    /*
     * delete the "strand" Uniprot feature - still have "helix"
     */
    sf.delete(sf2);
    groups = sf.getFeatureTypesForGroups(true, groupUniprot);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("helix"));

    /*
     * delete the "helix" Uniprot feature - none left
     */
    sf.delete(sf1);
    assertTrue(sf.getFeatureTypesForGroups(true, groupUniprot).isEmpty());

    /*
     * add some null group features
     */
    SequenceFeature sf3 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
            null);
    sf.add(sf3);
    SequenceFeature sf4 = new SequenceFeature("turn", "Desc", 10, 50, 0f,
            null);
    sf.add(sf4);
    groups = sf.getFeatureTypesForGroups(true, (String) null);
    assertEquals(groups.size(), 2);
    assertTrue(groups.contains("strand"));
    assertTrue(groups.contains("turn"));

    /*
     * add strand/Cath  and turn/Scop and query for one or both groups
     * (find feature types for groups selected in Feature Settings)
     */
    SequenceFeature sf5 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
            "Cath");
    sf.add(sf5);
    SequenceFeature sf6 = new SequenceFeature("turn", "Desc", 10, 50, 0f,
            "Scop");
    sf.add(sf6);
    groups = sf.getFeatureTypesForGroups(true, "Cath");
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("strand"));
    groups = sf.getFeatureTypesForGroups(true, "Scop");
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("turn"));
    groups = sf.getFeatureTypesForGroups(true, "Cath", "Scop");
    assertEquals(groups.size(), 2);
    assertTrue(groups.contains("turn"));
    assertTrue(groups.contains("strand"));
    // alternative vararg syntax
    groups = sf.getFeatureTypesForGroups(true,
            new String[]
            { "Cath", "Scop" });
    assertEquals(groups.size(), 2);
    assertTrue(groups.contains("turn"));
    assertTrue(groups.contains("strand"));
  }

  @Test(groups = "Functional")
  public void testGetFeatureTypes()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    Set<String> types = store.getFeatureTypes();
    assertTrue(types.isEmpty());

    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf1);
    types = store.getFeatureTypes();
    assertEquals(types.size(), 1);
    assertTrue(types.contains("Metal"));

    // null type is rejected...
    SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20,
            Float.NaN, null);
    assertFalse(store.add(sf2));
    types = store.getFeatureTypes();
    assertEquals(types.size(), 1);
    assertFalse(types.contains(null));
    assertTrue(types.contains("Metal"));

    /*
     * add non-positional feature
     */
    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf3);
    types = store.getFeatureTypes();
    assertEquals(types.size(), 2);
    assertTrue(types.contains("Pfam"));

    /*
     * add contact feature
     */
    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", 10,
            20, Float.NaN, null);
    store.add(sf4);
    types = store.getFeatureTypes();
    assertEquals(types.size(), 3);
    assertTrue(types.contains("Disulphide Bond"));

    /*
     * add another Pfam
     */
    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf5);
    types = store.getFeatureTypes();
    assertEquals(types.size(), 3); // unchanged

    /*
     * delete first Pfam - still have one
     */
    assertTrue(store.delete(sf3));
    types = store.getFeatureTypes();
    assertEquals(types.size(), 3);
    assertTrue(types.contains("Pfam"));

    /*
     * delete second Pfam - no longer have one
     */
    assertTrue(store.delete(sf5));
    types = store.getFeatureTypes();
    assertEquals(types.size(), 2);
    assertFalse(types.contains("Pfam"));
  }

  @Test(groups = "Functional")
  public void testGetFeatureCount()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    assertEquals(store.getFeatureCount(true), 0);
    assertEquals(store.getFeatureCount(false), 0);

    /*
     * add positional
     */
    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf1);
    assertEquals(store.getFeatureCount(true), 1);
    assertEquals(store.getFeatureCount(false), 0);

    /*
     * null feature type is rejected
     */
    SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20,
            Float.NaN, null);
    assertFalse(store.add(sf2));
    assertEquals(store.getFeatureCount(true), 1);
    assertEquals(store.getFeatureCount(false), 0);

    /*
     * add non-positional feature
     */
    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf3);
    assertEquals(store.getFeatureCount(true), 1);
    assertEquals(store.getFeatureCount(false), 1);

    /*
     * add contact feature (counts as 1)
     */
    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", 10,
            20, Float.NaN, null);
    store.add(sf4);
    assertEquals(store.getFeatureCount(true), 2);
    assertEquals(store.getFeatureCount(false), 1);

    /*
     * add another Pfam but this time as a positional feature
     */
    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf5);
    assertEquals(store.getFeatureCount(true), 3); // sf1, sf4, sf5
    assertEquals(store.getFeatureCount(false), 1); // sf3
    assertEquals(store.getFeatureCount(true, "Pfam"), 1); // positional
    assertEquals(store.getFeatureCount(false, "Pfam"), 1); // non-positional
    // search for type==null
    assertEquals(store.getFeatureCount(true, (String) null), 0);
    // search with no type specified
    assertEquals(store.getFeatureCount(true, (String[]) null), 3);
    assertEquals(store.getFeatureCount(true, "Metal", "Cath"), 1);
    assertEquals(store.getFeatureCount(true, "Disulphide Bond"), 1);
    assertEquals(store.getFeatureCount(true, "Metal", "Pfam", null), 2);

    /*
     * delete first Pfam (non-positional)
     */
    assertTrue(store.delete(sf3));
    assertEquals(store.getFeatureCount(true), 3);
    assertEquals(store.getFeatureCount(false), 0);

    /*
     * delete second Pfam (positional)
     */
    assertTrue(store.delete(sf5));
    assertEquals(store.getFeatureCount(true), 2);
    assertEquals(store.getFeatureCount(false), 0);
  }

  @Test(groups = "Functional")
  public void testGetAllFeatures()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    List<SequenceFeature> features = store.getAllFeatures();
    assertTrue(features.isEmpty());

    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf1);
    features = store.getAllFeatures();
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf1));

    SequenceFeature sf2 = new SequenceFeature("Metallic", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf2);
    features = store.getAllFeatures();
    assertEquals(features.size(), 2);
    assertTrue(features.contains(sf2));

    /*
     * add non-positional feature
     */
    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf3);
    features = store.getAllFeatures();
    assertEquals(features.size(), 3);
    assertTrue(features.contains(sf3));

    /*
     * add contact feature
     */
    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", 10,
            20, Float.NaN, null);
    store.add(sf4);
    features = store.getAllFeatures();
    assertEquals(features.size(), 4);
    assertTrue(features.contains(sf4));

    /*
     * add another Pfam
     */
    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf5);
    features = store.getAllFeatures();
    assertEquals(features.size(), 5);
    assertTrue(features.contains(sf5));

    /*
     * select by type does not apply to non-positional features
     */
    features = store.getAllFeatures("Cath");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf3));

    features = store.getAllFeatures("Pfam", "Cath", "Metal");
    assertEquals(features.size(), 3);
    assertTrue(features.contains(sf1));
    assertTrue(features.contains(sf3));
    assertTrue(features.contains(sf5));

    /*
     * delete first Pfam
     */
    assertTrue(store.delete(sf3));
    features = store.getAllFeatures();
    assertEquals(features.size(), 4);
    assertFalse(features.contains(sf3));

    /*
     * delete second Pfam
     */
    assertTrue(store.delete(sf5));
    features = store.getAllFeatures();
    assertEquals(features.size(), 3);
    assertFalse(features.contains(sf3));
  }

  @Test(groups = "Functional")
  public void testGetTotalFeatureLength()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    assertEquals(store.getTotalFeatureLength(), 0);

    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
            Float.NaN, null);
    assertTrue(store.add(sf1));
    assertEquals(store.getTotalFeatureLength(), 11);
    assertEquals(store.getTotalFeatureLength("Metal"), 11);
    assertEquals(store.getTotalFeatureLength("Plastic"), 0);

    // re-add does nothing!
    assertFalse(store.add(sf1));
    assertEquals(store.getTotalFeatureLength(), 11);

    /*
     * add non-positional feature
     */
    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf3);
    assertEquals(store.getTotalFeatureLength(), 11);

    /*
     * add contact feature - counts 1 to feature length
     */
    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc", 10,
            20, Float.NaN, null);
    store.add(sf4);
    assertEquals(store.getTotalFeatureLength(), 12);

    /*
     * add another Pfam
     */
    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf5);
    assertEquals(store.getTotalFeatureLength(), 23);

    /*
     * delete features
     */
    assertTrue(store.delete(sf3)); // non-positional
    assertEquals(store.getTotalFeatureLength(), 23); // no change

    assertTrue(store.delete(sf5));
    assertEquals(store.getTotalFeatureLength(), 12);

    assertTrue(store.delete(sf4)); // contact
    assertEquals(store.getTotalFeatureLength(), 11);

    assertTrue(store.delete(sf1));
    assertEquals(store.getTotalFeatureLength(), 0);
  }

  @Test(groups = "Functional")
  public void testGetMinimumScore_getMaximumScore()
  {
    SequenceFeatures sf = new SequenceFeatures();
    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 0, 0,
            Float.NaN, "group"); // non-positional, no score
    sf.add(sf1);
    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 10, 20,
            Float.NaN, "group"); // positional, no score
    sf.add(sf2);
    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 10, 20, 1f,
            "group");
    sf.add(sf3);
    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 12, 16, 4f,
            "group");
    sf.add(sf4);
    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 0, 0, 11f,
            "group");
    sf.add(sf5);
    SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, -7f,
            "group");
    sf.add(sf6);

    assertEquals(sf.getMinimumScore("nosuchtype", true), Float.NaN);
    assertEquals(sf.getMinimumScore("nosuchtype", false), Float.NaN);
    assertEquals(sf.getMaximumScore("nosuchtype", true), Float.NaN);
    assertEquals(sf.getMaximumScore("nosuchtype", false), Float.NaN);

    // positional features min-max:
    assertEquals(sf.getMinimumScore("Metal", true), 1f);
    assertEquals(sf.getMaximumScore("Metal", true), 4f);
    assertEquals(sf.getMinimumScore("Cath", true), Float.NaN);
    assertEquals(sf.getMaximumScore("Cath", true), Float.NaN);

    // non-positional features min-max:
    assertEquals(sf.getMinimumScore("Cath", false), -7f);
    assertEquals(sf.getMaximumScore("Cath", false), 11f);
    assertEquals(sf.getMinimumScore("Metal", false), Float.NaN);
    assertEquals(sf.getMaximumScore("Metal", false), Float.NaN);

    // delete features; min-max should get recomputed
    sf.delete(sf6);
    assertEquals(sf.getMinimumScore("Cath", false), 11f);
    assertEquals(sf.getMaximumScore("Cath", false), 11f);
    sf.delete(sf4);
    assertEquals(sf.getMinimumScore("Metal", true), 1f);
    assertEquals(sf.getMaximumScore("Metal", true), 1f);
    sf.delete(sf5);
    assertEquals(sf.getMinimumScore("Cath", false), Float.NaN);
    assertEquals(sf.getMaximumScore("Cath", false), Float.NaN);
    sf.delete(sf3);
    assertEquals(sf.getMinimumScore("Metal", true), Float.NaN);
    assertEquals(sf.getMaximumScore("Metal", true), Float.NaN);
    sf.delete(sf1);
    sf.delete(sf2);
    assertFalse(sf.hasFeatures());
    assertEquals(sf.getMinimumScore("Cath", false), Float.NaN);
    assertEquals(sf.getMaximumScore("Cath", false), Float.NaN);
    assertEquals(sf.getMinimumScore("Metal", true), Float.NaN);
    assertEquals(sf.getMaximumScore("Metal", true), Float.NaN);
  }

  @Test(groups = "Functional")
  public void testVarargsToTypes()
  {
    SequenceFeatures sf = new SequenceFeatures();
    sf.add(new SequenceFeature("Metal", "desc", 0, 0, Float.NaN, "group"));
    sf.add(new SequenceFeature("Cath", "desc", 10, 20, Float.NaN, "group"));

    /*
     * no type specified - get all types stored
     * they are returned in keyset (alphabetical) order
     */
    Map<String, FeatureStore> featureStores = (Map<String, FeatureStore>) PA
            .getValue(sf, "featureStore");

    Iterable<FeatureStore> types = sf.varargToTypes();
    Iterator<FeatureStore> iterator = types.iterator();
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Cath"));
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Metal"));
    assertFalse(iterator.hasNext());

    /*
     * empty array is the same as no vararg parameter supplied
     * so treated as all stored types
     */
    types = sf.varargToTypes(new String[] {});
    iterator = types.iterator();
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Cath"));
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Metal"));
    assertFalse(iterator.hasNext());

    /*
     * null type specified; this is passed as vararg
     * String[1] {null}
     */
    types = sf.varargToTypes((String) null);
    assertFalse(types.iterator().hasNext());

    /*
     * null types array specified; this is passed as vararg null
     */
    types = sf.varargToTypes((String[]) null);
    iterator = types.iterator();
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Cath"));
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Metal"));
    assertFalse(iterator.hasNext());

    /*
     * one type specified
     */
    types = sf.varargToTypes("Metal");
    iterator = types.iterator();
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Metal"));
    assertFalse(iterator.hasNext());

    /*
     * two types specified - order is preserved
     */
    types = sf.varargToTypes("Metal", "Cath");
    iterator = types.iterator();
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Metal"));
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Cath"));
    assertFalse(iterator.hasNext());

    /*
     * null type included - should be ignored
     */
    types = sf.varargToTypes("Metal", null, "Helix");
    iterator = types.iterator();
    assertTrue(iterator.hasNext());
    assertSame(iterator.next(), featureStores.get("Metal"));
    assertFalse(iterator.hasNext());
  }

  @Test(groups = "Functional")
  public void testGetFeatureTypes_byOntology()
  {
    SequenceFeaturesI store = new SequenceFeatures();

    SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf1);
    // mRNA isA mature_transcript isA transcript
    SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 10, 20,
            Float.NaN, null);
    store.add(sf2);
    // just to prove non-positional feature types are included
    SequenceFeature sf3 = new SequenceFeature("mRNA", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf3);
    SequenceFeature sf4 = new SequenceFeature("CDS", "desc", 0, 0,
            Float.NaN, null);
    store.add(sf4);

    Set<String> types = store.getFeatureTypes("transcript");
    assertEquals(types.size(), 2);
    assertTrue(types.contains("transcript"));
    assertTrue(types.contains("mRNA"));

    // matches include arguments whether SO terms or not
    types = store.getFeatureTypes("transcript", "CDS");
    assertEquals(types.size(), 3);
    assertTrue(types.contains("transcript"));
    assertTrue(types.contains("mRNA"));
    assertTrue(types.contains("CDS"));

    types = store.getFeatureTypes("exon");
    assertTrue(types.isEmpty());
  }

  @Test(groups = "Functional")
  public void testGetFeaturesByOntology()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    List<SequenceFeature> features = store.getFeaturesByOntology();
    assertTrue(features.isEmpty());
    assertTrue(store.getFeaturesByOntology(new String[] {}).isEmpty());
    assertTrue(store.getFeaturesByOntology((String[]) null).isEmpty());

    SequenceFeature transcriptFeature = new SequenceFeature("transcript",
            "desc", 10, 20, Float.NaN, null);
    store.add(transcriptFeature);

    /*
     * mRNA is a sub-type of transcript; added here 'as if' non-positional
     * just to show that non-positional features are included in results
     */
    SequenceFeature mrnaFeature = new SequenceFeature("mRNA", "desc", 0, 0,
            Float.NaN, null);
    store.add(mrnaFeature);

    SequenceFeature pfamFeature = new SequenceFeature("Pfam", "desc", 30,
            40, Float.NaN, null);
    store.add(pfamFeature);

    /*
     * "transcript" matches both itself and the sub-term "mRNA"
     */
    features = store.getFeaturesByOntology("transcript");
    assertEquals(features.size(), 2);
    assertTrue(features.contains(transcriptFeature));
    assertTrue(features.contains(mrnaFeature));

    /*
     * "mRNA" matches itself but not parent term "transcript"
     */
    features = store.getFeaturesByOntology("mRNA");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(mrnaFeature));

    /*
     * "pfam" is not an SO term but is included as an exact match
     */
    features = store.getFeaturesByOntology("mRNA", "Pfam");
    assertEquals(features.size(), 2);
    assertTrue(features.contains(mrnaFeature));
    assertTrue(features.contains(pfamFeature));

    features = store.getFeaturesByOntology("sequence_variant");
    assertTrue(features.isEmpty());
  }

  @Test(groups = "Functional")
  public void testSortFeatures()
  {
    List<SequenceFeature> sfs = new ArrayList<>();
    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30, 60,
            Float.NaN, null);
    sfs.add(sf1);
    SequenceFeature sf2 = new SequenceFeature("Rfam", "desc", 40, 50,
            Float.NaN, null);
    sfs.add(sf2);
    SequenceFeature sf3 = new SequenceFeature("Rfam", "desc", 50, 60,
            Float.NaN, null);
    sfs.add(sf3);
    SequenceFeature sf4 = new SequenceFeature("Xfam", "desc", 30, 80,
            Float.NaN, null);
    sfs.add(sf4);
    SequenceFeature sf5 = new SequenceFeature("Xfam", "desc", 30, 90,
            Float.NaN, null);
    sfs.add(sf5);

    /*
     * sort by end position descending, order unchanged if matched
     */
    SequenceFeatures.sortFeatures(sfs, false);
    assertSame(sfs.get(0), sf5); // end 90
    assertSame(sfs.get(1), sf4); // end 80
    assertSame(sfs.get(2), sf1); // end 60, start 50
    assertSame(sfs.get(3), sf3); // end 60, start 30
    assertSame(sfs.get(4), sf2); // end 50

    /*
     * resort {5, 4, 1, 3, 2} by start position ascending, end descending
     */
    SequenceFeatures.sortFeatures(sfs, true);
    assertSame(sfs.get(0), sf5); // start 30, end 90
    assertSame(sfs.get(1), sf4); // start 30, end 80
    assertSame(sfs.get(2), sf1); // start 30, end 60
    assertSame(sfs.get(3), sf2); // start 40
    assertSame(sfs.get(4), sf3); // start 50
  }

  @Test(groups = "Functional")
  public void testGetFeaturesForGroup()
  {
    SequenceFeaturesI store = new SequenceFeatures();

    List<SequenceFeature> features = store.getFeaturesForGroup(true, null);
    assertTrue(features.isEmpty());
    assertTrue(store.getFeaturesForGroup(false, null).isEmpty());
    assertTrue(store.getFeaturesForGroup(true, "Uniprot").isEmpty());
    assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty());

    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
            null);
    SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
            null);
    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
            "Uniprot");
    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 0, 0, 0f,
            "Rfam");
    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 5, 15, 0f,
            null);
    store.add(sf1);
    store.add(sf2);
    store.add(sf3);
    store.add(sf4);
    store.add(sf5);

    // positional features for null group, any type
    features = store.getFeaturesForGroup(true, null);
    assertEquals(features.size(), 2);
    assertTrue(features.contains(sf1));
    assertTrue(features.contains(sf5));

    // positional features for null group, specified type
    features = store.getFeaturesForGroup(true, null,
            new String[]
            { "Pfam", "Xfam" });
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf1));
    features = store.getFeaturesForGroup(true, null,
            new String[]
            { "Pfam", "Xfam", "Cath" });
    assertEquals(features.size(), 2);
    assertTrue(features.contains(sf1));
    assertTrue(features.contains(sf5));

    // positional features for non-null group, any type
    features = store.getFeaturesForGroup(true, "Uniprot");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf3));
    assertTrue(store.getFeaturesForGroup(true, "Rfam").isEmpty());

    // positional features for non-null group, specified type
    features = store.getFeaturesForGroup(true, "Uniprot", "Pfam", "Xfam",
            "Rfam");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf3));
    assertTrue(
            store.getFeaturesForGroup(true, "Uniprot", "Cath").isEmpty());

    // non-positional features for null group, any type
    features = store.getFeaturesForGroup(false, null);
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf2));

    // non-positional features for null group, specified type
    features = store.getFeaturesForGroup(false, null, "Pfam", "Xfam");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf2));
    assertTrue(store.getFeaturesForGroup(false, null, "Cath").isEmpty());

    // non-positional features for non-null group, any type
    features = store.getFeaturesForGroup(false, "Rfam");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf4));
    assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty());

    // non-positional features for non-null group, specified type
    features = store.getFeaturesForGroup(false, "Rfam", "Pfam", "Metal");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf4));
    assertTrue(store.getFeaturesForGroup(false, "Rfam", "Cath", "Pfam")
            .isEmpty());
  }

  @Test(groups = "Functional")
  public void testShiftFeatures()
  {
    SequenceFeatures store = new SequenceFeatures();
    assertFalse(store.shiftFeatures(0, 1));

    SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
    store.add(sf1);
    // nested feature:
    SequenceFeature sf2 = new SequenceFeature("Metal", "", 8, 14, 0f, null);
    store.add(sf2);
    // contact feature:
    SequenceFeature sf3 = new SequenceFeature("Disulfide bond", "", 23, 32,
            0f, null);
    store.add(sf3);
    // non-positional feature:
    SequenceFeature sf4 = new SequenceFeature("Pfam", "", 0, 0, 0f, null);
    store.add(sf4);

    /*
     * shift features right by 5
     */
    assertTrue(store.shiftFeatures(0, 5));

    // non-positional features untouched:
    List<SequenceFeature> nonPos = store.getNonPositionalFeatures();
    assertEquals(nonPos.size(), 1);
    assertTrue(nonPos.contains(sf4));

    // positional features are replaced
    List<SequenceFeature> pos = store.getPositionalFeatures();
    assertEquals(pos.size(), 3);
    assertFalse(pos.contains(sf1));
    assertFalse(pos.contains(sf2));
    assertFalse(pos.contains(sf3));
    SequenceFeatures.sortFeatures(pos, true); // ascending start pos
    assertEquals(pos.get(0).getBegin(), 7);
    assertEquals(pos.get(0).getEnd(), 10);
    assertEquals(pos.get(0).getType(), "Cath");
    assertEquals(pos.get(1).getBegin(), 13);
    assertEquals(pos.get(1).getEnd(), 19);
    assertEquals(pos.get(1).getType(), "Metal");
    assertEquals(pos.get(2).getBegin(), 28);
    assertEquals(pos.get(2).getEnd(), 37);
    assertEquals(pos.get(2).getType(), "Disulfide bond");

    /*
     * now shift left by 15
     * feature at [7-10] should be removed
     * feature at [13-19] should become [1-4] 
     */
    assertTrue(store.shiftFeatures(0, -15));
    pos = store.getPositionalFeatures();
    assertEquals(pos.size(), 2);
    SequenceFeatures.sortFeatures(pos, true);
    assertEquals(pos.get(0).getBegin(), 1);
    assertEquals(pos.get(0).getEnd(), 4);
    assertEquals(pos.get(0).getType(), "Metal");
    assertEquals(pos.get(1).getBegin(), 13);
    assertEquals(pos.get(1).getEnd(), 22);
    assertEquals(pos.get(1).getType(), "Disulfide bond");

    /*
     * shift right by 4 from column 2
     * feature at [1-4] should be unchanged
     * feature at [13-22] should become [17-26] 
     */
    assertTrue(store.shiftFeatures(2, 4));
    pos = store.getPositionalFeatures();
    assertEquals(pos.size(), 2);
    SequenceFeatures.sortFeatures(pos, true);
    assertEquals(pos.get(0).getBegin(), 1);
    assertEquals(pos.get(0).getEnd(), 4);
    assertEquals(pos.get(0).getType(), "Metal");
    assertEquals(pos.get(1).getBegin(), 17);
    assertEquals(pos.get(1).getEnd(), 26);
    assertEquals(pos.get(1).getType(), "Disulfide bond");

    /*
     * shift right from column 18
     * should be no updates
     */
    SequenceFeature f1 = pos.get(0);
    SequenceFeature f2 = pos.get(1);
    assertFalse(store.shiftFeatures(18, 6));
    pos = store.getPositionalFeatures();
    assertEquals(pos.size(), 2);
    SequenceFeatures.sortFeatures(pos, true);
    assertSame(pos.get(0), f1);
    assertSame(pos.get(1), f2);
  }

  @Test(groups = "Functional")
  public void testIsOntologyTerm()
  {
    SequenceFeatures store = new SequenceFeatures();
    assertTrue(store.isOntologyTerm("gobbledygook"));
    assertTrue(store.isOntologyTerm("transcript", "transcript"));
    assertTrue(store.isOntologyTerm("mRNA", "transcript"));
    assertFalse(store.isOntologyTerm("transcript", "mRNA"));
    assertTrue(store.isOntologyTerm("junk", "transcript", "junk"));
    assertTrue(store.isOntologyTerm("junk", new String[] {}));
    assertTrue(store.isOntologyTerm("junk", (String[]) null));
  }

  @Test(groups = "Functional")
  public void testDeleteAll()
  {
    SequenceFeaturesI store = new SequenceFeatures();
    assertFalse(store.hasFeatures());
    store.deleteAll();
    assertFalse(store.hasFeatures());
    store.add(new SequenceFeature("Cath", "Desc", 12, 20, 0f, "Group"));
    store.add(new SequenceFeature("Pfam", "Desc", 6, 12, 2f, "Group2"));
    assertTrue(store.hasFeatures());
    store.deleteAll();
    assertFalse(store.hasFeatures());
  }
}
