<commit_before>// Copyright © 2016 <NAME>
//
// This file is part of kotlin-argparser, a library which can be found at
// http://github.com/xenomachina/kotlin-argparser
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by the
// Free Software Foundation; either version 2.1 of the License, or (at your
// option) any later version.
//
// This library 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 Lesser General Public License
// for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, see http://www.gnu.org/licenses/

package com.xenomachina.argparser

import com.xenomachina.argparser.PosixNaming.identifierToOptionName
import com.xenomachina.common.orElse
import io.kotlintest.matchers.beOfType
import io.kotlintest.matchers.should
import io.kotlintest.matchers.shouldBe
import io.kotlintest.matchers.shouldEqual
import io.kotlintest.matchers.shouldThrow
import io.kotlintest.specs.FunSpec
import java.io.File
import java.io.StringWriter

val TEST_HELP = "test help message"

fun parserOf(
    vararg args: String,
    mode: ArgParser.Mode = ArgParser.Mode.GNU,
    helpFormatter: HelpFormatter? = DefaultHelpFormatter()
) = ArgParser(args, mode, helpFormatter)

enum class Color { RED, GREEN, BLUE }

open class Shape
class Rectangle(val s: String) : Shape()
class Circle : Shape()

/**
 * Helper function for getting the static (not runtime) type of an expression. This is useful for verifying that the
 * inferred type of an expression is what you think it should be. For example:
 *
 *     staticType(actuallyACircle) shouldBe Shape::class
 */
inline fun <reified T : Any> staticType(@Suppress("UNUSED_PARAMETER") x: T) = T::class

private val oneArgName = listOf("ARG_NAME")

abstract class Test(body: () -> Unit) : FunSpec({ test(this::class.qualifiedName!!, body) })

class OptionNameValidationTest : Test({
    val parser = parserOf()

    // These are all acceptable.
    parser.option<Int>("-x", help = TEST_HELP) { 0 }
    parser.option<Int>("--x", help = TEST_HELP) { 0 }
    parser.option<Int>("--xy", help = TEST_HELP) { 0 }
    parser.option<Int>("-X", help = TEST_HELP) { 0 }
    parser.option<Int>("--X", help = TEST_HELP) { 0 }
    parser.option<Int>("--XY", help = TEST_HELP) { 0 }
    parser.option<Int>("--X-Y", help = TEST_HELP) { 0 }
    parser.option<Int>("--X_Y", help = TEST_HELP) { 0 }
    parser.option<Int>("-5", help = TEST_HELP) { 0 }
    parser.option<Int>("--5", help = TEST_HELP) { 0 }
    parser.option<Int>("--5Y", help = TEST_HELP) { 0 }
    parser.option<Int>("--X5", help = TEST_HELP) { 0 }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("-_", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("---x", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("x", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("-xx", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--foo bar", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--foo--bar", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--f!oobar", help = TEST_HELP) { 0 }
    }
})

class PositionalNameValidationTest : Test({
    val parser = parserOf()

    // These are all acceptable.
    parser.positional<Int>("X", help = TEST_HELP) { 0 }
    parser.positional<Int>("XYZ", help = TEST_HELP) { 0 }
    parser.positional<Int>("XY-Z", help = TEST_HELP) { 0 }
    parser.positional<Int>("XY_Z", help = TEST_HELP) { 0 }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("-", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("_", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("x", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("-X", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("X-", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("X--Y", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("X!", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("5", help = TEST_HELP) { 0 }
    }

    // This should be acceptable
    parser.option<Int>("--foobar", argNames = listOf("X-Y"), help = TEST_HELP) { 0 }

    // This should not
    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--foobar", argNames = listOf("X--Y"), help = TEST_HELP) { 0 }
    }
})

class ArglessShortOptionsTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("-x", "-y", "-z",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
    }

    Args(parserOf("-x", "-y", "-z", "-z", "-y")).xyz shouldBe listOf("-x", "-y", "-z", "-z", "-y")

    Args(parserOf("-xyz")).xyz shouldBe listOf("-x", "-y", "-z")
})

class ShortOptionsWithArgsTest : Test({
    class Args(parser: ArgParser) {
        val a by parser.flagging("-a", help = TEST_HELP)
        val b by parser.flagging("-b", help = TEST_HELP)
        val c by parser.flagging("-c", help = TEST_HELP)
        val xyz by parser.option<MutableList<String>>("-x", "-y", "-z",
                argNames = oneArgName, help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName:${arguments.first()}")
            }
        }
    }

    // Test with value as separate arg
    Args(parserOf("-x", "0", "-y", "1", "-z", "2", "-z", "3", "-y", "4")).xyz shouldBe listOf("-x:0", "-y:1", "-z:2", "-z:3", "-y:4")

    // Test with value concatenated
    Args(parserOf("-x0", "-y1", "-z2", "-z3", "-y4")).xyz shouldBe listOf("-x:0", "-y:1", "-z:2", "-z:3", "-y:4")

    // Test with = between option and value. Note that the "=" is treated as part of the option value for short options.
    Args(parserOf("-x=0", "-y=1", "-z=2", "-z=3", "-y=4")).xyz shouldBe listOf("-x:=0", "-y:=1", "-z:=2", "-z:=3", "-y:=4")

    // Test chained options. Note that an option with arguments must be last in the chain
    val chain1 = Args(parserOf("-abxc"))
    chain1.a shouldBe true
    chain1.b shouldBe true
    chain1.c shouldBe false
    chain1.xyz shouldBe listOf("-x:c")

    val chain2 = Args(parserOf("-axbc"))
    chain2.a shouldBe true
    chain2.b shouldBe false
    chain2.c shouldBe false
    chain2.xyz shouldBe listOf("-x:bc")
})

class MixedShortOptionsTest : Test({
    class Args(parser: ArgParser) {
        val def by parser.option<MutableList<String>>("-d", "-e", "-f",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
        val abc by parser.option<MutableList<String>>("-a", "-b", "-c",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
    }

    Args(parserOf("-adbefccbafed")).run {
        def shouldBe listOf("-d", "-e", "-f", "-f", "-e", "-d")
        abc shouldBe listOf("-a", "-b", "-c", "-c", "-b", "-a")
    }
})

class MixedShortOptionsWithArgsTest : Test({
    class Args(parser: ArgParser) {
        val def by parser.option<MutableList<String>>("-d", "-e", "-f",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
        val abc by parser.option<MutableList<String>>("-a", "-b", "-c",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
        val xyz by parser.option<MutableList<String>>("-x", "-y", "-z",
                argNames = oneArgName,
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName:${arguments.first()}")
            }
        }
    }

    Args(parserOf("-adecfy5", "-x0", "-bzxy")).run {
        abc shouldBe listOf("-a", "-c", "-b")
        def shouldBe listOf("-d", "-e", "-f")
        xyz shouldBe listOf("-y:5", "-x:0", "-z:xy")
    }
})

class ArglessLongOptionsTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("--xray", "--yellow", "--zebra",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
    }

    Args(parserOf("--xray", "--yellow", "--zebra", "--zebra", "--yellow")).xyz shouldBe listOf("--xray", "--yellow", "--zebra", "--zebra", "--yellow")

    Args(parserOf("--xray", "--yellow", "--zebra")).xyz shouldBe listOf("--xray", "--yellow", "--zebra")
})

class LongOptionsWithOneArgTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("--xray", "--yellow", "--zaphod",
                argNames = oneArgName,
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName:${arguments.first()}")
            }
        }
    }

    // Test with value as separate arg
    Args(parserOf("--xray", "0", "--yellow", "1", "--zaphod", "2", "--zaphod", "3", "--yellow", "4")).xyz shouldBe listOf("--xray:0", "--yellow:1", "--zaphod:2", "--zaphod:3", "--yellow:4")

    // Test with = between option and value
    Args(parserOf("--xray=0", "--yellow=1", "--zaphod=2", "--zaphod=3", "--yellow=4")).xyz shouldBe listOf("--xray:0", "--yellow:1", "--zaphod:2", "--zaphod:3", "--yellow:4")

    shouldThrow<UnrecognizedOptionException> {
        Args(parserOf("--xray0", "--yellow1", "--zaphod2", "--zaphod3", "--yellow4")).xyz
    }.run {
        message shouldBe "unrecognized option '--xray0'"
    }
})

class LongOptionsWithMultipleArgsTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("--xray", "--yak", "--zaphod",
                argNames = listOf("COLOR", "SIZE", "FLAVOR"),
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply { add("$optionName:$arguments") }
        }
    }

    // Test with value as separate arg
    Args(parserOf("--xray", "red", "5", "salty")).xyz shouldBe listOf("--xray:[red, 5, salty]")

    Args(parserOf("--zaphod", "green", "42", "sweet", "--yak", "blue", "7", "bitter")).xyz shouldBe listOf(
            "--zaphod:[green, 42, sweet]", "--yak:[blue, 7, bitter]")

    // Note that something that looks like an option is consumed as an argument if it appears where an argument
    // should be. This is expected behavior.
    Args(parserOf("--zaphod", "green", "42", "--yak")).xyz shouldBe listOf(
            "--zaphod:[green, 42, --yak]")

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--zaphod", "green", "42", "sweet", "--yak", "blue", "7")).xyz
    }.run {
        message shouldBe "option '--yak' is missing the required argument FLAVOR"
    }

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--zaphod", "green")).xyz
    }.run {
        message shouldBe "option '--zaphod' is missing the required argument SIZE"
    }

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--xray")).xyz
    }.run {
        message shouldBe "option '--xray' is missing the required argument COLOR"
    }
})

