#include "argparse.hpp"
#include "argparse_util.hpp"

using argparse::ArgValue;
using argparse::ConvertedValue;

#define TEST

struct Args {
    ArgValue<std::string> architecture_file;
    ArgValue<std::string> circuit;

    ArgValue<bool> disp;
    ArgValue<bool> auto_value;

    ArgValue<bool> pack;
    ArgValue<bool> place;
    ArgValue<bool> route;

    ArgValue<bool> show_help;
    ArgValue<bool> timing_analysis;
    ArgValue<const char*> slack_definition;
    ArgValue<bool> echo_files;
    ArgValue<bool> verify_file_digests;
    ArgValue<unsigned> num_workers;

    ArgValue<std::string> blif_file;
    ArgValue<std::string> net_file;
    ArgValue<std::string> place_file;
    ArgValue<std::string> route_file;
    ArgValue<std::string> sdc_file;
    ArgValue<std::string> outfile_prefix;

    ArgValue<bool> absorb_buffer_luts;
    ArgValue<bool> sweep_dangling_primary_ios;
    ArgValue<bool> sweep_dangling_nets;
    ArgValue<bool> sweep_dangling_blocks;
    ArgValue<bool> sweep_constant_primary_outputs;

    ArgValue<bool> connection_driven_clustering;
    ArgValue<bool> allow_unrelated_clustering;
    ArgValue<float> alpha_clustering;
    ArgValue<float> beta_clustering;
    ArgValue<bool> timing_driven_clustering;
    ArgValue<std::string> cluster_seed_type;

    ArgValue<size_t> seed;
    ArgValue<bool> enable_timing_computations;
    ArgValue<float> inner_num;
    ArgValue<float> init_t;
    ArgValue<float> exit_t;
    ArgValue<float> alpha_t;
    ArgValue<std::string> fix_pins;
    ArgValue<std::string> place_algorithm;
    ArgValue<size_t> place_chan_width;

    ArgValue<float> timing_tradeoff;
    ArgValue<int> recompute_crit_iter;
    ArgValue<int> inner_loop_recompute_divider;
    ArgValue<float> td_place_exp_first;
    ArgValue<float> td_place_exp_last;

    ArgValue<int> max_router_iterations;
    ArgValue<float> first_iter_pres_fac;
    ArgValue<float> initial_pres_fac;
    ArgValue<float> pres_fac_mult;
    ArgValue<float> acc_fac;
    ArgValue<int> bb_factor;
    ArgValue<std::string> base_cost_type;
    ArgValue<float> bend_cost;
    ArgValue<std::string> route_type;
    ArgValue<size_t> route_chan_width;
    ArgValue<size_t> min_route_chan_width_hint;
    ArgValue<bool> verify_binary_search;
    ArgValue<std::string> router_algorithm;
    ArgValue<int> min_incremental_reroute_fanout;

    ArgValue<float> astar_fac;
    ArgValue<float> max_criticality;
    ArgValue<float> criticality_exp;
    ArgValue<std::string> routing_failure_predictor;

    ArgValue<bool> power;
    ArgValue<std::string> tech_properties_file;
    ArgValue<std::string> activity_file;

    ArgValue<bool> full_stats;
    ArgValue<bool> gen_post_synthesis_netlist;

    ArgValue<std::vector<float>> one_or_more;
    ArgValue<std::vector<float>> zero_or_more;
};

bool expect_pass(argparse::ArgumentParser& parser, const std::vector<std::string>& cmd_line);
bool expect_fail(argparse::ArgumentParser& parser, const std::vector<std::string>& cmd_line);

struct OnOff {
    ConvertedValue<bool> from_str(const std::string& str) {
        ConvertedValue<bool> converted_value;

        if      (str == "on")  converted_value.set_value(true);
        else if (str == "off") converted_value.set_value(false);
        else                   converted_value.set_error("Invalid argument value");
        return converted_value;
    }

    ConvertedValue<std::string> to_str(bool val) {
        ConvertedValue<std::string> converted_value;
        if (val) converted_value.set_value("on");
        else     converted_value.set_value("off");
        return converted_value;
    }

    std::vector<std::string> default_choices() {
        return {"on", "off"};
    }
};

