content: /*************************************************************************
  *                                                                        *
  *  This file is part of the 20n/act project.                             *
  *  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
  *  Copyright (C) 2017 20n Labs, Inc.                                     *
  *                                                                        *
  *  Please direct all queries to act@20n.com.                             *
  *                                                                        *
  *  This program 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.                                   *
  *                                                                        *
  *  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. *
  *                                                                        *
  *************************************************************************/
  
  package com.act.biointerpretation.networkanalysis;
  
  import com.act.lcms.v2.Metabolite;
  import com.act.lcms.v2.MolecularStructure;
  import org.junit.Before;
  import org.junit.Test;
  import org.mockito.Mockito;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.stream.Collectors;
  
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertTrue;
  
  /**
   * Tests for class MetabolismNetwork
   */
  public class MetabolismNetworkTest {
  
    private static final String METABOLITE_1 = "A";
    private static final String METABOLITE_2 = "B";
    private static final String METABOLITE_3 = "C";
    private static final String METABOLITE_4 = "D";
    private static final String METABOLITE_5 = "E";
    private static final String METABOLITE_6 = "F";
    private static final String METABOLITE_7 = "G";
  
    private static final Double MASS_1 = 0.1;
    private static final Double MASS_2 = 0.2;
    private static final Double MASS_3 = 0.4;
    private static final Double MASS_4 = 0.6;
    private static final Double MASS_5 = 0.7;
    private static final Double MASS_6 = 0.9;
    private static final Double MASS_7 = 1.0;
  
    private static List<NetworkNode> nodes = new ArrayList<>();
    private static Map<String, Integer> inchiToID = new HashMap<>();
  
    private static final Integer RXN_1 = 100;
    private static final Integer RXN_2 = 102;
    private static final String PROJECTOR = "RO";
  
    @Before
    public void createMockNodes() {
      nodes.add(new NetworkNode(getMockMetabolite(METABOLITE_1, MASS_1)));
      nodes.add(new NetworkNode(getMockMetabolite(METABOLITE_2, MASS_2)));
      nodes.add(new NetworkNode(getMockMetabolite(METABOLITE_3, MASS_3)));
      nodes.add(new NetworkNode(getMockMetabolite(METABOLITE_4, MASS_4)));
      nodes.add(new NetworkNode(getMockMetabolite(METABOLITE_5, MASS_5)));
      nodes.add(new NetworkNode(getMockMetabolite(METABOLITE_6, MASS_6)));
      nodes.add(new NetworkNode(getMockMetabolite(METABOLITE_7, MASS_7)));
  
      nodes.forEach(n -> inchiToID.put(n.getMetabolite().getStructure().get().getInchi(), n.getUID()));
    }
  
    private Metabolite getMockMetabolite(String s, double mass) {
      MolecularStructure structure = Mockito.mock(MolecularStructure.class);
      Mockito.when(structure.getInchi()).thenReturn(s);
      Mockito.when(structure.getMonoIsotopicMass()).thenReturn(mass);
  
      Metabolite m = Mockito.mock(Metabolite.class);
      Mockito.when(m.getStructure()).thenReturn(Optional.of(structure));
      Mockito.when(m.getMonoIsotopicMass()).thenReturn(mass);
      return m;
    }
  
    @Test
    public void testAddNewEdge() {
      // Arrange
      MetabolismNetwork network = new MetabolismNetwork();
      nodes.forEach(network::addNode);
  
      // Act
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1), Arrays.asList(METABOLITE_2));
  
      // Assert
      assertEquals("Graph should have one edge", 1, network.getEdges().size());
      assertEquals("Product should have one in edge.", 1, network.getNodeByInchi(METABOLITE_2).getInEdges().size());
      assertEquals("Product should have no out edges.", 0, network.getNodeByInchi(METABOLITE_2).getOutEdges().size());
      assertEquals("Substrate should have one out edge.", 1, network.getNodeByInchi(METABOLITE_1).getOutEdges().size());
      assertEquals("Substrate should have no in edge.", 0, network.getNodeByInchi(METABOLITE_1).getInEdges().size());
  
      NetworkEdge edge = network.getEdges().iterator().next();
      assertEquals("Edge should have one substrate", 1, edge.getSubstrates().size());
      ;
      assertEquals("Edge's substrate should be METABOLITE_1", inchiToID.get(METABOLITE_1), edge.getSubstrates().get(0));
      assertEquals("Edge should have one product", 1, edge.getProducts().size());
      assertEquals("Edge's product should be METABOLITE_2", inchiToID.get(METABOLITE_2), edge.getProducts().get(0));
      assertTrue("Edge's reaction info should be empty", edge.getReactionIds().isEmpty());
      assertTrue("Edge's RO info should be empty", edge.getProjectorNames().isEmpty());
    }
  
    /**
     * Tests adding two edges with same substrates and products. Verifies that the edge is not duplicated,
     * but the reaction and projection info from the edges is merged.
     */
    @Test
    public void testAddNewRedundantEdge() {
      // Arrange
      MetabolismNetwork network = new MetabolismNetwork();
      nodes.forEach(network::addNode);
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1), Arrays.asList(METABOLITE_2)).addReactionId(RXN_1);
  
      // Act
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1), Arrays.asList(METABOLITE_2)).addReactionId(RXN_2).addProjectorName(PROJECTOR);
  
      // Assert
      assertEquals("Graph should have one edge", 1, network.getEdges().size());
      assertEquals("Product should have one in edge.", 1, network.getNodeByInchi(METABOLITE_2).getInEdges().size());
      assertEquals("Substrate should have one out edge.", 1, network.getNodeByInchi(METABOLITE_1).getOutEdges().size());
  
      NetworkEdge edge = network.getEdges().iterator().next();
      assertEquals("Edge should have one substrate", 1, edge.getSubstrates().size());
      assertEquals("Edge's substrate should be METABOLITE_1", inchiToID.get(METABOLITE_1), edge.getSubstrates().get(0));
      assertEquals("Edge should have one product", 1, edge.getProducts().size());
      assertEquals("Edge's product should be METABOLITE_2", inchiToID.get(METABOLITE_2), edge.getProducts().get(0));
      assertEquals("Edge's reaction info should contain 2 reactions", 2, edge.getReactionIds().size());
      assertTrue("Edge's reaction info should contain the right reactions",
          edge.getReactionIds().containsAll(Arrays.asList(RXN_1, RXN_2)));
      assertEquals("Edge's RO info should contain one RO", 1, edge.getProjectorNames().size());
      assertTrue("Edge's RO info should contain the right RO", edge.getProjectorNames().contains(PROJECTOR));
    }
  
    @Test
    public void testGetDerivatives() {
      // Arrange
      MetabolismNetwork network = new MetabolismNetwork();
      nodes.forEach(network::addNode);
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1), Arrays.asList(METABOLITE_2));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1), Arrays.asList(METABOLITE_3, METABOLITE_4));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_5), Arrays.asList(METABOLITE_3, METABOLITE_1));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_5), Arrays.asList(METABOLITE_3, METABOLITE_2));
  
      // Act
      List<NetworkNode> derivatives = network.getDerivatives(network.getNodeByInchi(METABOLITE_1));
  
      // Assert
      List<String> expectedInchis = Arrays.asList(METABOLITE_2, METABOLITE_3, METABOLITE_4);
      List<String> inchis = derivatives.stream().map(n -> n.getMetabolite().getStructure().get().getInchi()).collect(Collectors.toList());
      assertEquals("Should be 3 derivatives.", 3, inchis.size());
      assertTrue("Derivatives should be metabolites 2,3,4", inchis.containsAll(expectedInchis));
    }
  
    @Test
    public void testGetPrecursors() {
      // Arrange
      MetabolismNetwork network = new MetabolismNetwork();
      nodes.forEach(network::addNode);
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_2), Arrays.asList(METABOLITE_1));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_3, METABOLITE_4), Arrays.asList(METABOLITE_1));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1, METABOLITE_3), Arrays.asList(METABOLITE_5));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_2, METABOLITE_3), Arrays.asList(METABOLITE_5));
  
      // Act
      List<NetworkNode> precursors = network.getPrecursors(network.getNodeByInchi(METABOLITE_1));
  
      // Assert
      List<String> expectedInchis = Arrays.asList(METABOLITE_2, METABOLITE_3, METABOLITE_4);
      List<String> inchis = precursors.stream().map(n -> n.getMetabolite().getStructure().get().getInchi()).collect(Collectors.toList());
      assertEquals("Should be 3 precursors.", 3, inchis.size());
      assertTrue("Precursors should be metabolites 2,3,4", inchis.containsAll(expectedInchis));
    }
  
    /**
     * Test precursor report of 1 level.
     * Adds one relevant inedge, one irrelevant outedge of the precursor, and two level 2 precursors.
     * Test verifies that only the substrates of the first inedge are included in the subgraph.
     */
    @Test
    public void testPrecursorSubgraphN1() {
      // Arrange
      MetabolismNetwork network = new MetabolismNetwork();
      nodes.forEach(network::addNode);
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_3, METABOLITE_4), Arrays.asList(METABOLITE_5));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_3), Arrays.asList(METABOLITE_1, METABOLITE_2));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1), Arrays.asList(METABOLITE_3));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_2), Arrays.asList(METABOLITE_3));
      NetworkNode e = network.getNodeByInchi(METABOLITE_5);
  
      // Act
      PrecursorReport report = network.getPrecursorReport(e, 1);
  
      // Assert
      assertEquals("Report has correct target", e.getMetabolite(), report.getTarget());
  
      ImmutableNetwork precursorNetwork = report.getNetwork();
      assertEquals("Subgraph should contain three nodes", 3, precursorNetwork.getNodes().size());
      assertEquals("Subgraph should contain one edge", 1, precursorNetwork.getEdges().size());
  
      assertTrue("Subgraph should contain query node", precursorNetwork.getNodeOptionByInchi(METABOLITE_5).isPresent());
      assertTrue("Subgraph should contain first precursor", precursorNetwork.getNodeOptionByInchi(METABOLITE_3).isPresent());
      assertTrue("Subgraph should contain second precursor", precursorNetwork.getNodeOptionByInchi(METABOLITE_4).isPresent());
    }
  
    /**
     * Test precursor report of 2 levels.
     * Adds one relevant inedge, one irrelevant outedge of a precursor, and two level 2 precursors.
     * Test verifies that every metabolite except METABOLITE 6 is reported. Metabolite 6 should not be reported since
     * it is the derivative of a precursor of the target metabolite 5. Metabolites 1-5 are either direct percursors,
     * or precursors of precursors, and so they should all be reported. Metabolite 7 is a second product of an in-edge
     * of the target, so it should also be in the subgraph, but not the level map.
     */
    @Test
    public void testPrecursorSubgraphN2() {
      // Arrange
      MetabolismNetwork network = new MetabolismNetwork();
      nodes.forEach(network::addNode);
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_3, METABOLITE_4), Arrays.asList(METABOLITE_5, METABOLITE_7));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_3, METABOLITE_4), Arrays.asList(METABOLITE_6));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_1), Arrays.asList(METABOLITE_3));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_2), Arrays.asList(METABOLITE_3));
      NetworkNode e = network.getNodeByInchi(METABOLITE_5);
  
      // Act
      PrecursorReport report = network.getPrecursorReport(e, 2);
  
      // Assert
      ImmutableNetwork precursorNetwork = report.getNetwork();
      assertEquals("Subgraph should contain six nodes", 6, precursorNetwork.getNodes().size());
      assertEquals("Subgraph should contain three edges", 3, precursorNetwork.getEdges().size());
  
      assertTrue("Subgraph should contain query node", precursorNetwork.getNodeOptionByInchi(METABOLITE_5).isPresent());
      assertTrue("Subgraph should contain first n1 precursor", precursorNetwork.getNodeOptionByInchi(METABOLITE_3).isPresent());
      assertTrue("Subgraph should contain second n1 precursor", precursorNetwork.getNodeOptionByInchi(METABOLITE_4).isPresent());
      assertTrue("Subgraph should contain second product of target's in-edge1", precursorNetwork.getNodeOptionByInchi(METABOLITE_7).isPresent());
      assertTrue("Subgraph should contain second n2 precursor", precursorNetwork.getNodeOptionByInchi(METABOLITE_1).isPresent());
      assertTrue("Subgraph should contain second n2 precursor", precursorNetwork.getNodeOptionByInchi(METABOLITE_2).isPresent());
  
      assertEquals("Level of target is 0", 0, (int) report.getLevel(precursorNetwork.getNodeByInchi(METABOLITE_5)));
      assertEquals("Level of 1st precursor is 1", 1, (int) report.getLevel(precursorNetwork.getNodeByInchi(METABOLITE_3)));
      assertEquals("Level of 2nd precursor is 2", 2, (int) report.getLevel(precursorNetwork.getNodeByInchi(METABOLITE_1)));
      assertEquals("Level of non-precursor is null", null, report.getLevel(precursorNetwork.getNodeByInchi(METABOLITE_7)));
    }
  
  
    /**
     * Test precursor report of 2 levels to see how it handles cycles.  The key test is that the target, METABOLITE_5,
     * should have level 0, despite the fact that it is also its own second-level precursor.
     */
    @Test
    public void testPrecursorSubgraphCycles() {
      // Arrange
      MetabolismNetwork network = new MetabolismNetwork();
      nodes.forEach(network::addNode);
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_4), Arrays.asList(METABOLITE_5));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_3), Arrays.asList(METABOLITE_4));
      network.addEdgeFromInchis(Arrays.asList(METABOLITE_5), Arrays.asList(METABOLITE_4));
      NetworkNode e = network.getNodeByInchi(METABOLITE_5);
  
      // Act
      PrecursorReport report = network.getPrecursorReport(e, 2);
  
      // Assert
      ImmutableNetwork precursorNetwork = report.getNetwork();
      assertEquals("Level of target is 0", 0, (int) report.getLevel(precursorNetwork.getNodeByInchi(METABOLITE_5)));
      assertEquals("Level of 1st precursor is 1", 1, (int) report.getLevel(precursorNetwork.getNodeByInchi(METABOLITE_4)));
      assertEquals("Level of 2nd precursor is 2", 2, (int) report.getLevel(precursorNetwork.getNodeByInchi(METABOLITE_3)));
    }
  }
  