/*
 * 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 jalview.datamodel.SequenceFeature;

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

import org.testng.annotations.Test;

public class FeatureStoreTest
{

  @Test(groups = "Functional")
  public void testFindFeatures_nonNested()
  {
    FeatureStore fs = new FeatureStore();
    fs.addFeature(new SequenceFeature("", "", 10, 20, Float.NaN, null));
    // same range different description
    fs.addFeature(new SequenceFeature("", "desc", 10, 20, Float.NaN, null));
    fs.addFeature(new SequenceFeature("", "", 15, 25, Float.NaN, null));
    fs.addFeature(new SequenceFeature("", "", 20, 35, Float.NaN, null));

    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
    assertTrue(overlaps.isEmpty());

    overlaps = fs.findOverlappingFeatures(8, 10);
    assertEquals(overlaps.size(), 2);
    assertEquals(overlaps.get(0).getEnd(), 20);
    assertEquals(overlaps.get(1).getEnd(), 20);

    overlaps = fs.findOverlappingFeatures(12, 16);
    assertEquals(overlaps.size(), 3);
    assertEquals(overlaps.get(0).getEnd(), 20);
    assertEquals(overlaps.get(1).getEnd(), 20);
    assertEquals(overlaps.get(2).getEnd(), 25);

    overlaps = fs.findOverlappingFeatures(33, 33);
    assertEquals(overlaps.size(), 1);
    assertEquals(overlaps.get(0).getEnd(), 35);
  }

  @Test(groups = "Functional")
  public void testFindFeatures_nested()
  {
    FeatureStore fs = new FeatureStore();
    SequenceFeature sf1 = addFeature(fs, 10, 50);
    SequenceFeature sf2 = addFeature(fs, 10, 40);
    SequenceFeature sf3 = addFeature(fs, 20, 30);
    // fudge feature at same location but different group (so is added)
    SequenceFeature sf4 = new SequenceFeature("", "", 20, 30, Float.NaN,
            "different group");
    fs.addFeature(sf4);
    SequenceFeature sf5 = addFeature(fs, 35, 36);

    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
    assertTrue(overlaps.isEmpty());

    overlaps = fs.findOverlappingFeatures(10, 15);
    assertEquals(overlaps.size(), 2);
    assertTrue(overlaps.contains(sf1));
    assertTrue(overlaps.contains(sf2));

    overlaps = fs.findOverlappingFeatures(45, 60);
    assertEquals(overlaps.size(), 1);
    assertTrue(overlaps.contains(sf1));

    overlaps = fs.findOverlappingFeatures(32, 38);
    assertEquals(overlaps.size(), 3);
    assertTrue(overlaps.contains(sf1));
    assertTrue(overlaps.contains(sf2));
    assertTrue(overlaps.contains(sf5));

    overlaps = fs.findOverlappingFeatures(15, 25);
    assertEquals(overlaps.size(), 4);
    assertTrue(overlaps.contains(sf1));
    assertTrue(overlaps.contains(sf2));
    assertTrue(overlaps.contains(sf3));
    assertTrue(overlaps.contains(sf4));
  }

  @Test(groups = "Functional")
  public void testFindFeatures_mixed()
  {
    FeatureStore fs = new FeatureStore();
    SequenceFeature sf1 = addFeature(fs, 10, 50);
    SequenceFeature sf2 = addFeature(fs, 1, 15);
    SequenceFeature sf3 = addFeature(fs, 20, 30);
    SequenceFeature sf4 = addFeature(fs, 40, 100);
    SequenceFeature sf5 = addFeature(fs, 60, 100);
    SequenceFeature sf6 = addFeature(fs, 70, 70);

    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(200, 200);
    assertTrue(overlaps.isEmpty());

    overlaps = fs.findOverlappingFeatures(1, 9);
    assertEquals(overlaps.size(), 1);
    assertTrue(overlaps.contains(sf2));

    overlaps = fs.findOverlappingFeatures(5, 18);
    assertEquals(overlaps.size(), 2);
    assertTrue(overlaps.contains(sf1));
    assertTrue(overlaps.contains(sf2));

    overlaps = fs.findOverlappingFeatures(30, 40);
    assertEquals(overlaps.size(), 3);
    assertTrue(overlaps.contains(sf1));
    assertTrue(overlaps.contains(sf3));
    assertTrue(overlaps.contains(sf4));

    overlaps = fs.findOverlappingFeatures(80, 90);
    assertEquals(overlaps.size(), 2);
    assertTrue(overlaps.contains(sf4));
    assertTrue(overlaps.contains(sf5));

    overlaps = fs.findOverlappingFeatures(68, 70);
    assertEquals(overlaps.size(), 3);
    assertTrue(overlaps.contains(sf4));
    assertTrue(overlaps.contains(sf5));
    assertTrue(overlaps.contains(sf6));
  }

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

  @Test(groups = "Functional")
  public void testFindFeatures_contactFeatures()
  {
    FeatureStore fs = new FeatureStore();

    SequenceFeature sf = new SequenceFeature("disulphide bond", "bond", 10,
            20, Float.NaN, null);
    fs.addFeature(sf);

    /*
     * neither contact point in range
     */
    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
    assertTrue(overlaps.isEmpty());

    /*
     * neither contact point in range
     */
    overlaps = fs.findOverlappingFeatures(11, 19);
    assertTrue(overlaps.isEmpty());

    /*
     * first contact point in range
     */
    overlaps = fs.findOverlappingFeatures(5, 15);
    assertEquals(overlaps.size(), 1);
    assertTrue(overlaps.contains(sf));

    /*
     * second contact point in range
     */
    overlaps = fs.findOverlappingFeatures(15, 25);
    assertEquals(overlaps.size(), 1);
    assertTrue(overlaps.contains(sf));

    /*
     * both contact points in range
     */
    overlaps = fs.findOverlappingFeatures(5, 25);
    assertEquals(overlaps.size(), 1);
    assertTrue(overlaps.contains(sf));
  }

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

    List<SequenceFeature> features = store.getPositionalFeatures();
    assertEquals(features.size(), 6);
    assertTrue(features.contains(sf1));
    assertTrue(features.contains(sf2));
    assertTrue(features.contains(sf3));
    assertTrue(features.contains(sf4));
    assertTrue(features.contains(sf5));
    assertFalse(features.contains(sf6));
    assertTrue(features.contains(sf7));

    features = store.getNonPositionalFeatures();
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf6));
  }

  @Test(groups = "Functional")
  public void testDelete()
  {
    FeatureStore store = new FeatureStore();
    SequenceFeature sf1 = addFeature(store, 10, 20);
    assertTrue(store.getPositionalFeatures().contains(sf1));

    /*
     * simple deletion
     */
    assertTrue(store.delete(sf1));
    assertTrue(store.getPositionalFeatures().isEmpty());

    /*
     * non-positional feature deletion
     */
    SequenceFeature sf2 = addFeature(store, 0, 0);
    assertFalse(store.getPositionalFeatures().contains(sf2));
    assertTrue(store.getNonPositionalFeatures().contains(sf2));
    assertTrue(store.delete(sf2));
    assertTrue(store.getNonPositionalFeatures().isEmpty());

    /*
     * contact feature deletion
     */
    SequenceFeature sf3 = new SequenceFeature("", "Disulphide Bond", 11, 23,
            Float.NaN, null);
    store.addFeature(sf3);
    assertEquals(store.getPositionalFeatures().size(), 1);
    assertTrue(store.getPositionalFeatures().contains(sf3));
    assertTrue(store.delete(sf3));
    assertTrue(store.getPositionalFeatures().isEmpty());

    /*
     * nested feature deletion
     */
    SequenceFeature sf4 = addFeature(store, 20, 30);
    SequenceFeature sf5 = addFeature(store, 22, 26); // to NCList
    SequenceFeature sf6 = addFeature(store, 23, 24); // child of sf5
    SequenceFeature sf7 = addFeature(store, 25, 25); // sibling of sf6
    SequenceFeature sf8 = addFeature(store, 24, 24); // child of sf6
    SequenceFeature sf9 = addFeature(store, 23, 23); // child of sf6
    assertEquals(store.getPositionalFeatures().size(), 6);

    // delete a node with children - they take its place
    assertTrue(store.delete(sf6)); // sf8, sf9 should become children of sf5
    assertEquals(store.getPositionalFeatures().size(), 5);
    assertFalse(store.getPositionalFeatures().contains(sf6));

    // delete a node with no children
    assertTrue(store.delete(sf7));
    assertEquals(store.getPositionalFeatures().size(), 4);
    assertFalse(store.getPositionalFeatures().contains(sf7));

    // delete root of NCList
    assertTrue(store.delete(sf5));
    assertEquals(store.getPositionalFeatures().size(), 3);
    assertFalse(store.getPositionalFeatures().contains(sf5));

    // continue the killing fields
    assertTrue(store.delete(sf4));
    assertEquals(store.getPositionalFeatures().size(), 2);
    assertFalse(store.getPositionalFeatures().contains(sf4));

    assertTrue(store.delete(sf9));
    assertEquals(store.getPositionalFeatures().size(), 1);
    assertFalse(store.getPositionalFeatures().contains(sf9));

    assertTrue(store.delete(sf8));
    assertTrue(store.getPositionalFeatures().isEmpty());
  }

  @Test(groups = "Functional")
  public void testAddFeature()
  {
    FeatureStore fs = new FeatureStore();

    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20, Float.NaN,
            null);
    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20, Float.NaN,
            null);

    assertTrue(fs.addFeature(sf1));
    assertEquals(fs.getFeatureCount(true), 1); // positional
    assertEquals(fs.getFeatureCount(false), 0); // non-positional

    /*
     * re-adding the same or an identical feature should fail
     */
    assertFalse(fs.addFeature(sf1));
    assertEquals(fs.getFeatureCount(true), 1);
    assertFalse(fs.addFeature(sf2));
    assertEquals(fs.getFeatureCount(true), 1);

    /*
     * add non-positional
     */
    SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
            null);
    assertTrue(fs.addFeature(sf3));
    assertEquals(fs.getFeatureCount(true), 1); // positional
    assertEquals(fs.getFeatureCount(false), 1); // non-positional
    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
            null);
    assertFalse(fs.addFeature(sf4)); // already stored
    assertEquals(fs.getFeatureCount(true), 1); // positional
    assertEquals(fs.getFeatureCount(false), 1); // non-positional

    /*
     * add contact
     */
    SequenceFeature sf5 = new SequenceFeature("Disulfide bond", "", 10, 20,
            Float.NaN, null);
    assertTrue(fs.addFeature(sf5));
    assertEquals(fs.getFeatureCount(true), 2); // positional - add 1 for contact
    assertEquals(fs.getFeatureCount(false), 1); // non-positional
    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "", 10, 20,
            Float.NaN, null);
    assertFalse(fs.addFeature(sf6)); // already stored
    assertEquals(fs.getFeatureCount(true), 2); // no change
    assertEquals(fs.getFeatureCount(false), 1); // no change
  }

  @Test(groups = "Functional")
  public void testIsEmpty()
  {
    FeatureStore fs = new FeatureStore();
    assertTrue(fs.isEmpty());
    assertEquals(fs.getFeatureCount(true), 0);

    /*
     * non-nested feature
     */
    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20, Float.NaN,
            null);
    fs.addFeature(sf1);
    assertFalse(fs.isEmpty());
    assertEquals(fs.getFeatureCount(true), 1);
    fs.delete(sf1);
    assertTrue(fs.isEmpty());
    assertEquals(fs.getFeatureCount(true), 0);

    /*
     * non-positional feature
     */
    sf1 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, null);
    fs.addFeature(sf1);
    assertFalse(fs.isEmpty());
    assertEquals(fs.getFeatureCount(false), 1); // non-positional
    assertEquals(fs.getFeatureCount(true), 0); // positional
    fs.delete(sf1);
    assertTrue(fs.isEmpty());
    assertEquals(fs.getFeatureCount(false), 0);

    /*
     * contact feature
     */
    sf1 = new SequenceFeature("Disulfide bond", "", 19, 49, Float.NaN,
            null);
    fs.addFeature(sf1);
    assertFalse(fs.isEmpty());
    assertEquals(fs.getFeatureCount(true), 1);
    fs.delete(sf1);
    assertTrue(fs.isEmpty());
    assertEquals(fs.getFeatureCount(true), 0);

    /*
     * sf2, sf3 added as nested features
     */
    sf1 = new SequenceFeature("Cath", "", 19, 49, Float.NaN, null);
    SequenceFeature sf2 = new SequenceFeature("Cath", "", 20, 40, Float.NaN,
            null);
    SequenceFeature sf3 = new SequenceFeature("Cath", "", 25, 35, Float.NaN,
            null);
    fs.addFeature(sf1);
    fs.addFeature(sf2);
    fs.addFeature(sf3);
    assertEquals(fs.getFeatureCount(true), 3);
    assertTrue(fs.delete(sf1));
    assertEquals(fs.getFeatureCount(true), 2);
    assertEquals(fs.features.size(), 2);
    assertFalse(fs.isEmpty());
    assertTrue(fs.delete(sf2));
    assertEquals(fs.getFeatureCount(true), 1);
    assertFalse(fs.isEmpty());
    assertTrue(fs.delete(sf3));
    assertEquals(fs.getFeatureCount(true), 0);
    assertTrue(fs.isEmpty()); // all gone
  }

  @Test(groups = "Functional")
  public void testGetFeatureGroups()
  {
    FeatureStore fs = new FeatureStore();
    assertTrue(fs.getFeatureGroups(true).isEmpty());
    assertTrue(fs.getFeatureGroups(false).isEmpty());

    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f,
            "group1");
    fs.addFeature(sf1);
    Set<String> groups = fs.getFeatureGroups(true);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("group1"));

    /*
     * add another feature of the same group, delete one, delete both
     */
    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 20, 30, 1f,
            "group1");
    fs.addFeature(sf2);
    groups = fs.getFeatureGroups(true);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("group1"));
    fs.delete(sf2);
    groups = fs.getFeatureGroups(true);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("group1"));
    fs.delete(sf1);
    groups = fs.getFeatureGroups(true);
    assertTrue(fs.getFeatureGroups(true).isEmpty());

    SequenceFeature sf3 = new SequenceFeature("Cath", "desc", 20, 30, 1f,
            "group2");
    fs.addFeature(sf3);
    SequenceFeature sf4 = new SequenceFeature("Cath", "desc", 20, 30, 1f,
            "Group2");
    fs.addFeature(sf4);
    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 20, 30, 1f,
            null);
    fs.addFeature(sf5);
    groups = fs.getFeatureGroups(true);
    assertEquals(groups.size(), 3);
    assertTrue(groups.contains("group2"));
    assertTrue(groups.contains("Group2")); // case sensitive
    assertTrue(groups.contains(null)); // null allowed
    assertTrue(fs.getFeatureGroups(false).isEmpty()); // non-positional

    fs.delete(sf3);
    groups = fs.getFeatureGroups(true);
    assertEquals(groups.size(), 2);
    assertFalse(groups.contains("group2"));
    fs.delete(sf4);
    groups = fs.getFeatureGroups(true);
    assertEquals(groups.size(), 1);
    assertFalse(groups.contains("Group2"));
    fs.delete(sf5);
    groups = fs.getFeatureGroups(true);
    assertTrue(groups.isEmpty());

    /*
     * add non-positional feature
     */
    SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
            "CathGroup");
    fs.addFeature(sf6);
    groups = fs.getFeatureGroups(false);
    assertEquals(groups.size(), 1);
    assertTrue(groups.contains("CathGroup"));
    assertTrue(fs.delete(sf6));
    assertTrue(fs.getFeatureGroups(false).isEmpty());
  }

  @Test(groups = "Functional")
  public void testGetTotalFeatureLength()
  {
    FeatureStore fs = new FeatureStore();
    assertEquals(fs.getTotalFeatureLength(), 0);

    addFeature(fs, 10, 20); // 11
    assertEquals(fs.getTotalFeatureLength(), 11);
    addFeature(fs, 17, 37); // 21
    SequenceFeature sf1 = addFeature(fs, 14, 74); // 61
    assertEquals(fs.getTotalFeatureLength(), 93);

    // non-positional features don't count
    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
            "group1");
    fs.addFeature(sf2);
    assertEquals(fs.getTotalFeatureLength(), 93);

    // contact features count 1
    SequenceFeature sf3 = new SequenceFeature("disulphide bond", "desc", 15,
            35, 1f, "group1");
    fs.addFeature(sf3);
    assertEquals(fs.getTotalFeatureLength(), 94);

    assertTrue(fs.delete(sf1));
    assertEquals(fs.getTotalFeatureLength(), 33);
    assertFalse(fs.delete(sf1));
    assertEquals(fs.getTotalFeatureLength(), 33);
    assertTrue(fs.delete(sf2));
    assertEquals(fs.getTotalFeatureLength(), 33);
    assertTrue(fs.delete(sf3));
    assertEquals(fs.getTotalFeatureLength(), 32);
  }

  @Test(groups = "Functional")
  public void testGetFeatureLength()
  {
    /*
     * positional feature
     */
    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f,
            "group1");
    assertEquals(FeatureStore.getFeatureLength(sf1), 11);

    /*
     * non-positional feature
     */
    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
            "CathGroup");
    assertEquals(FeatureStore.getFeatureLength(sf2), 0);

    /*
     * contact feature counts 1
     */
    SequenceFeature sf3 = new SequenceFeature("Disulphide Bond", "desc", 14,
            28, 1f, "AGroup");
    assertEquals(FeatureStore.getFeatureLength(sf3), 1);
  }

  @Test(groups = "Functional")
  public void testMin()
  {
    assertEquals(FeatureStore.min(Float.NaN, Float.NaN), Float.NaN);
    assertEquals(FeatureStore.min(Float.NaN, 2f), 2f);
    assertEquals(FeatureStore.min(-2f, Float.NaN), -2f);
    assertEquals(FeatureStore.min(2f, -3f), -3f);
  }

  @Test(groups = "Functional")
  public void testMax()
  {
    assertEquals(FeatureStore.max(Float.NaN, Float.NaN), Float.NaN);
    assertEquals(FeatureStore.max(Float.NaN, 2f), 2f);
    assertEquals(FeatureStore.max(-2f, Float.NaN), -2f);
    assertEquals(FeatureStore.max(2f, -3f), 2f);
  }

  @Test(groups = "Functional")
  public void testGetMinimumScore_getMaximumScore()
  {
    FeatureStore fs = new FeatureStore();
    assertEquals(fs.getMinimumScore(true), Float.NaN); // positional
    assertEquals(fs.getMaximumScore(true), Float.NaN);
    assertEquals(fs.getMinimumScore(false), Float.NaN); // non-positional
    assertEquals(fs.getMaximumScore(false), Float.NaN);

    // add features with no score
    SequenceFeature sf1 = new SequenceFeature("type", "desc", 0, 0,
            Float.NaN, "group");
    fs.addFeature(sf1);
    SequenceFeature sf2 = new SequenceFeature("type", "desc", 10, 20,
            Float.NaN, "group");
    fs.addFeature(sf2);
    assertEquals(fs.getMinimumScore(true), Float.NaN);
    assertEquals(fs.getMaximumScore(true), Float.NaN);
    assertEquals(fs.getMinimumScore(false), Float.NaN);
    assertEquals(fs.getMaximumScore(false), Float.NaN);

    // add positional features with score
    SequenceFeature sf3 = new SequenceFeature("type", "desc", 10, 20, 1f,
            "group");
    fs.addFeature(sf3);
    SequenceFeature sf4 = new SequenceFeature("type", "desc", 12, 16, 4f,
            "group");
    fs.addFeature(sf4);
    assertEquals(fs.getMinimumScore(true), 1f);
    assertEquals(fs.getMaximumScore(true), 4f);
    assertEquals(fs.getMinimumScore(false), Float.NaN);
    assertEquals(fs.getMaximumScore(false), Float.NaN);

    // add non-positional features with score
    SequenceFeature sf5 = new SequenceFeature("type", "desc", 0, 0, 11f,
            "group");
    fs.addFeature(sf5);
    SequenceFeature sf6 = new SequenceFeature("type", "desc", 0, 0, -7f,
            "group");
    fs.addFeature(sf6);
    assertEquals(fs.getMinimumScore(true), 1f);
    assertEquals(fs.getMaximumScore(true), 4f);
    assertEquals(fs.getMinimumScore(false), -7f);
    assertEquals(fs.getMaximumScore(false), 11f);

    // delete one positional and one non-positional
    // min-max should be recomputed
    assertTrue(fs.delete(sf6));
    assertTrue(fs.delete(sf3));
    assertEquals(fs.getMinimumScore(true), 4f);
    assertEquals(fs.getMaximumScore(true), 4f);
    assertEquals(fs.getMinimumScore(false), 11f);
    assertEquals(fs.getMaximumScore(false), 11f);

    // delete remaining features with score
    assertTrue(fs.delete(sf4));
    assertTrue(fs.delete(sf5));
    assertEquals(fs.getMinimumScore(true), Float.NaN);
    assertEquals(fs.getMaximumScore(true), Float.NaN);
    assertEquals(fs.getMinimumScore(false), Float.NaN);
    assertEquals(fs.getMaximumScore(false), Float.NaN);

    // delete all features
    assertTrue(fs.delete(sf1));
    assertTrue(fs.delete(sf2));
    assertTrue(fs.isEmpty());
    assertEquals(fs.getMinimumScore(true), Float.NaN);
    assertEquals(fs.getMaximumScore(true), Float.NaN);
    assertEquals(fs.getMinimumScore(false), Float.NaN);
    assertEquals(fs.getMaximumScore(false), Float.NaN);
  }

  @Test(groups = "Functional")
  public void testListContains()
  {
    assertFalse(FeatureStore.listContains(null, null));
    List<SequenceFeature> features = new ArrayList<>();
    assertFalse(FeatureStore.listContains(features, null));

    SequenceFeature sf1 = new SequenceFeature("type1", "desc1", 20, 30, 3f,
            "group1");
    assertFalse(FeatureStore.listContains(null, sf1));
    assertFalse(FeatureStore.listContains(features, sf1));

    features.add(sf1);
    SequenceFeature sf2 = new SequenceFeature("type1", "desc1", 20, 30, 3f,
            "group1");
    SequenceFeature sf3 = new SequenceFeature("type1", "desc1", 20, 40, 3f,
            "group1");

    // sf2.equals(sf1) so contains should return true
    assertTrue(FeatureStore.listContains(features, sf2));
    assertFalse(FeatureStore.listContains(features, sf3));
  }

  @Test(groups = "Functional")
  public void testGetFeaturesForGroup()
  {
    FeatureStore fs = new FeatureStore();

    /*
     * with no features
     */
    assertTrue(fs.getFeaturesForGroup(true, null).isEmpty());
    assertTrue(fs.getFeaturesForGroup(false, null).isEmpty());
    assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty());
    assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty());

    /*
     * sf1: positional feature in the null group
     */
    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
            null);
    fs.addFeature(sf1);
    assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty());
    assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty());
    assertTrue(fs.getFeaturesForGroup(false, null).isEmpty());
    List<SequenceFeature> features = fs.getFeaturesForGroup(true, null);
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf1));

    /*
     * sf2: non-positional feature in the null group
     * sf3: positional feature in a non-null group
     * sf4: non-positional feature in a non-null group
     */
    SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
            null);
    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
            "Uniprot");
    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
            "Rfam");
    fs.addFeature(sf2);
    fs.addFeature(sf3);
    fs.addFeature(sf4);

    features = fs.getFeaturesForGroup(true, null);
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf1));

    features = fs.getFeaturesForGroup(false, null);
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf2));

    features = fs.getFeaturesForGroup(true, "Uniprot");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf3));

    features = fs.getFeaturesForGroup(false, "Rfam");
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf4));
  }

  @Test(groups = "Functional")
  public void testShiftFeatures()
  {
    FeatureStore fs = new FeatureStore();
    assertFalse(fs.shiftFeatures(0, 1)); // nothing to do

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

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

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

    // positional features are replaced
    List<SequenceFeature> pos = fs.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(1).getBegin(), 13);
    assertEquals(pos.get(1).getEnd(), 19);
    assertEquals(pos.get(2).getBegin(), 28);
    assertEquals(pos.get(2).getEnd(), 37);

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

    /*
     * shift right by 4 from position 2 onwards
     * feature at [1-4] unchanged, feature at [13-22] shifts
     */
    assertTrue(fs.shiftFeatures(2, 4));
    pos = fs.getPositionalFeatures();
    assertEquals(pos.size(), 2);
    SequenceFeatures.sortFeatures(pos, true);
    assertEquals(pos.get(0).getBegin(), 1);
    assertEquals(pos.get(0).getEnd(), 4);
    assertEquals(pos.get(1).getBegin(), 17);
    assertEquals(pos.get(1).getEnd(), 26);

    /*
     * shift right by 4 from position 18 onwards
     * should be no change
     */
    SequenceFeature f1 = pos.get(0);
    SequenceFeature f2 = pos.get(1);
    assertFalse(fs.shiftFeatures(18, 4)); // no update
    pos = fs.getPositionalFeatures();
    assertEquals(pos.size(), 2);
    SequenceFeatures.sortFeatures(pos, true);
    assertSame(pos.get(0), f1);
    assertSame(pos.get(1), f2);
  }

  @Test(groups = "Functional")
  public void testDelete_readd()
  {
    /*
     * add a feature and a nested feature
     */
    FeatureStore store = new FeatureStore();
    SequenceFeature sf1 = addFeature(store, 10, 20);
    // sf2 is nested in sf1 so will be stored in nestedFeatures
    SequenceFeature sf2 = addFeature(store, 12, 14);
    List<SequenceFeature> features = store.getPositionalFeatures();
    assertEquals(features.size(), 2);
    assertTrue(features.contains(sf1));
    assertTrue(features.contains(sf2));
    assertTrue(store.features.contains(sf1));
    assertTrue(store.features.contains(sf2));

    /*
     * delete the first feature
     */
    assertTrue(store.delete(sf1));
    features = store.getPositionalFeatures();
    assertFalse(features.contains(sf1));
    assertTrue(features.contains(sf2));

    /*
     * re-add the 'nested' feature; is it now duplicated?
     */
    store.addFeature(sf2);
    features = store.getPositionalFeatures();
    assertEquals(features.size(), 1);
    assertTrue(features.contains(sf2));
  }

  @Test(groups = "Functional")
  public void testContains()
  {
    FeatureStore fs = new FeatureStore();
    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20, Float.NaN,
            "group1");
    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20, Float.NaN,
            "group2");
    SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
            "group1");
    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, 0f,
            "group1");
    SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "", 5, 15,
            Float.NaN, "group1");
    SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "", 5, 15,
            Float.NaN, "group2");

    fs.addFeature(sf1);
    fs.addFeature(sf3);
    fs.addFeature(sf5);
    assertTrue(fs.contains(sf1)); // positional feature
    assertTrue(fs.contains(new SequenceFeature(sf1))); // identical feature
    assertFalse(fs.contains(sf2)); // different group
    assertTrue(fs.contains(sf3)); // non-positional
    assertTrue(fs.contains(new SequenceFeature(sf3)));
    assertFalse(fs.contains(sf4)); // different score
    assertTrue(fs.contains(sf5)); // contact feature
    assertTrue(fs.contains(new SequenceFeature(sf5)));
    assertFalse(fs.contains(sf6)); // different group

    /*
     * add a nested feature
     */
    SequenceFeature sf7 = new SequenceFeature("Cath", "", 12, 16, Float.NaN,
            "group1");
    fs.addFeature(sf7);
    assertTrue(fs.contains(sf7));
    assertTrue(fs.contains(new SequenceFeature(sf7)));

    /*
     * delete the outer (enclosing, non-nested) feature
     */
    fs.delete(sf1);
    assertFalse(fs.contains(sf1));
    assertTrue(fs.contains(sf7));
  }
}