int main(
        int 
#ifndef TEST
        argc
#endif
         , const char** argv) {
    Args args;

    auto parser = argparse::ArgumentParser(argv[0], "Test parser for libargparse");
    parser.epilog("This is the epilog");

    auto& pos_grp = parser.add_argument_group("positional arguments");
    pos_grp.add_argument(args.architecture_file, "architecture")
            .help("FPGA Architecture description file (XML)");
    pos_grp.add_argument(args.circuit, "circuit")
            .help("Circuit file (or circuit name if --blif_file specified)");

    auto& stage_grp = parser.add_argument_group("stage options");
    stage_grp.add_argument<bool,OnOff>(args.pack, "--pack")
            .help("Run packing")
            .action(argparse::Action::STORE_TRUE)
            .default_value("off");
    stage_grp.add_argument<bool,OnOff>(args.place, "--place")
            .help("Run placement")
            .action(argparse::Action::STORE_TRUE)
            .default_value("off");
    stage_grp.add_argument<bool,OnOff>(args.route, "--route")
            .help("Run routing")
            .action(argparse::Action::STORE_TRUE)
            .default_value("off");
    stage_grp.add_argument<bool,OnOff>(args.route, "--analysis")
            .help("Run analysis")
            .action(argparse::Action::STORE_TRUE)
            .required(true)
            .default_value("off");

    stage_grp.epilog("If none of the stage options are specified, all stages are run.\n"
                     "Analysis is always run after routing.");

    auto& gfx_grp = parser.add_argument_group("graphics options");
    gfx_grp.add_argument<bool,OnOff>(args.disp, "--disp")
            .help("Enable or disable interactive graphics")
            .default_value("off");
    gfx_grp.add_argument(args.auto_value, "--auto")
            .help("Controls how often VPR pauses for interactive"
                  " graphics (requiring Proceed to be clicked)."
                  " Higher values pause less frequently")
            .default_value("1")
            .choices({"0", "1", "2"})
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& gen_grp = parser.add_argument_group("general options");
    gen_grp.add_argument(args.show_help, "--help", "-h")
            .help("Show this help message then exit")
            .action(argparse::Action::HELP);
    gen_grp.add_argument<bool,OnOff>(args.timing_analysis, "--timing_analysis")
            .help("Controls whether timing analysis (and timing driven optimizations) are enabled.")
            .default_value("on") ;
    gen_grp.add_argument(args.slack_definition, "--slack_definition")
            .help("Sets the slack definition used by the classic timing analyyzer")
            .default_value("R")
            .choices({"R", "I", "S", "G", "C", "N"})
            .show_in(argparse::ShowIn::HELP_ONLY);
    gen_grp.add_argument<bool,OnOff>(args.echo_files, "--echo_file")
            .help("Generate echo files of key internal data structures."
                  " Useful for debugging VPR, and typically end in .echo")
            .default_value("off")
            .show_in(argparse::ShowIn::HELP_ONLY);
    gen_grp.add_argument<bool,OnOff>(args.verify_file_digests, "--verify_file_digests")
            .help("Verify that files loaded by VPR (e.g. architecture, netlist,"
                  " previous packing/placement/routing) are consistent")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    gen_grp.add_argument(args.num_workers, "--num_workers", "-j")
            .help("Number of parallel workers")
            .default_value("1")
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& file_grp = parser.add_argument_group("filename options");
    file_grp.add_argument(args.blif_file, "--blif_file")
            .help("Path to technology mapped circuit in BLIF format")
            .show_in(argparse::ShowIn::HELP_ONLY);
    file_grp.add_argument(args.net_file, "--net_file")
            .help("Path to packed netlist file")
            .show_in(argparse::ShowIn::HELP_ONLY);
    file_grp.add_argument(args.place_file, "--place_file")
            .help("Path to placement file")
            .show_in(argparse::ShowIn::HELP_ONLY);
    file_grp.add_argument(args.route_file, "--route_file")
            .help("Path to routing file")
            .show_in(argparse::ShowIn::HELP_ONLY);
    file_grp.add_argument(args.sdc_file, "--sdc_file")
            .help("Path to timing constraints file in SDC format")
            .show_in(argparse::ShowIn::HELP_ONLY);
    file_grp.add_argument(args.outfile_prefix, "--outfile_prefix")
            .help("Prefix for output files")
            .show_in(argparse::ShowIn::HELP_ONLY);


    auto& netlist_grp = parser.add_argument_group("netlist options");
    netlist_grp.add_argument<bool,OnOff>(args.absorb_buffer_luts, "--absorb_buffer_luts")
            .help("Controls whether LUTS programmed as buffers are absorbed by downstream logic")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    netlist_grp.add_argument<bool,OnOff>(args.sweep_dangling_primary_ios, "--sweep_dangling_primary_ios")
            .help("Controls whether dangling primary inputs and outputs are removed from the netlist")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    netlist_grp.add_argument<bool,OnOff>(args.sweep_dangling_nets, "--sweep_dangling_nets")
            .help("Controls whether dangling nets are removed from the netlist")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    netlist_grp.add_argument<bool,OnOff>(args.sweep_dangling_blocks, "--sweep_dangling_blocks")
            .help("Controls whether dangling blocks are removed from the netlist")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    netlist_grp.add_argument<bool,OnOff>(args.sweep_constant_primary_outputs, "--sweep_constant_primary_outputs")
            .help("Controls whether primary outputs driven by constant values are removed from the netlist")
            .default_value("off")
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& pack_grp = parser.add_argument_group("packing options");
    pack_grp.add_argument<bool,OnOff>(args.connection_driven_clustering, "--connection_driven_clustering")
            .help("Controls whether or not packing prioritizes the absorption of nets with fewer"
                  " connections into a complex logic block over nets with more connections")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    pack_grp.add_argument<bool,OnOff>(args.allow_unrelated_clustering, "--allow_unrelated_clustering")
            .help("Controls whether or not primitives with no attraction to the current cluster"
                  " can be packed into it")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    pack_grp.add_argument(args.alpha_clustering, "--alpha_clustering")
            .help("Parameter that weights the optimization of timing vs area. 0.0 focuses solely on"
                  " area, 1.0 solely on timing.")
            .default_value("0.75")
            .show_in(argparse::ShowIn::HELP_ONLY);
    pack_grp.add_argument(args.beta_clustering, "--beta_clustering")
            .help("Parameter that weights the absorption of small nets vs signal sharing."
                  " 0.0 focuses solely on sharing, 1.0 solely on small net absoprtion."
                  " Only meaningful if --connection_driven_clustering=on")
            .default_value("0.9")
            .show_in(argparse::ShowIn::HELP_ONLY);
    pack_grp.add_argument<bool,OnOff>(args.timing_driven_clustering, "--timing_driven_clustering")
            .help("Controls whether custering optimizes for timing")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    pack_grp.add_argument(args.cluster_seed_type, "--cluster_seed_type")
            .help("Controls how primitives are chosen as seeds."
                  " (Default: blend if timing driven, max_inputs otherwise)")
            .choices({"blend", "timing", "max_inputs"})
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& place_grp = parser.add_argument_group("placement options");
    place_grp.add_argument(args.seed, "--seed")
            .help("Placement random number generator seed")
            .default_value("1")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument<bool,OnOff>(args.enable_timing_computations, "--enable_timing_computations")
            .help("Displays delay statistics even if placement is not timing driven")
            .default_value("on")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument(args.inner_num, "--inner_num")
            .help("Controls number of moves per temperature: inner_num * num_blocks ^ (4/3)")
            .default_value("10.0")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument(args.init_t, "--init_t")
            .help("Initial temperature for manual annealing schedule")
            .default_value("100.0")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument(args.exit_t, "--exit_t")
            .help("Temperature at which annealing which terminate for manual annealing schedule")
            .default_value("0.01")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument(args.alpha_t, "--alpha_t")
            .help("Temperature scaling factor for manual annealing schedule."
                  " Old temperature is multiplied by alpha_t")
            .default_value("0.01")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument(args.fix_pins, "--fix_pins")
            .help("Fixes I/O pad locations during placement."
                  " Can be 'random' for a random initial assignment,"
                  " 'off' to allow the place to optimize pad locations,"
                  " or a file specifying the pad locations.")
            .default_value("off")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument(args.place_algorithm, "--place_algorithm")
            .help("Controls which placement algorithm is used")
            .default_value("path_timing_driven")
            .choices({"bounding_box", "path_timing_driven"})
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_grp.add_argument(args.place_chan_width, "--place_chan_width")
            .help("Sets the assumed channel width during placement")
            .default_value("100")
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& place_timing_grp = parser.add_argument_group("timing-driven placement options");
    place_timing_grp.add_argument(args.timing_tradeoff, "--timing_tradeoff")
            .help("Trade-off control between delay and wirelength during placement."
                  " 0.0 focuses completely on wirelength, 1.0 completely on timing")
            .default_value("0.5")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_timing_grp.add_argument(args.recompute_crit_iter, "--recompute_crit_iter")
            .help("Controls how many temperature updates occur between timing analysis during placement")
            .default_value("1")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_timing_grp.add_argument(args.inner_loop_recompute_divider, "--inner_loop_recompute_divider")
            .help("Controls how many timing analysies are perform per temperature during placement")
            .default_value("0")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_timing_grp.add_argument(args.td_place_exp_first, "--td_place_exp_first")
            .help("Controls how critical a connection is as a function of slack at the start of placement."
                  " A value of zero treats all connections as equally critical (regardless of slack)."
                  " Values larger than 1.0 cause low slack connections to be treated more critically."
                  " The value increases to --td_place_exp_last during placement.")
            .default_value("1.0")
            .show_in(argparse::ShowIn::HELP_ONLY);
    place_timing_grp.add_argument(args.td_place_exp_last, "--td_place_exp_last")
            .help("Controls how critical a connection is as a function of slack at the end of placement.")
            .default_value("8.0")
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& route_grp = parser.add_argument_group("routing options");
    route_grp.add_argument(args.max_router_iterations, "--max_route_iterations")
            .help("Maximum number of Pathfinder-based routing iterations before the circuit is"
                  " declared unroutable at a given channel width")
            .default_value("50")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.initial_pres_fac, "--first_iter_pres_fac")
            .help("Sets the present overuse factor for the first routing iteration")
            .default_value("0.0")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.initial_pres_fac, "--initial_pres_fac")
            .help("Sets the present overuse factor for the second routing iteration")
            .default_value("0.5")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.pres_fac_mult, "--pres_fac_mult")
            .help("Sets the growth factor by which the present overuse penalty factor is"
                  " multiplied after each routing iteration")
            .default_value("1.3")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.acc_fac, "--acc_fac")
            .help("Specifies the accumulated overuse factor (historical congestion cost factor)")
            .default_value("1.0")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.bb_factor, "--bb_factor")
            .help("Sets the distance (in channels) outside a connection's bounding box which can be explored")
            .default_value("3")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.base_cost_type, "--base_cost_type")
            .help("Sets the basic cost of routing resource nodes:\n"
                  " * demand_only: based on expected demand of node type\n"
                  " * delay_normalized: like demand_only but normalized to magnitude of typical routing resource delay\n"
                  "(Default: demand_only for bread-first router, delay_normalized for timing-driven router)")
            .choices({"demand_only", "delay_normalized"})
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.bend_cost, "--bend_cost")
            .help("The cost of a bend. (Default: 1.0 for global routing, 0.0 for detailed routing)")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.route_type, "--route_type")
            .help("Specifies whether global, or combined global and detailed routing is performed.")
            .choices({"global", "detailed"})
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.route_chan_width, "--route_chan_width")
            .help("Specifies a fixed channel width to route at.")
            .metavar("CHANNEL_WIDTH");
    route_grp.add_argument(args.min_route_chan_width_hint, "--min_route_chan_width_hint")
            .help("Hint to the router what the minimum routable channel width is."
                  " Good hints can speed-up determining the minimum channel width.")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument<bool,OnOff>(args.verify_binary_search, "--verify_binary_search")
            .help("Force the router to verify the minimum channel width by routing at"
                  " consecutively lower channel widths until two consecutive failures are observed.")
            .default_value("off")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.router_algorithm, "--router_algorithm")
            .help("Specifies the router algorithm to use.\n"
                  " * breadth_first: focuses solely on routability\n"
                  " * timing driven: focuses on routability and circuit speed\n")
            .default_value("timing_driven")
            .choices({"breadth_first", "timing_driven"})
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_grp.add_argument(args.min_incremental_reroute_fanout, "--min_incremental_reroute_fanout")
            .help("The net fanout thershold above which nets will be re-routed incrementally.")
            .default_value("64")
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& route_timing_grp = parser.add_argument_group("timing-driven routing options");
    route_timing_grp.add_argument(args.astar_fac, "--astar_fac")
            .help("How aggressive the directed search used by the timing-driven router is."
                  " Values between 1 and 2 are resonable; higher values trade some quality for reduced run-time")
            .default_value("1.2")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_timing_grp.add_argument(args.max_criticality, "--max_criticality")
            .help("Sets the maximum fraction of routing cost derived from delay (vs routability) for any net."
                  " 0.0 means no attention is paid to delay, 1.0 means nets on the critical path ignore congestion")
            .default_value("0.99")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_timing_grp.add_argument(args.criticality_exp, "--criticality_exp")
            .help("Controls the delay-routability trade-off for nets as a function of slack."
                  " 0.0 implies all nets treated equally regardless of slack."
                  " At large values (>> 1) only nets on the critical path will consider delay.")
            .default_value("1.0")
            .show_in(argparse::ShowIn::HELP_ONLY);
    route_timing_grp.add_argument(args.routing_failure_predictor, "--routing_failure_predictor")
            .help("Controls how aggressively the router will predict a routing as unsuccessful"
                  " and give up early. This can significantly reducing the run-time required"
                  " to find the minimum channel width).\n"
                  " * safe: Only abort when it is extremely unlikely a routing will succeed\n"
                  " * aggressive: Further reduce run-time by giving up earlier. This may increase the reported minimum channel width\n"
                  " * off: Only abort when the maximum number of iterations is reached\n")
            .default_value("safe")
            .choices({"safe", "aggressive", "off"})
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& analysis_grp = parser.add_argument_group("analysis options");

    analysis_grp.add_argument<bool,OnOff>(args.full_stats, "--full_stats")
            .help("Print extra statistics about the circuit and it's routing (useful for wireability analysis)")
            .default_value("off")
            .show_in(argparse::ShowIn::HELP_ONLY);

    analysis_grp.add_argument<bool,OnOff>(args.gen_post_synthesis_netlist, "--gen_post_synthesis_netlist")
            .help("Generates the post-synthesis netlist (in BLIF and Verilog) along with delay information (in SDF)."
                  " Used for post-implementation simulation and verification")
            .default_value("off")
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& power_grp = parser.add_argument_group("power analysis options");
    power_grp.add_argument<bool,OnOff>(args.power, "--power")
            .help("Enable power estimation")
            .action(argparse::Action::STORE_TRUE)
            .default_value("off")
            .show_in(argparse::ShowIn::HELP_ONLY);
    power_grp.add_argument(args.tech_properties_file, "--tech_properties_file")
            .help("XML file containing CMOS technology properties (see documentation).")
            .show_in(argparse::ShowIn::HELP_ONLY);
    power_grp.add_argument(args.activity_file, "--activity_file")
            .help("Signal activities file for all nets (see documentation).")
            .show_in(argparse::ShowIn::HELP_ONLY);

    auto& test_grp = parser.add_argument_group("test options");

    test_grp.add_argument(args.one_or_more, "--one_or_more")
            .nargs('+');
    test_grp.add_argument(args.zero_or_more, "--zero_or_more")
            .nargs('*');