class DelegateProviderTest : Test({
    fun ArgParser.putting(vararg names: String, help: String) =
            option<MutableMap<String, String>>(*names,
                    argNames = listOf("KEY", "VALUE"),
                    help = help) {
                value.orElse { mutableMapOf<String, String>() }.apply {
                    put(arguments.first(), arguments.last()) }
            }

    fun ArgParser.putting(help: String) =
            ArgParser.DelegateProvider { identifier ->
                putting(identifierToOptionName(identifier), help = help) }

    class Args(parser: ArgParser) {
        val dict by parser.putting(TEST_HELP)
    }

    // Test with value as separate arg
    Args(parserOf("--dict", "red", "5")).dict shouldBe mapOf("red" to "5")

    Args(parserOf(
            "--dict", "green", "42",
            "--dict", "blue", "7"
    )).dict shouldBe mapOf(
            "green" to "42",
            "blue" to "7")

    // Note that something that looks like an option is consumed as an argument if it appears where an argument
    // should be. This is expected behavior.
    Args(parserOf("--dict", "green", "--dict")).dict shouldBe mapOf("green" to "--dict")

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--dict", "green", "42", "--dict", "blue")).dict
    }.run {
        message shouldBe "option '--dict' is missing the required argument VALUE"
    }

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--dict")).dict
    }.run {
        message shouldBe "option '--dict' is missing the required argument KEY"
    }
})

class DefaultTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x",
                help = TEST_HELP) { toInt() }.default(5)
    }

    // Test with no value
    Args(parserOf()).x shouldBe 5

    // Test with value
    Args(parserOf("-x6")).x shouldBe 6

    // Test with value as separate arg
    Args(parserOf("-x", "7")).x shouldBe 7

    // Test with multiple values
    Args(parserOf("-x9", "-x8")).x shouldBe 8
})

class DefaultWithProviderTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing(help = TEST_HELP) { toInt() }.default(5)
    }

    // Test with no value
    Args(parserOf()).x shouldBe 5

    // Test with value
    Args(parserOf("-x6")).x shouldBe 6

    // Test with value as separate arg
    Args(parserOf("-x", "7")).x shouldBe 7

    // Test with multiple values
    Args(parserOf("-x9", "-x8")).x shouldBe 8
})

class DefaultWithLambda : Test({
    class Args(parser: ArgParser) {
        var defaultCalled = false
        val x by parser.storing(help = TEST_HELP) { toInt() }.default { defaultCalled = true; 5 }
    }

    // Test default hasn't been called
    Args(parserOf("-x6")).defaultCalled shouldBe false

    // Test with no value
    val args = Args(parserOf())
    args.x shouldBe 5
    args.defaultCalled shouldBe true
})

class FlagTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.flagging("-x", "--ecks",
                help = TEST_HELP)
        val y by parser.flagging("-y",
                help = TEST_HELP)
        val z by parser.flagging("--zed",
                help = TEST_HELP)
    }

    Args(parserOf("-x", "-y", "--zed", "--zed", "-y")).run {
        x shouldBe true
        y shouldBe true
        z shouldBe true
    }

    Args(parserOf()).run {
        x shouldBe false
        y shouldBe false
        z shouldBe false
    }

    Args(parserOf("-y", "--ecks")).run {
        x shouldBe true
        y shouldBe true
    }

    Args(parserOf("--zed")).run {
        x shouldBe false
        y shouldBe false
        z shouldBe true
    }
})

class Argument_noParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("--ecks", "-x",
                help = TEST_HELP)
    }

    Args(parserOf("-x", "foo")).x shouldBe "foo"

    Args(parserOf("-x", "bar", "-x", "baz")).x shouldBe "baz"

    Args(parserOf("--ecks", "long", "-x", "short")).x shouldBe "short"

    Args(parserOf("-x", "short", "--ecks", "long")).x shouldBe "long"

    val args = Args(parserOf())
    shouldThrow<MissingValueException> {
        args.x
    }.run {
        message shouldBe "missing ECKS"
    }
})

class Argument_missing_longTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("--ecks",
                help = TEST_HELP)
    }

    val args = Args(parserOf())
    shouldThrow<MissingValueException> {
        args.x
    }.run {
        message shouldBe "missing ECKS"
    }
})

class Argument_missing_shortTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x",
                help = TEST_HELP)
    }

    val args = Args(parserOf())
    shouldThrow<MissingValueException> {
        args.x
    }.run {
        message shouldBe "missing X"
    }
})

class Argument_withParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x", "--ecks",
                help = TEST_HELP) { toInt() }
    }

    val opts1 = Args(parserOf("-x", "5"))
    opts1.x shouldBe 5

    val opts2 = Args(parserOf("-x", "1", "-x", "2"))
    opts2.x shouldBe 2

    val opts3 = Args(parserOf("--ecks", "3", "-x", "4"))
    opts3.x shouldBe 4

    val opts4 = Args(parserOf("-x", "5", "--ecks", "6"))
    opts4.x shouldBe 6

    val opts6 = Args(parserOf())
    shouldThrow<MissingValueException> {
        opts6.x
    }.run {
        message shouldBe "missing ECKS"
    }
})

class Accumulator_noParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.adding("-x", "--ecks",
                help = TEST_HELP)
    }

    Args(parserOf()).x shouldBe listOf<String>()

    Args(parserOf("-x", "foo")).x shouldBe listOf("foo")

    Args(parserOf("-x", "bar", "-x", "baz")).x shouldBe listOf("bar", "baz")

    Args(parserOf("--ecks", "long", "-x", "short")).x shouldBe listOf("long", "short")

    Args(parserOf("-x", "short", "--ecks", "long")).x shouldBe listOf("short", "long")
})

