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.reactionmerging;
  
  import act.server.NoSQLAPI;
  import act.shared.Reaction;
  import act.shared.Seq;
  import com.act.biointerpretation.test.util.TestUtils;
  import com.act.biointerpretation.test.util.MockedNoSQLAPI;
  import org.biopax.paxtools.model.level3.ConversionDirectionType;
  import org.biopax.paxtools.model.level3.StepDirection;
  import org.json.JSONArray;
  import org.json.JSONObject;
  import org.junit.After;
  import org.junit.Before;
  import org.junit.Test;
  import org.mockito.MockitoAnnotations;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.PriorityQueue;
  import java.util.Set;
  
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertFalse;
  import static org.junit.Assert.assertNotNull;
  import static org.junit.Assert.assertTrue;
  
  
  // TODO: consider using https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo instead of manual mocking?
  public class ReactionMergerTest {
  
    TestUtils utilsObject;
  
    @Before
    public void setUp() throws Exception {
      // In case we ever use Mockito annotations, don't forget to initialize them.
      MockitoAnnotations.initMocks(ReactionMergerTest.class);
      utilsObject = new TestUtils();
    }
  
    @After
    public void tearDown() throws Exception {
  
    }
  
    public static class ReactionHashingTestCase {
      String description;
      Integer expectedHashBucketCount;
      List<Reaction> reactions = new ArrayList<>();
      public ReactionHashingTestCase(String description, Integer expectedHashBucketCount) {
        this.description = description;
        this.expectedHashBucketCount = expectedHashBucketCount;
      }
  
      public ReactionHashingTestCase addRxn(Reaction rxn) {
        this.reactions.add(rxn);
        return this;
      }
  
      public Iterator<Reaction> rxnIterator() {
        return this.reactions.iterator();
      }
  
      public String getDescription() {
        return this.description;
      }
  
      public Integer getExpectedHashBucketCount() {
        return this.expectedHashBucketCount;
      }
  
      public ReactionHashingTestCase setSubstrateCoefficient(int reactionIdx, long chemId, int count) {
        this.reactions.get(reactionIdx).setSubstrateCoefficient(chemId, count);
        return this;
      }
  
      public ReactionHashingTestCase setProductCoefficient(int reactionIdx, long chemId, int count) {
        this.reactions.get(reactionIdx).setProductCoefficient(chemId, count);
        return this;
      }
    }
  
    @Test
    public void testHashing() throws Exception {
      ReactionHashingTestCase[] testCases = new ReactionHashingTestCase[] {
          new ReactionHashingTestCase("Rxns with same sub/prod and no cofactors/enzymes should be hashed to one bucket", 1).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Reactions with subset substrates should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L, 5L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Reactions with subset substreates should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L, 5L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxns with different subs. and no cofactors/enzymes should be hashed as two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{5L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxn with same sub/prod but different EC numbers should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.2", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxns with same sub/prod and different sub. cofactors should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[]{1L}, new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxns with same sub/prod and different prod. cofactors should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[]{1L}, new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxns with same sub/prod and different coenzymes should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[]{1L},
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[]{2L},
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxns with same sub/prod and different coefficients should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).setSubstrateCoefficient(0, 1, 1).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)).setSubstrateCoefficient(1, 1, 2),
  
          new ReactionHashingTestCase("Rxns with same sub/prod and different conv. directions should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.RIGHT_TO_LEFT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxns with same sub/prod and different step directions should be hashed to two buckets", 2).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.RIGHT_TO_LEFT, "Reaction 2",
                  Reaction.RxnDetailType.CONCRETE)),
  
          new ReactionHashingTestCase("Rxns with same sub/prod and different rxn detail types should be hashed to one bucket", 1).
              addRxn(new Reaction(
                  1L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 1",
                  Reaction.RxnDetailType.CONCRETE)).
              addRxn(new Reaction(
                  2L, new Long[]{1L, 2L}, new Long[]{3L, 4L}, new Long[0], new Long[0], new Long[0],
                  "1.1.1.1", ConversionDirectionType.LEFT_TO_RIGHT, StepDirection.LEFT_TO_RIGHT, "Reaction 2",
                  Reaction.RxnDetailType.ABSTRACT))
      };
  
      for (ReactionHashingTestCase testCase : testCases) {
        Map<ReactionMerger.SubstratesProducts, PriorityQueue<Long>> results =
            ReactionMerger.hashReactions(testCase.rxnIterator());
        assertEquals(testCase.getDescription(), testCase.getExpectedHashBucketCount(), Integer.valueOf(results.size()));
      }
    }
  
    /**
     * BRENDA and Metacyc structure their protein objects differently.  BRENDA specifies an "organism" field with a
     * single ID value, while Metacyc specifies a list of "organisms" ids.  Handling these incorrectly can cause
     * exceptions to be thrown when running the reaction merger.
     * @throws Exception
     */
    @Test
    public void testOrganismMigrationForDifferentProteinTypes() throws Exception {
      List<Reaction> testReactions = new ArrayList<>();
      // BRENDA style
      testReactions.add(utilsObject.makeTestReaction(new Long[]{1L, 2L, 3L}, new Long[]{4L, 5L, 6L}, false));
      // Metacyc style
      testReactions.add(utilsObject.makeTestReaction(new Long[]{4L, 5L, 6L}, new Long[]{7L, 8L, 9L}, true));
  
      MockedNoSQLAPI mockAPI = new MockedNoSQLAPI();
      mockAPI.installMocks(testReactions, utilsObject.SEQUENCES, utilsObject.ORGANISM_NAMES, new HashMap<>());
  
      NoSQLAPI mockNoSQLAPI = mockAPI.getMockNoSQLAPI();
  
  
      ReactionMerger merger = new ReactionMerger(mockNoSQLAPI);
      merger.init();
      merger.run();
  
      assertEquals("Reactions should not have been merged", 2, mockAPI.getWrittenReactions().size());
  
      Reaction writtenReaction1 = mockAPI.getWrittenReactions().get(0);
      assertEquals("Reaction 1 has one protein", 1, writtenReaction1.getProteinData().size());
      JSONObject reaction1Protein = writtenReaction1.getProteinData().iterator().next();
      assertTrue("Reaction 1's protein has an organism field", reaction1Protein.has("organism"));
      assertFalse("Reaction 1's protein does not have an organisms field", reaction1Protein.has("organisms"));
      assertTrue("Reaction 2's protein's organisms is a Long", reaction1Protein.get("organism") instanceof Long);
      assertEquals("Reaction 1's protein has the correct single organism id", 1L, reaction1Protein.getLong("organism"));
  
      Reaction writtenReaction2 = mockAPI.getWrittenReactions().get(1);
      assertEquals("Reaction 2 has one protein", 1, writtenReaction2.getProteinData().size());
      JSONObject reaction2Protein = writtenReaction2.getProteinData().iterator().next();
      assertFalse("Reaction 2's protein does not have an organism field", reaction2Protein.has("organism"));
      assertTrue("Reaction 2's protein has an organisms field", reaction2Protein.has("organisms"));
      assertTrue("Reaction 2's protein's organisms is a JSONArray",
          reaction2Protein.get("organisms") instanceof JSONArray);
      JSONArray reaction2ProteinOrganisms = reaction2Protein.getJSONArray("organisms");
      assertEquals("Reaction 2's protein's organisms has one entry", 1L, reaction2ProteinOrganisms.length());
      assertTrue("Reaction 2's protein's organisms' first entry is a long",
          reaction2ProteinOrganisms.get(0) instanceof Long);
      assertEquals("Reaction 2's protein's organisms' first entry is unchanged from the original",
          1L, reaction2ProteinOrganisms.getLong(0));
    }
  
    /**
     * This large and sprawling test verifies that reactions are merged as expected based on their substrates and
     * products.  It's volume is primarily due to the complicated context the ReactionMerger requires, both in terms
     * of the data it consumes (reactions, chemicals, sequences) and the fact that it relies on the NoSQLAPI class to
     * iterate over reactions and store the merged results.
     * @throws Exception
     */
    @Test
    public void testMergingEndToEnd() throws Exception {
      List<Reaction> testReactions = new ArrayList<>();
      // Group 1
      testReactions.add(utilsObject.makeTestReaction(new Long[]{1L, 2L, 3L}, new Long[]{4L, 5L, 6L}));
      testReactions.add(utilsObject.makeTestReaction(new Long[]{1L, 2L, 3L}, new Long[]{4L, 5L, 6L}));
      testReactions.add(utilsObject.makeTestReaction(new Long[]{1L, 2L, 3L}, new Long[]{4L, 5L, 6L}));
      // Group 2
      testReactions.add(utilsObject.makeTestReaction(new Long[]{7L, 2L, 3L}, new Long[]{8L, 5L, 6L}));
      testReactions.add(utilsObject.makeTestReaction(new Long[]{7L, 2L, 3L}, new Long[]{8L, 5L, 6L}));
      // Group 3
      testReactions.add(utilsObject.makeTestReaction(new Long[]{9L, 2L, 3L}, new Long[]{8L, 5L, 6L}));
  
      MockedNoSQLAPI mockAPI = new MockedNoSQLAPI();
      mockAPI.installMocks(testReactions, utilsObject.SEQUENCES, utilsObject.ORGANISM_NAMES, new HashMap<>());
  
      NoSQLAPI mockNoSQLAPI = mockAPI.getMockNoSQLAPI();
  
      /* ****************************************
       * Create a reaction merger and run it on the mocked objects. */
  
      ReactionMerger merger = new ReactionMerger(mockNoSQLAPI);
      merger.init();
      merger.run();
  
      // Test the results of the merge.
      assertEquals("Input reactions should be merged into three output reactions",
          3, mockAPI.getWrittenReactions().size());
  
      // Check merged structure of first three reactions.
      // TODO: we might be able to do this faster by creating an expected reaction and doing a deep comparison.
  
      /* Beware: sloppy, repetative test code.  I'll let this sort of thing slide in tests that aren't likely to be
       * reused elsewhere, but it's definitely not pretty. */
      Reaction r1 = mockAPI.getWrittenReactions().get(0);
      assertNotNull("Merged reaction 1 should not be null", r1);
  
      assertEquals("Merged reaction 1 has expected substrates",
          mockAPI.readDBChemicalIdsToInchis(new Long[] {1L, 2L, 3L}),
          mockAPI.writeDBChemicalIdsToInchis(r1.getSubstrates())
      );
      assertEquals("Merged reaction 1 has expected products",
          mockAPI.readDBChemicalIdsToInchis(new Long[] {4L, 5L, 6L}),
          mockAPI.writeDBChemicalIdsToInchis(r1.getProducts())
      );
  
      assertEquals("Merged reaction 1 should have 3 protein objects", 3, r1.getProteinData().size());
      Set<String> r1Sequences = new HashSet<>(3);
      for (JSONObject o : r1.getProteinData()) {
        Long id = o.getLong("id");
        JSONArray sequences = o.getJSONArray("sequences");
        assertNotNull("Sequences for protein %d should not be null", id);
        assertEquals(String.format("Protein %d should have one sequence", id),
            1, sequences.length());
        Seq seq = mockAPI.getWrittenSequences().get(sequences.getLong(0));
        assertNotNull("Referenced seq object should not be null", seq);
        assertEquals("New sequence object's sequence string should match original",
            utilsObject.SEQ_MAP.get(id * 10L).getSequence(), seq.getSequence());
        r1Sequences.add(seq.getSequence());
        assertEquals("New Seq object should reference the migrated reaction by id",
            Long.valueOf(r1.getUUID()), seq.getReactionsCatalyzed().iterator().next());
      }
      assertEquals("All expected sequences are accounted for",
          new HashSet<>(Arrays.asList("SEQA", "SEQB", "SEQC")),
          r1Sequences
      );
  
  
      Reaction r2 = mockAPI.getWrittenReactions().get(1);
      assertNotNull("Merged reaction 2 should not be null", r2);
      assertEquals("Merged reaction 2 has expected substrates",
          mockAPI.readDBChemicalIdsToInchis(new Long[] {7L, 2L, 3L}),
          mockAPI.writeDBChemicalIdsToInchis(r2.getSubstrates())
      );
      assertEquals("Merged reaction 2 has expected products",
          mockAPI.readDBChemicalIdsToInchis(new Long[] {8L, 5L, 6L}),
          mockAPI.writeDBChemicalIdsToInchis(r2.getProducts())
      );
  
      assertEquals("Merged reaction 2 should have 2 protein objects", 2, r2.getProteinData().size());
      Set<String> r2Sequences = new HashSet<>(2);
      for (JSONObject o : r2.getProteinData()) {
        Long id = o.getLong("id");
        JSONArray sequences = o.getJSONArray("sequences");
        assertNotNull("Sequences for protein %d should not be null", id);
        assertEquals(String.format("Protein %d should have one sequence", id),
            1, sequences.length());
        Seq seq = mockAPI.getWrittenSequences().get(sequences.getLong(0));
        assertNotNull("Referenced seq object should not be null", seq);
        assertEquals("New sequence object's sequence string should match original",
            utilsObject.SEQ_MAP.get(id * 10L).getSequence(), seq.getSequence());
        r2Sequences.add(seq.getSequence());
        assertEquals("New Seq object should reference the migrated reaction by id",
            Long.valueOf(r2.getUUID()), seq.getReactionsCatalyzed().iterator().next());
      }
      assertEquals("All expected sequences are accounted for",
          new HashSet<>(Arrays.asList("SEQD", "SEQE")),
          r2Sequences
      );
  
  
      Reaction r3 = mockAPI.getWrittenReactions().get(2);
      assertNotNull("Merged reaction 3 should not be null", r3);
      assertEquals("Merged reaction 3 has expected substrates",
          mockAPI.readDBChemicalIdsToInchis(new Long[] {9L, 2L, 3L}),
          mockAPI.writeDBChemicalIdsToInchis(r3.getSubstrates())
      );
      assertEquals("Merged reaction 3 has expected products",
          mockAPI.readDBChemicalIdsToInchis(new Long[] {8L, 5L, 6L}),
          mockAPI.writeDBChemicalIdsToInchis(r3.getProducts())
      );
  
      assertEquals("Merged reaction 3 should have 1 protein objects", 1, r3.getProteinData().size());
      Set<String> r3Sequences = new HashSet<>(1);
      for (JSONObject o : r3.getProteinData()) {
        Long id = o.getLong("id");
        JSONArray sequences = o.getJSONArray("sequences");
        assertNotNull("Sequences for protein %d should not be null", id);
        assertEquals(String.format("Protein %d should have one sequence", id),
            1, sequences.length());
        Seq seq = mockAPI.getWrittenSequences().get(sequences.getLong(0));
        assertNotNull("Referenced seq object should not be null", seq);
        assertEquals("New sequence object's sequence string should match original",
            utilsObject.SEQ_MAP.get(id * 10L).getSequence(), seq.getSequence());
        r3Sequences.add(seq.getSequence());
        assertEquals("New Seq object should reference the migrated reaction by id",
            Long.valueOf(r3.getUUID()), seq.getReactionsCatalyzed().iterator().next());
      }
      assertEquals("All expected sequences are accounted for",
          new HashSet<>(Arrays.asList("SEQF")),
          r3Sequences
      );
    }
  
    @Test
    public void testCoefficientsAreCorrectlyTransferred() throws Exception {
      List<Reaction> testReactions = new ArrayList<>();
  
      Long[] substrates = {1L, 2L, 3L};
      Long[] products = {4L, 5L, 6L};
      Integer[] substrateCoefficients = {1, 2, 3};
      Integer[] productCoefficients = {2, 3, 1};
  
      // Group 1
      testReactions.add(utilsObject.makeTestReaction(substrates, products, substrateCoefficients, productCoefficients, false));
      testReactions.add(utilsObject.makeTestReaction(substrates, products, substrateCoefficients, productCoefficients, false));
  
      for (int i = 0; i < substrates.length; i++) {
        assertEquals(String.format("Input reaction substrate %d has correct coefficient set", substrates[i]),
            substrateCoefficients[i], testReactions.get(0).getSubstrateCoefficient(substrates[i]));
      }
      for (int i = 0; i < products.length; i++) {
        assertEquals(String.format("Input reaction product %d has correct coefficient set", products[i]),
            productCoefficients[i], testReactions.get(0).getProductCoefficient(products[i]));
      }
  
  
      MockedNoSQLAPI mockAPI = new MockedNoSQLAPI();
      mockAPI.installMocks(testReactions, utilsObject.SEQUENCES, utilsObject.ORGANISM_NAMES, new HashMap<>());
  
      NoSQLAPI mockNoSQLAPI = mockAPI.getMockNoSQLAPI();
  
      /* **************************************** */
      ReactionMerger merger = new ReactionMerger(mockNoSQLAPI);
      merger.init();
      merger.run();
  
      assertEquals("Input reactions should be merged into one output reactions",
          1, mockAPI.getWrittenReactions().size());
      Reaction rxn = mockAPI.getWrittenReactions().get(0);
  
      /* We don't necessarily know what ids the products/substrates will get (we can guess, but it's just a guess), but we
       * do know for certain that they'll be inserted in the same order they appear in the original reaction.  By sorting
       * the substrate/product ids, we can iterate over them in the same order they would have appeared originally, which
       * allows us to compare each new reaction's coefficient against the similarly positioned coefficient from the
       * old reaction. */
  
      // Copy the substrates/products into new arrays before we modify them.
      List<Long> writtenSubstrates = new ArrayList<>(Arrays.asList(rxn.getSubstrates()));
      List<Long> writtenProducts = new ArrayList<>(Arrays.asList(rxn.getProducts()));
  
      Collections.sort(writtenSubstrates);
      Collections.sort(writtenProducts);
  
      for (int i = 0; i < writtenSubstrates.size(); i++) {
        Integer newCoefficient = rxn.getSubstrateCoefficient(writtenSubstrates.get(i));
        assertNotNull(String.format("Output reaction substrate coefficient for chemical %d is not null", i),
            newCoefficient);
        assertEquals(String.format("Coefficient for output chemical %d matches original", i),
            substrateCoefficients[i], newCoefficient);
      }
  
      for (int i = 0; i < writtenProducts.size(); i++) {
        Integer newCoefficient = rxn.getProductCoefficient(writtenProducts.get(i));
        assertNotNull(String.format("Output reaction product coefficient for chemical %d is not null", i),
            newCoefficient);
        assertEquals(String.format("Coefficient for output chemical %d matches original", i),
            productCoefficients[i], newCoefficient);
      }
    }
  }
  