#ifndef TEST
    auto specified_args = parser.parse_args(argc, argv);
    for(auto& arg : specified_args) {
        std::cout << "Group: " << arg->group_name() << " Specified argument: " << arg->long_option();
        auto short_opt = arg->short_option();
        if (!short_opt.empty()) {
            std::cout << "/" << short_opt;
        }
        std::cout << "\n";
    }
    return 0;
#else
    parser.print_help();
    std::cout << "\n";

    std::vector<std::vector<std::string>> pass_cases = {
        {"my_arch1.xml", "my_circuit1.blif", "--analysis"},
        {"my_arch2.xml", "my_circuit2.blif", "--analysis", "--pack"},
        {"my_arch3.xml", "my_circuit3.blif", "--analysis", "--timing_analysis", "on"},
        {"my_arch4.xml", "my_circuit4.blif", "--analysis", "--route_chan_width", "300"},
        {"my_arch5.xml", "my_circuit5.blif", "--analysis", "--criticality_exp", "2"}, //Float from integer
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--criticality_exp", "2.0"}, //Float
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "-j", "3"},
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "-j3"}, //No-space for single letter arg
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "-j 3"}, //Space in short arg (but one string)
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "-j3", "--analysis"}, //Space in short arg (but one string)
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--one_or_more", "3.24"}, //Single value argument
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--one_or_more", "3.24", "10", "29"}, //Multiple values
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--zero_or_more"}, //No values
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--zero_or_more", "234"}, //One values
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--zero_or_more", "234", "254", "1.23"}, //Multiple values
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--zero_or_more", "234", "254", "1.23", "--one_or_more", "284"}, //* followed by +
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--zero_or_more", "--one_or_more", "284"}, //* followed by +
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--one_or_more", "284", "--zero_or_more", }, //+ followed by *
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--one_or_more", "284", "--zero_or_more", "798"}, //+ followed by *
        {"my_arch6.xml", "--analysis", "--one_or_more", "3.24", "10", "29", "my_circuit6.blif"}, //positional after nargs='+'
        {"my_arch6.xml", "--analysis", "--one_or_more", "3.24", "10", "29", "my_circuit6.blif"}, //positional after nargs='+'
        {"my_arch6.xml", "--analysis", "--zero_or_more", "3.24", "10", "29", "my_circuit6.blif"}, //positional after nargs='*'
    };

    int num_failed = 0;
    for(const auto& cmd_line : pass_cases) {
        bool pass = expect_pass(parser, cmd_line);

        if(!pass) {
            std::cout << "       Failed to parse: '" << argparse::join(cmd_line, " ") << "'" << std::endl;
            ++num_failed;
        }
    }

    std::vector<std::vector<std::string>> fail_cases = {
        {"--analysis"}, //Missing positional
        {"my_arch7.xml", "--analysis"}, //Missing positional
        {"my_arch8.xml", "my_circuit8.blif", "--analysis", "extra"}, //Extra positional
        {"my_arch9.xml", "my_circuit9.blif", "--analysis", "--route_chan_width"}, //Missing value to option
        {"my_arch10.xml", "my_circuit10.blif", "--analysis", "--route_chan_width", "off"}, //Wrong option value type
        {"my_arch11.xml", "my_circuit11.blif", "--analysis", "--disp", "132"}, //Wrong option value
        {"my_arch12.xml", "my_circuit12.blif", "--analysis", "--route_chan_width", "300", "5"}, //Extra option value
        {"my_arch13.xml", "my_circuit13.blif", "--analysis", "--pack", "on"}, //Extra option value to toggle option
        {"my_arch14.xml", "my_circuit14.blif", "--analysis", "--route_chan_width", "300.5"}, //Type mismatch: float->int
        {"my_arch15.xml", "my_circuit15.blif", "--analysis", "--criticality_exp", "on"}, //Wrong value type for float
        {"my_arch16.xml", "my_circuit16.blif", "--analysis", "--slack_definition", "Z"}, //Valid type, but wrong choice
        {"my_arch17.xml", "my_circuit17.blif"}, //Missing required
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "-j", "3.4"}, //Float when expected unsigned
        {"my_arch6.xml", "my_circuit6.blif", "--analysis", "--one_or_more"}, //Expected at least one argument
    };

    for(const auto& cmd_line : fail_cases) {
        bool pass = expect_fail(parser, cmd_line);

        if(!pass) {
            std::cout << "       Parsed successfully when expected failure: '" << argparse::join(cmd_line, " ") << "'" << std::endl;
            ++num_failed;
        }
    }

    if (num_failed != 0) {
        std::cout << "\n";
        std::cout << "FAILED: " << num_failed << " test(s)!" << "\n";
    }

    return num_failed;
#endif
}

bool expect_pass(argparse::ArgumentParser& parser, const std::vector<std::string>& cmd_line) {
    try {
        parser.parse_args_throw(cmd_line);
    } catch(const argparse::ArgParseHelp&) {
        parser.reset_destinations();
        std::cout << "[PASS] Parsed help OK" << std::endl;
        return true;
    } catch(const argparse::ArgParseError& err) {
        std::cout << "[FAIL] " << err.what() << std::endl;
        parser.reset_destinations();
        return false;
    }
    std::cout << "[PASS] Parsed OK" << std::endl;
    parser.reset_destinations();
    return true;
}

bool expect_fail(argparse::ArgumentParser& parser, const std::vector<std::string>& cmd_line) {
    try {
        parser.parse_args_throw(cmd_line);
    } catch(const argparse::ArgParseError& err) {
        std::cout << "[PASS] " << err.what() << std::endl;
        parser.reset_destinations();
        return true;
    }
    std::cout << "[FAIL] Parsed OK when expected fail" << std::endl;
    parser.reset_destinations();
    return false;
}