class Accumulator_withParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.adding("-x", "--ecks",
                help = TEST_HELP) { toInt() }
    }

    Args(parserOf()).x shouldBe listOf<Int>()
    Args(parserOf("-x", "5")).x shouldBe listOf(5)
    Args(parserOf("-x", "1", "-x", "2")).x shouldBe listOf(1, 2)
    Args(parserOf("--ecks", "3", "-x", "4")).x shouldBe listOf(3, 4)
    Args(parserOf("-x", "5", "--ecks", "6")).x shouldBe listOf(5, 6)
})

class ColorArgs(parser: ArgParser) {
    val color by parser.mapping(
            "--red" to Color.RED,
            "--green" to Color.GREEN,
            "--blue" to Color.BLUE,
            help = TEST_HELP)
}

class MappingTest : Test({

    ColorArgs(parserOf("--red")).color shouldBe Color.RED
    ColorArgs(parserOf("--green")).color shouldBe Color.GREEN
    ColorArgs(parserOf("--blue")).color shouldBe Color.BLUE

    // Last one takes precedence
    ColorArgs(parserOf("--blue", "--red")).color shouldBe Color.RED
    ColorArgs(parserOf("--blue", "--green")).color shouldBe Color.GREEN
    ColorArgs(parserOf("--red", "--blue")).color shouldBe Color.BLUE

    val args = ColorArgs(parserOf())
    shouldThrow<MissingValueException> {
        args.color
    }.run {
        message shouldBe "missing --red|--green|--blue"
    }
})

class OptionalColorArgs(parser: ArgParser) {
    val color by parser.mapping(
            "--red" to Color.RED,
            "--green" to Color.GREEN,
            "--blue" to Color.BLUE,
            help = TEST_HELP)
            .default(Color.GREEN)
}

class Mapping_withDefaultTest : Test({
    OptionalColorArgs(parserOf("--red")).color shouldBe Color.RED
    OptionalColorArgs(parserOf("--green")).color shouldBe Color.GREEN
    OptionalColorArgs(parserOf("--blue")).color shouldBe Color.BLUE
    OptionalColorArgs(parserOf()).color shouldBe Color.GREEN
})

class UnrecognizedShortOptTest : Test({
    shouldThrow<UnrecognizedOptionException> {
        OptionalColorArgs(parserOf("-x")).color
    }.run {
        message shouldBe "unrecognized option '-x'"
    }
})

class UnrecognizedLongOptTest : Test({
    shouldThrow<UnrecognizedOptionException> {
        OptionalColorArgs(parserOf("--ecks")).color
    }.run {
        message shouldBe "unrecognized option '--ecks'"
    }
})

class StoringNoArgTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x", "--ecks",
                help = TEST_HELP)
    }

    // Note that name actually used for option is used in message
    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("-x")).x
    }.run {
        message shouldBe "option '-x' is missing a required argument"
    }

    // Note that name actually used for option is used in message
    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--ecks")).x
    }.run {
        message shouldBe "option '--ecks' is missing a required argument"
    }
})

class ShortStoringNoArgChainedTest : Test({
    class Args(parser: ArgParser) {
        val y by parser.flagging("-y",
                help = TEST_HELP)
        val x by parser.storing("-x",
                help = TEST_HELP)
    }

    // Note that despite chaining, hyphen appears in message
    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("-yx")).x
    }.run {
        message shouldBe "option '-x' is missing a required argument"
    }
})

class InitValidationTest : Test({
    class Args(parser: ArgParser) {
        val yDelegate = parser.storing("-y",
                help = TEST_HELP) { toInt() }
        val y by yDelegate

        val xDelegate = parser.storing("-x",
                help = TEST_HELP) { toInt() }
        val x by xDelegate

        init {
            if (y >= x)
                throw InvalidArgumentException("${yDelegate.errorName} must be less than ${xDelegate.errorName}")

            // A better way to accomplish validation that only depends on one Delegate is to use
            // Delegate.addValidator. See testAddValidator for an example of this.
            if (x.rem(2) != 0)
                throw InvalidArgumentException("${xDelegate.errorName} must be even, $x is odd")
        }
    }

    // This should pass validation
    val opts0 = Args(parserOf("-y1", "-x10"))
    opts0.y shouldBe 1
    opts0.x shouldBe 10

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y20", "-x10")).x
    }.run {
        message shouldBe "Y must be less than X"
    }

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y10", "-x15")).x
    }.run {
        message shouldBe "X must be even, 15 is odd"
    }

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y10", "-x15")).x
    }.run {
        message shouldBe "X must be even, 15 is odd"
    }
})

class AddValidatorTest : Test({
    class Args(parser: ArgParser) {
        val yDelegate = parser.storing("-y",
                help = TEST_HELP) { toInt() }
        val y by yDelegate

        val xDelegate = parser.storing("-x",
                help = TEST_HELP) { toInt() }
                .addValidator {
                    if (value.rem(2) != 0)
                        throw InvalidArgumentException("$errorName must be even, $value is odd")
                }
        val x by xDelegate

        init {
            if (y >= x)
                throw InvalidArgumentException("${yDelegate.errorName} must be less than ${xDelegate.errorName}")
        }
    }

    // This should pass validation
    val opts0 = Args(parserOf("-y1", "-x10"))
    opts0.y shouldBe 1
    opts0.x shouldBe 10

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y20", "-x10")).x
    }.run {
        message shouldBe "Y must be less than X"
    }

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y10", "-x15")).x
    }.run {
        message shouldBe "X must be even, 15 is odd"
    }
})

class UnconsumedTest : Test({
    class Args(parser: ArgParser) {
        val y by parser.flagging("-y", "--why",
                help = TEST_HELP)
        val x by parser.flagging("-x", "--ecks",
                help = TEST_HELP)
    }

    // No problem.
    Args(parserOf("-yx")).run {
        x shouldBe true
        y shouldBe true
    }

    // Attempting to give -y a parameter, "z", is treated as unrecognized option.
    shouldThrow<UnrecognizedOptionException> {
        Args(parserOf("-yz")).y
    }.run {
        message shouldBe "unrecognized option '-z'"
    }

    // Unconsumed "z" again, but note that it triggers even if we don't look at y.
    shouldThrow<UnrecognizedOptionException> {
        Args(parserOf("-yz")).x
    }.run {
        message shouldBe "unrecognized option '-z'"
    }

    // No problem again, this time with long opts.
    Args(parserOf("--why", "--ecks")).run {
        x shouldBe true
        y shouldBe true
    }

    // Attempting to give --why a parameter, "z" causes an error.
    shouldThrow<UnexpectedOptionArgumentException> {
        Args(parserOf("--why=z")).y
    }.run {
        message shouldBe "option '--why' doesn't allow an argument"
    }

    // Unconsumed "z" again, but note that it triggers even if we don't look at y.
    shouldThrow<UnexpectedOptionArgumentException> {
        Args(parserOf("--why=z")).x
    }.run {
        message shouldBe "option '--why' doesn't allow an argument"
    }
})

