/*
 * Jalview - A Sequence Alignment Editor and Viewer (2.11.5.0)
 * Copyright (C) 2025 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.bin.argparser;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;

import org.apache.logging.log4j.util.Strings;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import jalview.bin.Cache;
import jalview.bin.Console;
import jalview.gui.Desktop;

@Test(singleThreaded = true)
public class ArgParserTest
{
  @AfterClass(alwaysRun = true)
  public static void resetProps()
  {
    Cache.loadProperties("test/jalview/testProps.jvprops");
  }

  @AfterMethod(alwaysRun = true)
  public void tearDown()
  {
    if (Desktop.instance != null)
      Desktop.instance.closeAll_actionPerformed(null);
  }

  @Test(groups = "Functional", dataProvider = "argLines")
  public void parseArgsTest(String commandLineArgs, Arg a, String other)
  {
    String[] args = commandLineArgs.split("\\s+");
    ArgParser argparser = new ArgParser(args);
  }

  @Test(groups = "Functional", dataProvider = "argSubValsAndLinkedIds")
  public void parseSubValsAndLinkedIdsTest(String commandLineArgs,
          String linkedId, Arg a, String subvalKey, String value,
          boolean trueOrFalse)
  {
    String[] args = commandLineArgs.split("\\s+");
    ArgParser argparser = new ArgParser(args);
    ArgValuesMap avm = argparser.getLinkedArgs(linkedId);
    ArgValue av = avm.getArgValue(a);
    SubVals sv = av.getSubVals();
    String testString = null;
    if (subvalKey.equals("GETINDEX"))
    {
      testString = String.valueOf(sv.getIndex());
    }
    else
    {
      testString = sv.get(subvalKey);
    }
    if (trueOrFalse)
    {
      Assert.assertEquals(testString, value);
    }
    else
    {
      Assert.assertNotEquals(testString, value);
    }
  }

  @Test(
    groups = "Functional",
    dataProvider = "argAutoIndexAndSubstitutions")
  public void parseAutoIndexAndSubstitutionsTest(String commandLineArgs,
          String linkedId, Arg a, String filename)
  {
    // { "--append=filename0 --new --append=filename1", "JALVIEW:1",
    // Arg.OPEN, "filename1" },
    String[] args = commandLineArgs.split("\\s+");
    ArgParser argparser = new ArgParser(args);
    ArgValuesMap avm = argparser.getLinkedArgs(linkedId);
    ArgValue av = avm.getArgValue(a);
    Assert.assertEquals(av.getValue(), filename);
  }

  @Test(groups = "Functional", dataProvider = "argLines")
  public void bootstrapArgsTest(String commandLineArgs, Arg a, String other)
  {
    String[] args = commandLineArgs.split("\\s+");
    BootstrapArgs b = BootstrapArgs.getBootstrapArgs(args);

    Assert.assertTrue(b.contains(a));
    if (a == Arg.PROPS)
    {
      Properties bP = Cache.bootstrapProperties(b.getValue(Arg.PROPS));
      Assert.assertNotNull(bP);
      Assert.assertTrue(other.equals(bP.get(Cache.BOOTSTRAP_TEST)));
      Assert.assertFalse(bP.contains("NOT" + Cache.BOOTSTRAP_TEST));
    }
    else if (a == Arg.ARGFILE)
    {
      List<String> filenames = b.getValueList(a);
      boolean found = false;
      for (String s : filenames)
      {
        File f = new File(s);
        File fo = new File(other);
        try
        {
          if (fo.getCanonicalPath().equals(f.getCanonicalPath()))
          {
            found = true;
            break;
          }
        } catch (IOException e)
        {
        }
      }
      Assert.assertTrue(found,
              "File '" + other + "' not found in shell expanded glob '"
                      + commandLineArgs + "'");
    }
  }

  @Test(groups = "Functional", dataProvider = "argFiles")
  public void argFilesTest(String commandLineArgs, Arg a, String other)
  {
    String[] args = commandLineArgs.split("\\s+");
    BootstrapArgs b = BootstrapArgs.getBootstrapArgs(args);

    Assert.assertTrue(b.contains(a));
    Assert.assertFalse(b.contains(Arg.APPEND));
    if (a == Arg.PROPS)
    {
      Properties bP = Cache.bootstrapProperties(b.getValue(Arg.PROPS));
      Assert.assertTrue("true".equals(bP.get(Cache.BOOTSTRAP_TEST)));
    }
  }

  @DataProvider(name = "argLinesNotworking")
  public Object[][] argLinesTest()
  {
    return new Object[][] {
        // can't use this one yet as it doesn't get shell glob expanded by the
        // test
        { "--argfile test/jalview/bin/argparser/testfiles/argfile*.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile0.txt" }, };
  }

  @DataProvider(name = "argLines")
  public Object[][] argLines()
  {
    return new Object[][] { {
        "--append=test/jalview/bin/argparser/testfiles/test1.fa --props=test/jalview/bin/argparser/testfiles/testProps.jvprops",
        Arg.PROPS, "true" },
        { "--debug --append=test/jalview/bin/argparser/testfiles/test1.fa",
            Arg.DEBUG, null },
        { "--append=test/jalview/bin/argparser/testfiles/test1.fa --headless",
            Arg.HEADLESS, null },

        { "--argfile test/jalview/bin/argparser/testfiles/argfile0.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile0.txt" },
        // these next three are what a shell glob expansion would look like
        { "--argfile test/jalview/bin/argparser/testfiles/argfile0.txt test/jalview/bin/argparser/testfiles/argfile1.txt test/jalview/bin/argparser/testfiles/argfile2.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile0.txt" },
        { "--argfile test/jalview/bin/argparser/testfiles/argfile0.txt test/jalview/bin/argparser/testfiles/argfile1.txt test/jalview/bin/argparser/testfiles/argfile2.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile1.txt" },
        { "--argfile test/jalview/bin/argparser/testfiles/argfile0.txt test/jalview/bin/argparser/testfiles/argfile1.txt test/jalview/bin/argparser/testfiles/argfile2.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile2.txt" },
        { "--argfile=test/jalview/bin/argparser/testfiles/argfile*.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile0.txt" },
        { "--argfile=test/jalview/bin/argparser/testfiles/argfile*.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile1.txt" },
        { "--argfile=test/jalview/bin/argparser/testfiles/argfile*.txt",
            Arg.ARGFILE,
            "test/jalview/bin/argparser/testfiles/argfile2.txt" } };
  }

  @DataProvider(name = "argSubValsAndLinkedIds")
  public Object[][] argSubValsAndLinkedIds()
  {
    return new Object[][] {
        //
        /*
         */
        { "--debug --append=[hi]test/jalview/bin/argparser/testfiles/test1.fa",
            "JALVIEW:0", Arg.APPEND, "hi", "true", true },
        { "--append[linkedId1]=[new,hello=world,1]test/jalview/bin/argparser/testfiles/test1.fa --headless",
            "linkedId1", Arg.APPEND, "new", "true", true },
        { "--append[linkedId2]=[new,hello=world,1]test/jalview/bin/argparser/testfiles/test1.fa --headless",
            "linkedId2", Arg.APPEND, "hello", "world", true },
        { "--append[linkedId3]=[new,hello=world,1]test/jalview/bin/argparser/testfiles/test1.fa --headless",
            "linkedId3", Arg.APPEND, "GETINDEX", "1", true },
        { "--append[linkedId4]=[new,hello=world,1]test/jalview/bin/argparser/testfiles/test1.fa --append[linkedId5]=[notnew;hello=world;1]test/jalview/bin/argparser/testfiles/test1.fa --headless",
            "linkedId5", Arg.APPEND, "new", "true", false },
        { "--append[linkedId5]=[new,hello=worlddomination,1]test/jalview/bin/argparser/testfiles/test1.fa --append[linkedId2]=[new;hello=world;1]test/jalview/bin/argparser/testfiles/test1.fa --headless",
            "linkedId5", Arg.APPEND, "hello", "world", false },
        { "--append[linkedId6]=[new,hello=world,0]test/jalview/bin/argparser/testfiles/test1.fa --append[linkedId7]=[new;hello=world;1]test/jalview/bin/argparser/testfiles/test1.fa --headless",
            "linkedId7", Arg.APPEND, "GETINDEX", "0", false },
        /*
         */
        //
    };
  }

  @DataProvider(name = "argAutoIndexAndSubstitutions")
  public Object[][] argAutoIndexAndSubstitutions()
  {
    return new Object[][] {
        //
        /*
         */
        { "--append=filename0 --append=filename1", "JALVIEW:0", Arg.APPEND,
            "filename0" },
        { "--append=filename0 --new --append=filename1", "JALVIEW:1",
            Arg.APPEND, "filename1" },
        { "--append=filename0 --new --new --append=filename2", "JALVIEW:0",
            Arg.APPEND, "filename0" },
        { "--append=filename0 --new --new --append=filename2", "JALVIEW:2",
            Arg.APPEND, "filename2" },
        { "--append[linkA-{n}]=filenameA0 --append[linkA-{++n}]=filenameA1",
            "linkA-0", Arg.APPEND, "filenameA0" },
        { "--append[linkB-{n}]=filenameB0 --append[linkB-{++n}]=filenameB1",
            "linkB-1", Arg.APPEND, "filenameB1" },
        { "--append[linkC-{n}]=filenameC0 --image[linkC-{n}]=outputC{n}.txt",
            "linkC-0", Arg.IMAGE, "outputC{n}.txt" },
        { "--append[linkD-{n}]=filenameD0 --substitutions --image[linkD-{n}]=outputD{n}.txt",
            "linkD-0", Arg.IMAGE, "outputD0.txt" },
        { "--append[linkE-{n}]=filenameE0 --substitutions --image[linkE-{n}]=output-E{n}.txt --nil[{++n}] --image[linkE-{n}]=outputE{n}.txt",
            "linkE-0", Arg.IMAGE, "output-E0.txt" },
        { "--append[linkF-{n}]=filenameF0 --substitutions --image[linkF-{n}]=output-F{n}.txt --nil[{++n}] --image[linkF-{n}]=outputF{n}.txt",
            "linkF-1", Arg.IMAGE, "outputF1.txt" },
        { "--append[linkG-{n}]=filenameG0 --substitutions --image[linkG-{n}]=output-G{n}.txt --nil[{++n}] --nosubstitutions --image[linkG-{n}]=outputG{n}.txt",
            "linkG-1", Arg.IMAGE, "outputG{n}.txt" },
        { "--append[linkH-{n}]=filenameH0 --substitutions --image[linkH-{n}]=output-H{n}.txt --nil[{++n}] --nosubstitutions --image[linkH-{n}]=outputH{n}.txt",
            "linkH-0", Arg.IMAGE, "output-H0.txt" },
        { "--open=filename0 --append=filename1", "JALVIEW:0", Arg.OPEN,
            "filename0" },
        { "--open=filename0 --new --append=filename1", "JALVIEW:1",
            Arg.APPEND, "filename1" },
        { "--open=filename0 --new --new --append=filename2", "JALVIEW:0",
            Arg.OPEN, "filename0" },
        { "--open=filename0 --new --new --append=filename2", "JALVIEW:2",
            Arg.APPEND, "filename2" },
        { "--open[linkA-{n}]=filenameA0 --append[linkA-{++n}]=filenameA1",
            "linkA-0", Arg.OPEN, "filenameA0" },
        { "--open[linkB-{n}]=filenameB0 --append[linkB-{++n}]=filenameB1",
            "linkB-1", Arg.APPEND, "filenameB1" },
        { "--open[linkC-{n}]=filenameC0 --image[linkC-{n}]=outputC{n}.txt",
            "linkC-0", Arg.IMAGE, "outputC{n}.txt" },
        { "--open[linkD-{n}]=filenameD0 --substitutions --image[linkD-{n}]=outputD{n}.txt",
            "linkD-0", Arg.IMAGE, "outputD0.txt" },
        { "--open[linkE-{n}]=filenameE0 --substitutions --image[linkE-{n}]=output-E{n}.txt --nil[{++n}] --image[linkE-{n}]=outputE{n}.txt",
            "linkE-0", Arg.IMAGE, "output-E0.txt" },
        { "--open[linkF-{n}]=filenameF0 --substitutions --image[linkF-{n}]=output-F{n}.txt --nil[{++n}] --image[linkF-{n}]=outputF{n}.txt",
            "linkF-1", Arg.IMAGE, "outputF1.txt" },
        { "--open[linkG-{n}]=filenameG0 --substitutions --image[linkG-{n}]=output-G{n}.txt --nil[{++n}] --nosubstitutions --image[linkG-{n}]=outputG{n}.txt",
            "linkG-1", Arg.IMAGE, "outputG{n}.txt" },
        { "--open[linkH-{n}]=filenameH0 --substitutions --image[linkH-{n}]=output-H{n}.txt --nil[{++n}] --nosubstitutions --image[linkH-{n}]=outputH{n}.txt",
            "linkH-0", Arg.IMAGE, "output-H0.txt" },
        /*
         */

        //
    };
  }

  @DataProvider(name = "argFiles")
  public Object[][] argFiles()
  {
    return new Object[][] { {
        "--argfile=test/jalview/bin/argparser/testfiles/argfile0.txt --open=shouldntbeabootstrap",
        Arg.ARGFILE, "test/jalview/bin/argfiles/testfiles/test1.fa" } };
  }

  @Test(groups = "Functional", dataProvider = "allLinkedIdsData")
  public void allLinkedIdsTest(String pwd, String commandLineArgs, Arg a,
          String[] values, String[] nonvalues)
  {
    String userDir = System.getProperty("user.dir");
    if (pwd != null)
    {
      File pwdFile = new File(pwd);
      System.setProperty("user.dir", pwdFile.getAbsolutePath());
    }
    String[] args = commandLineArgs.split("\\s+");
    ArgParser argparser = new ArgParser(args);

    int num = values.length;
    List<String> linkedIds = argparser.getLinkedIds();
    Assert.assertEquals(linkedIds.size(), num,
            "Wrong number of linkedIds: " + linkedIds.toString());
    for (int i = 0; i < num; i++)
    {
      String value = values[i];
      String linkedId = linkedIds.get(i);
      ArgValuesMap avm = argparser.getLinkedArgs(linkedId);
      if (value == null)
      {
        Assert.assertTrue(avm.containsArg(a),
                "Arg value for " + a.argString()
                        + " not applied correctly to linkedId '" + linkedId
                        + "'");
      }
      else
      {
        ArgValues avs = avm.getArgValues(a);
        ArgValue av = avs.getArgValue();
        String v = av.getValue();
        value = new File(value).getPath();
        Assert.assertEquals(v, value, "Arg value for " + a.argString()
                + " not applied correctly to linkedId '" + linkedId + "'");
      }
    }

    System.setProperty("user.dir", userDir);
  }

  @DataProvider(name = "allLinkedIdsData")
  public Object[][] allLinkedIdsData()
  {
    return new Object[][] {
        //
        /*
        */
        { null,
            "--open=test/jalview/bin/argparser/testfiles/*.fa --substitutions --all --image={dirname}/{basename}.png --close",
            Arg.CLOSE, new String[]
            { null, null, null },
            null },
        { null,
            "--open=test/jalview/bin/argparser/testfiles/*.fa --substitutions --all --output={dirname}/{basename}.stk --close",
            Arg.OUTPUT, new String[]
            { "test/jalview/bin/argparser/testfiles/test1.stk",
                "test/jalview/bin/argparser/testfiles/test2.stk",
                "test/jalview/bin/argparser/testfiles/test3.stk", },
            null },
        { null,
            "--open=test/jalview/bin/argparser/testfiles/*.fa --substitutions --all --image={dirname}/{basename}.png --close",
            Arg.IMAGE, new String[]
            { "test/jalview/bin/argparser/testfiles/test1.png",
                "test/jalview/bin/argparser/testfiles/test2.png",
                "test/jalview/bin/argparser/testfiles/test3.png", },
            null },
        /*
         * Find a way to change pwd reliably -- need to match "*.fa" against some files!
         { "test/jalview/bin/argparser/testfiles",
         
            "--open=*.fa --image={dirname}/{basename}.png --close",
            Arg.IMAGE, new String[]
            { "./test1.png", "./test2.png", "./test3.png", }, null },
            */
        //
    };
  }

  @Test(groups = "Functional", dataProvider = "bootstrapArgsData")
  public void bootstrapArgsValuesAndHeadlessModeTest(String commandLineArgs,
          Arg a, String valS, boolean valB, boolean headlessValue)
  {
    String[] args = commandLineArgs.split("\\s+");
    BootstrapArgs bsa = BootstrapArgs.getBootstrapArgs(args);
    if (a != null)
    {
      if (valS != null)
      {
        Assert.assertEquals(bsa.getValue(a), valS,
                "BootstrapArg " + a.argString()
                        + " value does not match expected '" + valS + "'");
      }
      else
      {
        Assert.assertEquals(bsa.getBoolean(a), valB,
                "Boolean/Unary value of BootstrapArg " + a.argString()
                        + "' is not the expected '" + valB + "'");
      }
    }

    boolean isHeadless = bsa.isHeadless();
    Assert.assertEquals(isHeadless, headlessValue,
            "Assumed headless setting '" + isHeadless + "' is wrong.");
  }

  @DataProvider(name = "bootstrapArgsData")
  public Object[][] bootstrapArgsData()
  {
    return new Object[][] {
        /*
         * cmdline args
         * Arg (null if only testing headless)
         * String value if there is one (null otherwise)
         * boolean value if String value is null
         * expected value of isHeadless()
         */
        /*
        */
        { "--open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
            Arg.JABAWS, "https://forwardsandbackwards.com/", false, true },
        { "--help-all --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
            Arg.HELP, null, true, true },
        { "--help-all --nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
            Arg.NEWS, null, false, true },
        { "--help --nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
            Arg.NEWS, null, false, true },
        { "--help-opening --nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
            Arg.NEWS, null, false, true },
        { "--nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
            Arg.NEWS, null, false, true },
        { "--open thisway.fa --image thatway.png", null, null, false,
            true },
        { "--open thisway.fa --output thatway.png", null, null, false,
            true },
        { "--open thisway.fa --image thatway.png --noheadless", null, null,
            false, false },
        { "--open thisway.fa --output thatway.png --noheadless", null, null,
            false, false },
        { "--open thisway.fa --image thatway.png --gui", null, null, false,
            false },
        { "--open thisway.fa --output thatway.png --gui", null, null, false,
            false },
        // --gui takes precedence
        { "--open thisway.fa --image thatway.png --gui --headless", null,
            null, false, false },
        { "--open thisway.fa --output thatway.png --gui --headless", null,
            null, false, false },
        //
    };
  }

  @Test(groups = "Functional", dataProvider = "argValuesData")
  public void checkArgValues(String commandLineArgs, ArgAndValues[] avals)
  {
    String linkedId = "JALVIEW:0";
    String[] args = commandLineArgs.split("\\s+");
    ArgParser argparser = new ArgParser(args);
    ArgValuesMap avm = argparser.getLinkedArgs(linkedId);

    // Now look at the argparser
    for (ArgAndValues aval : avals)
    {
      Arg primaryArg = aval.arg;

      List<ArgValue> parsed_ArgValues = avm.getArgValueList(primaryArg);

      Assert.assertTrue(
              areEqualSets(aval.values,
                      ArgValuesMap.toValues(parsed_ArgValues)),
              "Primary arg (" + primaryArg.toString()
                      + ") does not have the expected values. Expected "
                      + aval.values + " and got "
                      + ArgValuesMap.toValues(parsed_ArgValues));

      for (int i = 0; i < parsed_ArgValues.size(); i++)
      {
        ArgValue parsed_ArgValue = parsed_ArgValues.get(i);
        String value = aval.values.get(i);

        Console.debug(
                "- primary arg '" + primaryArg + "' = '" + value + "'");

        Assert.assertEquals(parsed_ArgValue.getValue(), value,
                "Primary arg value not as expected");
      }
    }
  }

  @DataProvider(name = "argValuesData")
  public Object[][] argValuesData()
  {
    return new Object[][] {
        /*
         * { cmdline args },
         * {
         *   { Primary Arg, Secondary Args },
         *  },
         */
        /*
        */
        { "--open=https://a.location/alignment.fa --structure=https://a.location/structure/file.cif",
            new ArgAndValues[]
            { av(Arg.OPEN, "https://a.location/alignment.fa"),
                av(Arg.STRUCTURE,
                        "https://a.location/structure/file.cif") } },
        { "--open=alignment.fa --structure=https://a.location/structure/file.cif",
            new ArgAndValues[]
            { av(Arg.OPEN, "alignment.fa"), av(Arg.STRUCTURE,
                    "https://a.location/structure/file.cif") } } };

  }

  @Test(groups = "Functional", dataProvider = "argsWithinTypesData")
  public void checkArgsWithinTypesTest(String commandLineArgs,
          Object[] stuff)
  {
    String linkedId = "JALVIEW:0";
    String[] args = commandLineArgs.split("\\s+");
    ArgParser argparser = new ArgParser(args);
    ArgValuesMap avm = argparser.getLinkedArgs(linkedId);

    ArgAndValues avals = (ArgAndValues) stuff[0];

    Object[] moreStuff = (Object[]) stuff[1];

    ArgAndValues[] subAv = (ArgAndValues[]) moreStuff[0];

    Object[] secondaryArgAndAuxStuff = (Object[]) stuff[2];

    // Now look at the argparser

    Arg primaryArg = avals.arg;

    List<ArgValue> parsed_ArgValues = avm.getArgValueList(primaryArg);

    Assert.assertTrue(
            areEqualSets(avals.values,
                    ArgValuesMap.toValues(parsed_ArgValues)),
            "Primary arg (" + primaryArg.toString()
                    + ") does not have the expected values. Expected "
                    + avals.values + " and got "
                    + ArgValuesMap.toValues(parsed_ArgValues));

    for (int i = 0; i < parsed_ArgValues.size(); i++)
    {
      ArgValue parsed_ArgValue = parsed_ArgValues.get(i);
      String value = avals.values.get(i);

      Console.debug("- primary arg '" + primaryArg + "' = '" + value + "'");

      Assert.assertEquals(parsed_ArgValue.getValue(), value,
              "Primary arg value not as expected");

      ArgAndValues[] aux_avals = (ArgAndValues[]) moreStuff[i];

      for (ArgAndValues aux_aval : aux_avals)
      {
        Arg auxArg = aux_aval.arg;
        List<String> auxValues = aux_aval.values;

        String parsed_auxValue = avm.getFromSubValArgOrPref(auxArg,
                ArgValuesMap.Position.AFTER, parsed_ArgValue, null, null,
                null, null);

        if (auxValues.isEmpty())
        {
          Assert.assertTrue(parsed_auxValue == null,
                  "Not expecting to parse a value for '" + auxArg
                          + "' but found '" + parsed_auxValue + "'");
        }

        for (String auxValue : auxValues)
        {

          Console.debug("- + primary aux arg '" + auxArg + "' = '"
                  + auxValue + "'");

          Assert.assertEquals(parsed_auxValue, auxValue,
                  "Primary auxiliary arg (" + auxArg.toString()
                          + ") values do not match");

        }
      }

      // Now for the secondary args
      Object[] secondaryStuff = (Object[]) secondaryArgAndAuxStuff[i];
      ArgAndValues secondaryArgAndValues = (ArgAndValues) secondaryStuff[0];
      Arg secondaryArg = secondaryArgAndValues.arg;
      List<String> secondaryValues = secondaryArgAndValues.values;

      List<ArgValue> parsed_secondaryArgValues = avm
              .getArgValueListFromSubValOrArg(parsed_ArgValue, secondaryArg,
                      null);

      Assert.assertTrue(
              areEqualSets(secondaryValues,
                      ArgValuesMap.toValues(parsed_secondaryArgValues)),
              "Secondary arg (" + secondaryArg.toString()
                      + ") does not have the expected values");

      Object[] secondaryMoreStuff = (Object[]) secondaryStuff[1];

      for (int j = 0; j < parsed_secondaryArgValues.size(); j++)
      {
        ArgValue parsed_secondaryArgValue = parsed_secondaryArgValues
                .get(j);
        String secondary_value = secondaryValues.get(j);

        Console.debug("-- secondary arg '" + secondaryArg + "' = '"
                + secondary_value + "'");

        Assert.assertEquals(parsed_secondaryArgValue.getValue(),
                secondary_value, "Secondary arg value not as expected");

        ArgAndValues[] secondary_aux_avals = (ArgAndValues[]) secondaryMoreStuff[j];

        for (ArgAndValues secondary_aux_aval : secondary_aux_avals)
        {
          Arg secondary_auxArg = secondary_aux_aval.arg;
          List<String> secondary_auxValues = secondary_aux_aval.values;

          String parsed_secondary_auxValue = avm.getValueFromSubValOrArg(
                  parsed_secondaryArgValue, secondary_auxArg, null);

          if (secondary_auxValues.isEmpty())
          {
            Assert.assertTrue(parsed_secondary_auxValue == null,
                    "Not expecting to parse a value for '"
                            + secondary_auxArg + "' but found '"
                            + parsed_secondary_auxValue + "'");
          }

          for (String secondary_auxValue : secondary_auxValues)
          {
            Console.debug("-- + secondary aux arg '" + secondary_auxArg
                    + "' for value '" + secondary_auxValue + "'");

            Assert.assertEquals(parsed_secondary_auxValue,
                    secondary_auxValue,
                    "Secondary auxiliary arg ("
                            + secondary_auxArg.toString()
                            + ") values do not match");
          }
        }
      }
    }
  }

  @DataProvider(name = "argsWithinTypesData")
  public Object[][] argsWithinTypesData()
  {
    return new Object[][] {
        /*
         * { cmdline args },
         * {
         *   { Primary Arg, Secondary Args },
         *   { { Secondary Arg => { Values } } }
         *  },
         */
        /*
        */

        { //
          // commandLineArgs
            "--open=alignment.fa --structure=structure0.pdb"
                    // structureimage0a and structureimage args
                    + " --structureimage=image0a.png --bgcolour=bg0a --imagecolour=col0a"
                    // structureimage0b and structureimage args
                    + " --structureimage=image0b.png --bgcolour=bg0b --imagecolour=col0b"
                    // more structure args
                    + " --structureviewer=sv0 --paematrix=pae0" //
                    // structure1
                    + " --structure=structure1.pdb"
                    // structure args
                    + " --structureviewer=sv1 --paematrix=pae1"
                    // structureimage1a with NO structureimage args
                    // (see `--all --bgcolour=pineapple` later)
                    + " --structureimage=image1a.png"
                    // structureimage1b and structureimage args
                    + " --structureimage=image1b.png --bgcolour=bg1b --imagecolour=col1b"
                    // --all args, should apply to structureimage1a only
                    + " --all --bgcolour=pineapple" //
            ,
            // stuff
            new Object[]
            {
                // avals (0) and (1)
                av(Arg.STRUCTURE, "structure0.pdb", "structure1.pdb"),
                // moreStuff (0) and (1)
                new ArgAndValues[][]
                { //
                    { av(Arg.STRUCTUREVIEWER, "sv0"),
                        av(Arg.PAEMATRIX, "pae0") },
                    { av(Arg.STRUCTUREVIEWER, "sv1"),
                        av(Arg.PAEMATRIX, "pae1") } //
                },
                // secondaryArgAndAuxStuff
                // (same size as values of avals)
                new Object[][]
                {
                    // secondaryStuff (0)
                    {
                        // secondaryArgAndValues (a) and (b)
                        av(Arg.STRUCTUREIMAGE, "image0a.png",
                                "image0b.png"),
                        // secondaryMoreStuff
                        // (same size as values of secondaryArgAndValues)
                        new ArgAndValues[][]
                        {
                            // secondary_aux_avals (a)
                            { av(Arg.BGCOLOUR, "bg0a"),
                                av(Arg.IMAGECOLOUR, "col0a") },
                            // secondary_aux_avals (b)
                            { av(Arg.BGCOLOUR, "bg0b"),
                                av(Arg.IMAGECOLOUR, "col0b") }, //
                        }, //
                    },
                    // secondaryStuff (1)
                    {
                        // secondaryArgAndValues (a) and (b)
                        av(Arg.STRUCTUREIMAGE, "image1a.png",
                                "image1b.png"),
                        // secondaryMoreStuff
                        new ArgAndValues[][]
                        {
                            // secondary_aux_avals (a)
                            { av(Arg.BGCOLOUR, "pineapple"),
                                av(Arg.IMAGECOLOUR) },
                            // secondary_aux_avals (b)
                            { av(Arg.BGCOLOUR, "bg1b"),
                                av(Arg.IMAGECOLOUR, "col1b") }, //
                        }, //
                    }, //
                } //
            } //
        }, //
        { //
            "--open=alignment.fa --wrap --colour=gecose-flower"
                    // structure0
                    + " --structure=structure0.pdb"
                    + " --structureimage=image0a.png --bgcolour=bg0a --scale=3"
                    + " --structureimage=image0b.png --imagecolour=col0b --scale=4"
                    + " --structureviewer=sv0 --paematrix=pae0" //
                    + " --structureimage=image0c.png"
                    // structure1
                    + " --structure=structure1.pdb"
                    + " --structureviewer=sv1 --paematrix=pae1"
                    + " --structureimage=image1a.png"
                    + " --structureimage=image1b.png --bgcolour=bg1b --imagecolour=col1b"
                    + " --structureimage=image1c.png --bgcolour=bg1c --imagecolour=col1c --scale=5"
                    + " --structureimage=image1d.png --imagecolour=col1d --scale=6"
                    + " --structureimage=image1e.png --bgcolour=bg1e"
                    // structure2
                    + " --structure=structure2.pdb"
                    + " --structureimage=image2a.png --bgcolour=bg2a --scale=23"
                    + " --all --bgcolour=pineapple --imagecolour=banana --scale=2" //
            ,
            // stuff
            new Object[]
            { av(Arg.STRUCTURE, "structure0.pdb", "structure1.pdb",
                    "structure2.pdb"),
                new ArgAndValues[][]
                { //
                    { av(Arg.STRUCTUREVIEWER, "sv0"),
                        av(Arg.PAEMATRIX, "pae0") },
                    { av(Arg.STRUCTUREVIEWER, "sv1"),
                        av(Arg.PAEMATRIX, "pae1") },
                    { av(Arg.STRUCTUREVIEWER), av(Arg.PAEMATRIX) } //
                }, //
                new Object[][]
                { //
                    { //
                        av(Arg.STRUCTUREIMAGE, "image0a.png", "image0b.png",
                                "image0c.png"), //
                        new ArgAndValues[][]
                        { //
                            { av(Arg.BGCOLOUR, "bg0a"),
                                av(Arg.IMAGECOLOUR, "banana"),
                                av(Arg.SCALE, "3") },
                            { av(Arg.BGCOLOUR, "pineapple"),
                                av(Arg.IMAGECOLOUR, "col0b"),
                                av(Arg.SCALE, "4") },
                            { av(Arg.BGCOLOUR, "pineapple"),
                                av(Arg.IMAGECOLOUR, "banana"),
                                av(Arg.SCALE, "2") }, //
                        }, //
                    }, //
                    { //
                        av(Arg.STRUCTUREIMAGE, "image1a.png", "image1b.png",
                                "image1c.png", "image1d.png",
                                "image1e.png"),
                        new ArgAndValues[][]
                        { //
                            { av(Arg.BGCOLOUR, "pineapple"),
                                av(Arg.IMAGECOLOUR, "banana"),
                                av(Arg.SCALE, "2") },
                            { av(Arg.BGCOLOUR, "bg1b"),
                                av(Arg.IMAGECOLOUR, "col1b"),
                                av(Arg.SCALE, "2") },
                            { av(Arg.BGCOLOUR, "bg1c"),
                                av(Arg.IMAGECOLOUR, "col1c"),
                                av(Arg.SCALE, "5") },
                            { av(Arg.BGCOLOUR, "pineapple"),
                                av(Arg.IMAGECOLOUR, "col1d"),
                                av(Arg.SCALE, "6") },
                            { av(Arg.BGCOLOUR, "bg1e"),
                                av(Arg.IMAGECOLOUR, "banana"),
                                av(Arg.SCALE, "2") } //
                        }, //
                    }, //
                    { //
                        av(Arg.STRUCTUREIMAGE, "image2a.png"),
                        new ArgAndValues[][]
                        { //
                            { av(Arg.BGCOLOUR, "bg2a"),
                                av(Arg.IMAGECOLOUR, "banana"),
                                av(Arg.SCALE, "23") }, //
                        }, //
                    }, //
                } //
            } //
        } //
    };
  }

  protected ArgAndValues av(Arg a, String... vals)
  {
    return new ArgAndValues(a, vals);
  }

  protected class ArgAndValues
  {
    protected Arg arg;

    protected List<String> values;

    ArgAndValues(Arg a, String... vals)
    {
      arg = a;
      values = vals == null ? new ArrayList<String>() : Arrays.asList(vals);
    }

    @Override
    public String toString()
    {
      StringBuilder sb = new StringBuilder();
      sb.append(arg.argString());
      sb.append("\n");
      sb.append(Strings.join(values, ','));
      sb.append("\n");
      return sb.toString();
    }
  }

  private static boolean areEqualSets(String[] strArray,
          List<String> strList)
  {
    return areEqualSets(Arrays.asList(strArray), strList);
  }

  private static boolean areEqualSets(List<String> l1, List<String> l2)
  {
    if (l1 == null && l2 == null)
    {
      Console.info(
              "Comparing null lists, should be okay but you might want to know");
      return true;
    }
    if (l1 == null || l2 == null)
    {
      return false;
    }
    return new HashSet<String>(l1).equals(new HashSet<String>(l2));
  }

}