class Positional_basicTest : Test({
    class Args(parser: ArgParser) {
        val flag by parser.flagging("-f", "--flag",
                help = TEST_HELP)
        val store by parser.storing("-s", "--store",
                help = TEST_HELP).default("DEFAULT")
        val sources by parser.positionalList("SOURCE",
                help = TEST_HELP)
        val destination by parser.positional("DEST",
                help = TEST_HELP)
    }

    Args(parserOf("foo", "bar", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"
        sources shouldBe listOf("foo", "bar", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-f", "foo", "bar", "baz", "quux")).run {
        flag shouldBe true
        store shouldBe "DEFAULT"
        sources shouldBe listOf("foo", "bar", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-s", "foo", "bar", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-s", "foo", "bar", "-f", "baz", "quux")).run {
        flag shouldBe true
        store shouldBe "foo"
        sources shouldBe listOf("bar", "baz")
        destination shouldBe "quux"
    }

    // "--" disables option processing for all further arguments.
    // Note that "-f" is now considered a positional argument.
    Args(parserOf("-s", "foo", "--", "bar", "-f", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "-f", "baz")
        destination shouldBe "quux"
    }

    // "--" disables option processing for all further arguments.
    // Note that the second "--" is also considered a positional argument.
    Args(parserOf("-s", "foo", "--", "bar", "--", "-f", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "--", "-f", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-s", "foo", "bar", "-f", "baz", "quux", mode = ArgParser.Mode.POSIX)).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "-f", "baz")
        destination shouldBe "quux"
    }
})

class Positional_withParserTest : Test({
    class Args(parser: ArgParser) {
        val flag by parser.flagging("-f", "--flag",
                help = TEST_HELP)
        val store by parser.storing("-s", "--store",
                help = TEST_HELP).default("DEFAULT")
        val start by parser.positionalList("START", TEST_HELP, 3..4) { toInt() }
        val end by parser.positionalList("END", TEST_HELP, 3..5) { toInt() }
    }

    shouldThrow<MissingRequiredPositionalArgumentException> {
        Args(parserOf("1", "2")).flag
    }.run {
        message shouldBe "missing START operand"
    }

    shouldThrow<MissingRequiredPositionalArgumentException> {
        Args(parserOf("1", "2", "3", "4", "5")).flag
    }.run {
        message shouldBe "missing END operand"
    }

    Args(parserOf("1", "2", "3", "4", "5", "6")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // end needs at least 3 args, so start only consumes 3
        start shouldBe listOf(1, 2, 3)
        end shouldBe listOf(4, 5, 6)
    }

    Args(parserOf("1", "2", "3", "4", "5", "6", "7")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // end only needs at 3 args, so start can consume 4
        start shouldBe listOf(1, 2, 3, 4)
        end shouldBe listOf(5, 6, 7)
    }

    Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // start can't consume more than 4, so end gets the rest.
        start shouldBe listOf(1, 2, 3, 4)
        end shouldBe listOf(5, 6, 7, 8)
    }

    Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8", "9")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // once again, start can't consume more than 4, so end gets the rest.
        start shouldBe listOf(1, 2, 3, 4)
        end shouldBe listOf(5, 6, 7, 8, 9)
    }

    shouldThrow<UnexpectedPositionalArgumentException> {
        Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")).flag
    }.run {
        message shouldBe "unexpected argument after END"
    }
})

class CountingTest : Test({
    class Args(parser: ArgParser) {
        val verbosity by parser.counting("-v", "--verbose",
                help = TEST_HELP)
    }

    Args(parserOf()).run {
        verbosity shouldBe 0
    }

    Args(parserOf("-v")).run {
        verbosity shouldBe 1
    }

    Args(parserOf("-v", "-v")).run {
        verbosity shouldBe 2
    }
})

class HelpTest : Test({
    class Args(parser: ArgParser) {
        val dryRun by parser.flagging("-n", "--dry-run",
                help = "don't do anything")
        val includes by parser.adding("-I", "--include",
                help = "search in this directory for header files")
        val outDir by parser.storing("-o", "--output",
                help = "directory in which all output should be generated")
        val verbosity by parser.counting("-v", "--verbose",
                help = "increase verbosity")
        val sources by parser.positionalList("SOURCE",
                help = "source file")
        val destination by parser.positional("DEST",
                help = "destination file")
    }

    shouldThrow<ShowHelpException> {
        Args(parserOf("--help",
                helpFormatter = DefaultHelpFormatter(
                        prologue = """
                            This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus.
                            """,
                        epilogue = """
                            This is the epilogue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel tortor nunc. Sed eu massa sed turpis auctor faucibus. Donec vel pellentesque tortor. Ut ultrices tempus lectus fermentum vestibulum. Phasellus.
                            """))).dryRun
    }.run {
        val help = StringWriter().apply { printUserMessage(this, "program_name", 60) }.toString()
        help shouldBe """
usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT
                    [-v]... SOURCE... DEST


This is the prologue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Aliquam malesuada maximus eros.
Fusce luctus risus eget quam consectetur, eu auctor est
ullamcorper. Maecenas eget suscipit dui, sed sodales erat.
Phasellus.


required arguments:
  -o OUTPUT,          directory in which all output should
  --output OUTPUT     be generated


optional arguments:
  -h, --help          show this help message and exit

  -n, --dry-run       don't do anything

  -I INCLUDE,         search in this directory for header
  --include INCLUDE   files

  -v, --verbose       increase verbosity


positional arguments:
  SOURCE              source file

  DEST                destination file


This is the epilogue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Donec vel tortor nunc. Sed eu
massa sed turpis auctor faucibus. Donec vel pellentesque
tortor. Ut ultrices tempus lectus fermentum vestibulum.
Phasellus.
""".trimStart()

        val help2 = StringWriter().apply { printUserMessage(this, "a_really_long_program_name", 60) }.toString()
        help2 shouldBe """
usage: a_really_long_program_name
         [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]...
         SOURCE... DEST


This is the prologue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Aliquam malesuada maximus eros.
Fusce luctus risus eget quam consectetur, eu auctor est
ullamcorper. Maecenas eget suscipit dui, sed sodales erat.
Phasellus.


required arguments:
  -o OUTPUT,          directory in which all output should
  --output OUTPUT     be generated


optional arguments:
  -h, --help          show this help message and exit

  -n, --dry-run       don't do anything

  -I INCLUDE,         search in this directory for header
  --include INCLUDE   files

  -v, --verbose       increase verbosity


positional arguments:
  SOURCE              source file

  DEST                destination file


This is the epilogue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Donec vel tortor nunc. Sed eu
massa sed turpis auctor faucibus. Donec vel pellentesque
tortor. Ut ultrices tempus lectus fermentum vestibulum.
Phasellus.
""".trimStart()

        // Regression test for issue #17
        val help_wide = StringWriter().apply { printUserMessage(this, "program_name", 0) }.toString()
        help_wide shouldBe """
usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]... SOURCE... DEST


This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus.


required arguments:
  -o OUTPUT, --output OUTPUT      directory in which all output should be generated

<commit_msg>Add multi-paragraph help prologue to tests<commit_after>// Copyright © 2016 <NAME>
//
// This file is part of kotlin-argparser, a library which can be found at
// http://github.com/xenomachina/kotlin-argparser
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by the
// Free Software Foundation; either version 2.1 of the License, or (at your
// option) any later version.
//
// This library 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 Lesser General Public License
// for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, see http://www.gnu.org/licenses/

package com.xenomachina.argparser

import com.xenomachina.argparser.PosixNaming.identifierToOptionName
import com.xenomachina.common.orElse
import io.kotlintest.matchers.beOfType
import io.kotlintest.matchers.should
import io.kotlintest.matchers.shouldBe
import io.kotlintest.matchers.shouldEqual
import io.kotlintest.matchers.shouldThrow
import io.kotlintest.specs.FunSpec
import java.io.File
import java.io.StringWriter

val TEST_HELP = "test help message"

fun parserOf(
    vararg args: String,
    mode: ArgParser.Mode = ArgParser.Mode.GNU,
    helpFormatter: HelpFormatter? = DefaultHelpFormatter()
) = ArgParser(args, mode, helpFormatter)

enum class Color { RED, GREEN, BLUE }

open class Shape
class Rectangle(val s: String) : Shape()
class Circle : Shape()

/**
 * Helper function for getting the static (not runtime) type of an expression. This is useful for verifying that the
 * inferred type of an expression is what you think it should be. For example:
 *
 *     staticType(actuallyACircle) shouldBe Shape::class
 */
inline fun <reified T : Any> staticType(@Suppress("UNUSED_PARAMETER") x: T) = T::class

private val oneArgName = listOf("ARG_NAME")

abstract class Test(body: () -> Unit) : FunSpec({ test(this::class.qualifiedName!!, body) })

class OptionNameValidationTest : Test({
    val parser = parserOf()

    // These are all acceptable.
    parser.option<Int>("-x", help = TEST_HELP) { 0 }
    parser.option<Int>("--x", help = TEST_HELP) { 0 }
    parser.option<Int>("--xy", help = TEST_HELP) { 0 }
    parser.option<Int>("-X", help = TEST_HELP) { 0 }
    parser.option<Int>("--X", help = TEST_HELP) { 0 }
    parser.option<Int>("--XY", help = TEST_HELP) { 0 }
    parser.option<Int>("--X-Y", help = TEST_HELP) { 0 }
    parser.option<Int>("--X_Y", help = TEST_HELP) { 0 }
    parser.option<Int>("-5", help = TEST_HELP) { 0 }
    parser.option<Int>("--5", help = TEST_HELP) { 0 }
    parser.option<Int>("--5Y", help = TEST_HELP) { 0 }
    parser.option<Int>("--X5", help = TEST_HELP) { 0 }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("-_", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("---x", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("x", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("-xx", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--foo bar", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--foo--bar", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--f!oobar", help = TEST_HELP) { 0 }
    }
})

class PositionalNameValidationTest : Test({
    val parser = parserOf()

    // These are all acceptable.
    parser.positional<Int>("X", help = TEST_HELP) { 0 }
    parser.positional<Int>("XYZ", help = TEST_HELP) { 0 }
    parser.positional<Int>("XY-Z", help = TEST_HELP) { 0 }
    parser.positional<Int>("XY_Z", help = TEST_HELP) { 0 }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("-", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("_", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("x", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("-X", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("X-", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("X--Y", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("X!", help = TEST_HELP) { 0 }
    }

    shouldThrow<IllegalArgumentException> {
        parser.positional<Int>("5", help = TEST_HELP) { 0 }
    }

    // This should be acceptable
    parser.option<Int>("--foobar", argNames = listOf("X-Y"), help = TEST_HELP) { 0 }

    // This should not
    shouldThrow<IllegalArgumentException> {
        parser.option<Int>("--foobar", argNames = listOf("X--Y"), help = TEST_HELP) { 0 }
    }
})

class ArglessShortOptionsTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("-x", "-y", "-z",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
    }

    Args(parserOf("-x", "-y", "-z", "-z", "-y")).xyz shouldBe listOf("-x", "-y", "-z", "-z", "-y")

    Args(parserOf("-xyz")).xyz shouldBe listOf("-x", "-y", "-z")
})

class ShortOptionsWithArgsTest : Test({
    class Args(parser: ArgParser) {
        val a by parser.flagging("-a", help = TEST_HELP)
        val b by parser.flagging("-b", help = TEST_HELP)
        val c by parser.flagging("-c", help = TEST_HELP)
        val xyz by parser.option<MutableList<String>>("-x", "-y", "-z",
                argNames = oneArgName, help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName:${arguments.first()}")
            }
        }
    }

    // Test with value as separate arg
    Args(parserOf("-x", "0", "-y", "1", "-z", "2", "-z", "3", "-y", "4")).xyz shouldBe listOf("-x:0", "-y:1", "-z:2", "-z:3", "-y:4")

    // Test with value concatenated
    Args(parserOf("-x0", "-y1", "-z2", "-z3", "-y4")).xyz shouldBe listOf("-x:0", "-y:1", "-z:2", "-z:3", "-y:4")

    // Test with = between option and value. Note that the "=" is treated as part of the option value for short options.
    Args(parserOf("-x=0", "-y=1", "-z=2", "-z=3", "-y=4")).xyz shouldBe listOf("-x:=0", "-y:=1", "-z:=2", "-z:=3", "-y:=4")

    // Test chained options. Note that an option with arguments must be last in the chain
    val chain1 = Args(parserOf("-abxc"))
    chain1.a shouldBe true
    chain1.b shouldBe true
    chain1.c shouldBe false
    chain1.xyz shouldBe listOf("-x:c")

    val chain2 = Args(parserOf("-axbc"))
    chain2.a shouldBe true
    chain2.b shouldBe false
    chain2.c shouldBe false
    chain2.xyz shouldBe listOf("-x:bc")
})

class MixedShortOptionsTest : Test({
    class Args(parser: ArgParser) {
        val def by parser.option<MutableList<String>>("-d", "-e", "-f",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
        val abc by parser.option<MutableList<String>>("-a", "-b", "-c",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
    }

    Args(parserOf("-adbefccbafed")).run {
        def shouldBe listOf("-d", "-e", "-f", "-f", "-e", "-d")
        abc shouldBe listOf("-a", "-b", "-c", "-c", "-b", "-a")
    }
})

class MixedShortOptionsWithArgsTest : Test({
    class Args(parser: ArgParser) {
        val def by parser.option<MutableList<String>>("-d", "-e", "-f",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
        val abc by parser.option<MutableList<String>>("-a", "-b", "-c",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
        val xyz by parser.option<MutableList<String>>("-x", "-y", "-z",
                argNames = oneArgName,
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName:${arguments.first()}")
            }
        }
    }

    Args(parserOf("-adecfy5", "-x0", "-bzxy")).run {
        abc shouldBe listOf("-a", "-c", "-b")
        def shouldBe listOf("-d", "-e", "-f")
        xyz shouldBe listOf("-y:5", "-x:0", "-z:xy")
    }
})

class ArglessLongOptionsTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("--xray", "--yellow", "--zebra",
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName")
            }
        }
    }

    Args(parserOf("--xray", "--yellow", "--zebra", "--zebra", "--yellow")).xyz shouldBe listOf("--xray", "--yellow", "--zebra", "--zebra", "--yellow")

    Args(parserOf("--xray", "--yellow", "--zebra")).xyz shouldBe listOf("--xray", "--yellow", "--zebra")
})

class LongOptionsWithOneArgTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("--xray", "--yellow", "--zaphod",
                argNames = oneArgName,
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply {
                add("$optionName:${arguments.first()}")
            }
        }
    }

    // Test with value as separate arg
    Args(parserOf("--xray", "0", "--yellow", "1", "--zaphod", "2", "--zaphod", "3", "--yellow", "4")).xyz shouldBe listOf("--xray:0", "--yellow:1", "--zaphod:2", "--zaphod:3", "--yellow:4")

    // Test with = between option and value
    Args(parserOf("--xray=0", "--yellow=1", "--zaphod=2", "--zaphod=3", "--yellow=4")).xyz shouldBe listOf("--xray:0", "--yellow:1", "--zaphod:2", "--zaphod:3", "--yellow:4")

    shouldThrow<UnrecognizedOptionException> {
        Args(parserOf("--xray0", "--yellow1", "--zaphod2", "--zaphod3", "--yellow4")).xyz
    }.run {
        message shouldBe "unrecognized option '--xray0'"
    }
})

class LongOptionsWithMultipleArgsTest : Test({
    class Args(parser: ArgParser) {
        val xyz by parser.option<MutableList<String>>("--xray", "--yak", "--zaphod",
                argNames = listOf("COLOR", "SIZE", "FLAVOR"),
                help = TEST_HELP) {
            value.orElse { mutableListOf<String>() }.apply { add("$optionName:$arguments") }
        }
    }

    // Test with value as separate arg
    Args(parserOf("--xray", "red", "5", "salty")).xyz shouldBe listOf("--xray:[red, 5, salty]")

    Args(parserOf("--zaphod", "green", "42", "sweet", "--yak", "blue", "7", "bitter")).xyz shouldBe listOf(
            "--zaphod:[green, 42, sweet]", "--yak:[blue, 7, bitter]")

    // Note that something that looks like an option is consumed as an argument if it appears where an argument
    // should be. This is expected behavior.
    Args(parserOf("--zaphod", "green", "42", "--yak")).xyz shouldBe listOf(
            "--zaphod:[green, 42, --yak]")

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--zaphod", "green", "42", "sweet", "--yak", "blue", "7")).xyz
    }.run {
        message shouldBe "option '--yak' is missing the required argument FLAVOR"
    }

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--zaphod", "green")).xyz
    }.run {
        message shouldBe "option '--zaphod' is missing the required argument SIZE"
    }

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--xray")).xyz
    }.run {
        message shouldBe "option '--xray' is missing the required argument COLOR"
    }
})

class DelegateProviderTest : Test({
    fun ArgParser.putting(vararg names: String, help: String) =
            option<MutableMap<String, String>>(*names,
                    argNames = listOf("KEY", "VALUE"),
                    help = help) {
                value.orElse { mutableMapOf<String, String>() }.apply {
                    put(arguments.first(), arguments.last()) }
            }

    fun ArgParser.putting(help: String) =
            ArgParser.DelegateProvider { identifier ->
                putting(identifierToOptionName(identifier), help = help) }

    class Args(parser: ArgParser) {
        val dict by parser.putting(TEST_HELP)
    }

    // Test with value as separate arg
    Args(parserOf("--dict", "red", "5")).dict shouldBe mapOf("red" to "5")

    Args(parserOf(
            "--dict", "green", "42",
            "--dict", "blue", "7"
    )).dict shouldBe mapOf(
            "green" to "42",
            "blue" to "7")

    // Note that something that looks like an option is consumed as an argument if it appears where an argument
    // should be. This is expected behavior.
    Args(parserOf("--dict", "green", "--dict")).dict shouldBe mapOf("green" to "--dict")

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--dict", "green", "42", "--dict", "blue")).dict
    }.run {
        message shouldBe "option '--dict' is missing the required argument VALUE"
    }

    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--dict")).dict
    }.run {
        message shouldBe "option '--dict' is missing the required argument KEY"
    }
})

class DefaultTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x",
                help = TEST_HELP) { toInt() }.default(5)
    }

    // Test with no value
    Args(parserOf()).x shouldBe 5

    // Test with value
    Args(parserOf("-x6")).x shouldBe 6

    // Test with value as separate arg
    Args(parserOf("-x", "7")).x shouldBe 7

    // Test with multiple values
    Args(parserOf("-x9", "-x8")).x shouldBe 8
})

class DefaultWithProviderTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing(help = TEST_HELP) { toInt() }.default(5)
    }

    // Test with no value
    Args(parserOf()).x shouldBe 5

    // Test with value
    Args(parserOf("-x6")).x shouldBe 6

    // Test with value as separate arg
    Args(parserOf("-x", "7")).x shouldBe 7

    // Test with multiple values
    Args(parserOf("-x9", "-x8")).x shouldBe 8
})

class DefaultWithLambda : Test({
    class Args(parser: ArgParser) {
        var defaultCalled = false
        val x by parser.storing(help = TEST_HELP) { toInt() }.default { defaultCalled = true; 5 }
    }

    // Test default hasn't been called
    Args(parserOf("-x6")).defaultCalled shouldBe false

    // Test with no value
    val args = Args(parserOf())
    args.x shouldBe 5
    args.defaultCalled shouldBe true
})

class FlagTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.flagging("-x", "--ecks",
                help = TEST_HELP)
        val y by parser.flagging("-y",
                help = TEST_HELP)
        val z by parser.flagging("--zed",
                help = TEST_HELP)
    }

    Args(parserOf("-x", "-y", "--zed", "--zed", "-y")).run {
        x shouldBe true
        y shouldBe true
        z shouldBe true
    }

    Args(parserOf()).run {
        x shouldBe false
        y shouldBe false
        z shouldBe false
    }

    Args(parserOf("-y", "--ecks")).run {
        x shouldBe true
        y shouldBe true
    }

    Args(parserOf("--zed")).run {
        x shouldBe false
        y shouldBe false
        z shouldBe true
    }
})

class Argument_noParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("--ecks", "-x",
                help = TEST_HELP)
    }

    Args(parserOf("-x", "foo")).x shouldBe "foo"

    Args(parserOf("-x", "bar", "-x", "baz")).x shouldBe "baz"

    Args(parserOf("--ecks", "long", "-x", "short")).x shouldBe "short"

    Args(parserOf("-x", "short", "--ecks", "long")).x shouldBe "long"

    val args = Args(parserOf())
    shouldThrow<MissingValueException> {
        args.x
    }.run {
        message shouldBe "missing ECKS"
    }
})

class Argument_missing_longTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("--ecks",
                help = TEST_HELP)
    }

    val args = Args(parserOf())
    shouldThrow<MissingValueException> {
        args.x
    }.run {
        message shouldBe "missing ECKS"
    }
})

class Argument_missing_shortTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x",
                help = TEST_HELP)
    }

    val args = Args(parserOf())
    shouldThrow<MissingValueException> {
        args.x
    }.run {
        message shouldBe "missing X"
    }
})

class Argument_withParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x", "--ecks",
                help = TEST_HELP) { toInt() }
    }

    val opts1 = Args(parserOf("-x", "5"))
    opts1.x shouldBe 5

    val opts2 = Args(parserOf("-x", "1", "-x", "2"))
    opts2.x shouldBe 2

    val opts3 = Args(parserOf("--ecks", "3", "-x", "4"))
    opts3.x shouldBe 4

    val opts4 = Args(parserOf("-x", "5", "--ecks", "6"))
    opts4.x shouldBe 6

    val opts6 = Args(parserOf())
    shouldThrow<MissingValueException> {
        opts6.x
    }.run {
        message shouldBe "missing ECKS"
    }
})

class Accumulator_noParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.adding("-x", "--ecks",
                help = TEST_HELP)
    }

    Args(parserOf()).x shouldBe listOf<String>()

    Args(parserOf("-x", "foo")).x shouldBe listOf("foo")

    Args(parserOf("-x", "bar", "-x", "baz")).x shouldBe listOf("bar", "baz")

    Args(parserOf("--ecks", "long", "-x", "short")).x shouldBe listOf("long", "short")

    Args(parserOf("-x", "short", "--ecks", "long")).x shouldBe listOf("short", "long")
})

class Accumulator_withParserTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.adding("-x", "--ecks",
                help = TEST_HELP) { toInt() }
    }

    Args(parserOf()).x shouldBe listOf<Int>()
    Args(parserOf("-x", "5")).x shouldBe listOf(5)
    Args(parserOf("-x", "1", "-x", "2")).x shouldBe listOf(1, 2)
    Args(parserOf("--ecks", "3", "-x", "4")).x shouldBe listOf(3, 4)
    Args(parserOf("-x", "5", "--ecks", "6")).x shouldBe listOf(5, 6)
})

class ColorArgs(parser: ArgParser) {
    val color by parser.mapping(
            "--red" to Color.RED,
            "--green" to Color.GREEN,
            "--blue" to Color.BLUE,
            help = TEST_HELP)
}

class MappingTest : Test({

    ColorArgs(parserOf("--red")).color shouldBe Color.RED
    ColorArgs(parserOf("--green")).color shouldBe Color.GREEN
    ColorArgs(parserOf("--blue")).color shouldBe Color.BLUE

    // Last one takes precedence
    ColorArgs(parserOf("--blue", "--red")).color shouldBe Color.RED
    ColorArgs(parserOf("--blue", "--green")).color shouldBe Color.GREEN
    ColorArgs(parserOf("--red", "--blue")).color shouldBe Color.BLUE

    val args = ColorArgs(parserOf())
    shouldThrow<MissingValueException> {
        args.color
    }.run {
        message shouldBe "missing --red|--green|--blue"
    }
})

class OptionalColorArgs(parser: ArgParser) {
    val color by parser.mapping(
            "--red" to Color.RED,
            "--green" to Color.GREEN,
            "--blue" to Color.BLUE,
            help = TEST_HELP)
            .default(Color.GREEN)
}

class Mapping_withDefaultTest : Test({
    OptionalColorArgs(parserOf("--red")).color shouldBe Color.RED
    OptionalColorArgs(parserOf("--green")).color shouldBe Color.GREEN
    OptionalColorArgs(parserOf("--blue")).color shouldBe Color.BLUE
    OptionalColorArgs(parserOf()).color shouldBe Color.GREEN
})

class UnrecognizedShortOptTest : Test({
    shouldThrow<UnrecognizedOptionException> {
        OptionalColorArgs(parserOf("-x")).color
    }.run {
        message shouldBe "unrecognized option '-x'"
    }
})

class UnrecognizedLongOptTest : Test({
    shouldThrow<UnrecognizedOptionException> {
        OptionalColorArgs(parserOf("--ecks")).color
    }.run {
        message shouldBe "unrecognized option '--ecks'"
    }
})

class StoringNoArgTest : Test({
    class Args(parser: ArgParser) {
        val x by parser.storing("-x", "--ecks",
                help = TEST_HELP)
    }

    // Note that name actually used for option is used in message
    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("-x")).x
    }.run {
        message shouldBe "option '-x' is missing a required argument"
    }

    // Note that name actually used for option is used in message
    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("--ecks")).x
    }.run {
        message shouldBe "option '--ecks' is missing a required argument"
    }
})

class ShortStoringNoArgChainedTest : Test({
    class Args(parser: ArgParser) {
        val y by parser.flagging("-y",
                help = TEST_HELP)
        val x by parser.storing("-x",
                help = TEST_HELP)
    }

    // Note that despite chaining, hyphen appears in message
    shouldThrow<OptionMissingRequiredArgumentException> {
        Args(parserOf("-yx")).x
    }.run {
        message shouldBe "option '-x' is missing a required argument"
    }
})

class InitValidationTest : Test({
    class Args(parser: ArgParser) {
        val yDelegate = parser.storing("-y",
                help = TEST_HELP) { toInt() }
        val y by yDelegate

        val xDelegate = parser.storing("-x",
                help = TEST_HELP) { toInt() }
        val x by xDelegate

        init {
            if (y >= x)
                throw InvalidArgumentException("${yDelegate.errorName} must be less than ${xDelegate.errorName}")

            // A better way to accomplish validation that only depends on one Delegate is to use
            // Delegate.addValidator. See testAddValidator for an example of this.
            if (x.rem(2) != 0)
                throw InvalidArgumentException("${xDelegate.errorName} must be even, $x is odd")
        }
    }

    // This should pass validation
    val opts0 = Args(parserOf("-y1", "-x10"))
    opts0.y shouldBe 1
    opts0.x shouldBe 10

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y20", "-x10")).x
    }.run {
        message shouldBe "Y must be less than X"
    }

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y10", "-x15")).x
    }.run {
        message shouldBe "X must be even, 15 is odd"
    }

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y10", "-x15")).x
    }.run {
        message shouldBe "X must be even, 15 is odd"
    }
})

class AddValidatorTest : Test({
    class Args(parser: ArgParser) {
        val yDelegate = parser.storing("-y",
                help = TEST_HELP) { toInt() }
        val y by yDelegate

        val xDelegate = parser.storing("-x",
                help = TEST_HELP) { toInt() }
                .addValidator {
                    if (value.rem(2) != 0)
                        throw InvalidArgumentException("$errorName must be even, $value is odd")
                }
        val x by xDelegate

        init {
            if (y >= x)
                throw InvalidArgumentException("${yDelegate.errorName} must be less than ${xDelegate.errorName}")
        }
    }

    // This should pass validation
    val opts0 = Args(parserOf("-y1", "-x10"))
    opts0.y shouldBe 1
    opts0.x shouldBe 10

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y20", "-x10")).x
    }.run {
        message shouldBe "Y must be less than X"
    }

    shouldThrow<InvalidArgumentException> {
        Args(parserOf("-y10", "-x15")).x
    }.run {
        message shouldBe "X must be even, 15 is odd"
    }
})

class UnconsumedTest : Test({
    class Args(parser: ArgParser) {
        val y by parser.flagging("-y", "--why",
                help = TEST_HELP)
        val x by parser.flagging("-x", "--ecks",
                help = TEST_HELP)
    }

    // No problem.
    Args(parserOf("-yx")).run {
        x shouldBe true
        y shouldBe true
    }

    // Attempting to give -y a parameter, "z", is treated as unrecognized option.
    shouldThrow<UnrecognizedOptionException> {
        Args(parserOf("-yz")).y
    }.run {
        message shouldBe "unrecognized option '-z'"
    }

    // Unconsumed "z" again, but note that it triggers even if we don't look at y.
    shouldThrow<UnrecognizedOptionException> {
        Args(parserOf("-yz")).x
    }.run {
        message shouldBe "unrecognized option '-z'"
    }

    // No problem again, this time with long opts.
    Args(parserOf("--why", "--ecks")).run {
        x shouldBe true
        y shouldBe true
    }

    // Attempting to give --why a parameter, "z" causes an error.
    shouldThrow<UnexpectedOptionArgumentException> {
        Args(parserOf("--why=z")).y
    }.run {
        message shouldBe "option '--why' doesn't allow an argument"
    }

    // Unconsumed "z" again, but note that it triggers even if we don't look at y.
    shouldThrow<UnexpectedOptionArgumentException> {
        Args(parserOf("--why=z")).x
    }.run {
        message shouldBe "option '--why' doesn't allow an argument"
    }
})

class Positional_basicTest : Test({
    class Args(parser: ArgParser) {
        val flag by parser.flagging("-f", "--flag",
                help = TEST_HELP)
        val store by parser.storing("-s", "--store",
                help = TEST_HELP).default("DEFAULT")
        val sources by parser.positionalList("SOURCE",
                help = TEST_HELP)
        val destination by parser.positional("DEST",
                help = TEST_HELP)
    }

    Args(parserOf("foo", "bar", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"
        sources shouldBe listOf("foo", "bar", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-f", "foo", "bar", "baz", "quux")).run {
        flag shouldBe true
        store shouldBe "DEFAULT"
        sources shouldBe listOf("foo", "bar", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-s", "foo", "bar", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-s", "foo", "bar", "-f", "baz", "quux")).run {
        flag shouldBe true
        store shouldBe "foo"
        sources shouldBe listOf("bar", "baz")
        destination shouldBe "quux"
    }

    // "--" disables option processing for all further arguments.
    // Note that "-f" is now considered a positional argument.
    Args(parserOf("-s", "foo", "--", "bar", "-f", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "-f", "baz")
        destination shouldBe "quux"
    }

    // "--" disables option processing for all further arguments.
    // Note that the second "--" is also considered a positional argument.
    Args(parserOf("-s", "foo", "--", "bar", "--", "-f", "baz", "quux")).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "--", "-f", "baz")
        destination shouldBe "quux"
    }

    Args(parserOf("-s", "foo", "bar", "-f", "baz", "quux", mode = ArgParser.Mode.POSIX)).run {
        flag shouldBe false
        store shouldBe "foo"
        sources shouldBe listOf("bar", "-f", "baz")
        destination shouldBe "quux"
    }
})

class Positional_withParserTest : Test({
    class Args(parser: ArgParser) {
        val flag by parser.flagging("-f", "--flag",
                help = TEST_HELP)
        val store by parser.storing("-s", "--store",
                help = TEST_HELP).default("DEFAULT")
        val start by parser.positionalList("START", TEST_HELP, 3..4) { toInt() }
        val end by parser.positionalList("END", TEST_HELP, 3..5) { toInt() }
    }

    shouldThrow<MissingRequiredPositionalArgumentException> {
        Args(parserOf("1", "2")).flag
    }.run {
        message shouldBe "missing START operand"
    }

    shouldThrow<MissingRequiredPositionalArgumentException> {
        Args(parserOf("1", "2", "3", "4", "5")).flag
    }.run {
        message shouldBe "missing END operand"
    }

    Args(parserOf("1", "2", "3", "4", "5", "6")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // end needs at least 3 args, so start only consumes 3
        start shouldBe listOf(1, 2, 3)
        end shouldBe listOf(4, 5, 6)
    }

    Args(parserOf("1", "2", "3", "4", "5", "6", "7")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // end only needs at 3 args, so start can consume 4
        start shouldBe listOf(1, 2, 3, 4)
        end shouldBe listOf(5, 6, 7)
    }

    Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // start can't consume more than 4, so end gets the rest.
        start shouldBe listOf(1, 2, 3, 4)
        end shouldBe listOf(5, 6, 7, 8)
    }

    Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8", "9")).run {
        flag shouldBe false
        store shouldBe "DEFAULT"

        // once again, start can't consume more than 4, so end gets the rest.
        start shouldBe listOf(1, 2, 3, 4)
        end shouldBe listOf(5, 6, 7, 8, 9)
    }

    shouldThrow<UnexpectedPositionalArgumentException> {
        Args(parserOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")).flag
    }.run {
        message shouldBe "unexpected argument after END"
    }
})

class CountingTest : Test({
    class Args(parser: ArgParser) {
        val verbosity by parser.counting("-v", "--verbose",
                help = TEST_HELP)
    }

    Args(parserOf()).run {
        verbosity shouldBe 0
    }

    Args(parserOf("-v")).run {
        verbosity shouldBe 1
    }

    Args(parserOf("-v", "-v")).run {
        verbosity shouldBe 2
    }
})

class HelpTest : Test({
    class Args(parser: ArgParser) {
        val dryRun by parser.flagging("-n", "--dry-run",
                help = "don't do anything")
        val includes by parser.adding("-I", "--include",
                help = "search in this directory for header files")
        val outDir by parser.storing("-o", "--output",
                help = "directory in which all output should be generated")
        val verbosity by parser.counting("-v", "--verbose",
                help = "increase verbosity")
        val sources by parser.positionalList("SOURCE",
                help = "source file")
        val destination by parser.positional("DEST",
                help = "destination file")
    }

    shouldThrow<ShowHelpException> {
        Args(parserOf("--help",
                helpFormatter = DefaultHelpFormatter(
                        prologue = """
                            This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam
                            malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper.
                            Maecenas eget suscipit dui, sed sodales erat. Phasellus.

                            This is the second paragraph of the prologue. I don't have anything else to say, but I'd
                            like there to be enough text that it wraps to the next line.
                            """,
                        epilogue = """
                            This is the epilogue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel tortor nunc. Sed eu massa sed turpis auctor faucibus. Donec vel pellentesque tortor. Ut ultrices tempus lectus fermentum vestibulum. Phasellus.
                            """))).dryRun
    }.run {
        val help = StringWriter().apply { printUserMessage(this, "program_name", 60) }.toString()
        help shouldBe """
usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT
                    [-v]... SOURCE... DEST


This is the prologue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Aliquam malesuada maximus eros.
Fusce luctus risus eget quam consectetur, eu auctor est
ullamcorper. Maecenas eget suscipit dui, sed sodales erat.
Phasellus.

This is the second paragraph of the prologue. I don't have
anything else to say, but I'd like there to be enough text
that it wraps to the next line.


required arguments:
  -o OUTPUT,          directory in which all output should
  --output OUTPUT     be generated


optional arguments:
  -h, --help          show this help message and exit

  -n, --dry-run       don't do anything

  -I INCLUDE,         search in this directory for header
  --include INCLUDE   files

  -v, --verbose       increase verbosity


positional arguments:
  SOURCE              source file

  DEST                destination file


This is the epilogue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Donec vel tortor nunc. Sed eu
massa sed turpis auctor faucibus. Donec vel pellentesque
tortor. Ut ultrices tempus lectus fermentum vestibulum.
Phasellus.
""".trimStart()

        val help2 = StringWriter().apply { printUserMessage(this, "a_really_long_program_name", 60) }.toString()
        help2 shouldBe """
usage: a_really_long_program_name
         [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]...
         SOURCE... DEST


This is the prologue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Aliquam malesuada maximus eros.
Fusce luctus risus eget quam consectetur, eu auctor est
ullamcorper. Maecenas eget suscipit dui, sed sodales erat.
Phasellus.

This is the second paragraph of the prologue. I don't have
anything else to say, but I'd like there to be enough text
that it wraps to the next line.


required arguments:
  -o OUTPUT,          directory in which all output should
  --output OUTPUT     be generated


optional arguments:
  -h, --help          show this help message and exit

  -n, --dry-run       don't do anything

  -I INCLUDE,         search in this directory for header
  --include INCLUDE   files

  -v, --verbose       increase verbosity


positional arguments:
  SOURCE              source file

  DEST                destination file


This is the epilogue. Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Donec vel tortor nunc. Sed eu
massa sed turpis auctor faucibus. Donec vel pellentesque
tortor. Ut ultrices tempus lectus fermentum vestibulum.
Phasellus.
""".trimStart()

        // Regression test for issue #17
        val help_wide = StringWriter().apply { printUserMessage(this, "program_name", 0) }.toString()
        help_wide shouldBe """
usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT [-v]... SOURCE... DEST


This is the prologue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada maximus eros. Fusce luctus risus eget quam consectetur, eu auctor est ullamcorper. Maecenas eget suscipit dui, sed sodales erat. Phasellus.

This is the second paragraph of the prologue. I don't have anything else to say, but I'd like there to be enough text that it wraps to the next line.


required arguments:
  -o OUTPUT, --output OUTPUT      directory in which all output should be generated


{'fraction_non_alphanumeric': 0.1460385921885351, 'fraction_numerical': 0.009725569791534525, 'mean_word_length': 2.392855434938549, 'pattern_counts': {'":': 0, '<': 241, '<?xml version=': 0, '>': 249, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '12486', 'n_tokens_mistral': 22983, 'n_tokens_neox': 22091, 'n_words': 6141}