#![deny(rust_2018_idioms)]
use conch_parser::ast::builder::*;
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_if_command_valid_with_else() {
    let guard1 = cmd("guard1");
    let guard2 = cmd("guard2");
    let guard3 = cmd("guard3");

    let body1 = cmd("body1");
    let body2 = cmd("body2");

    let els = cmd("else");

    let correct = IfFragments {
        conditionals: vec![
            GuardBodyPairGroup {
                guard: CommandGroup {
                    commands: vec![guard1, guard2],
                    trailing_comments: vec![Newline(Some("#guard_comment_a".into()))],
                },
                body: CommandGroup {
                    commands: vec![body1],
                    trailing_comments: vec![Newline(Some("#body_comment_a".into()))],
                },
            },
            GuardBodyPairGroup {
                guard: CommandGroup {
                    commands: vec![guard3],
                    trailing_comments: vec![Newline(Some("#guard_comment_b".into()))],
                },
                body: CommandGroup {
                    commands: vec![body2],
                    trailing_comments: vec![Newline(Some("#body_comment_b".into()))],
                },
            },
        ],
        else_branch: Some(CommandGroup {
            commands: vec![els],
            trailing_comments: vec![Newline(Some("#else_comment".into()))],
        }),
    };
    let mut p = make_parser(
        "\
        if guard1; guard2;
        #guard_comment_a
        then body1
        #body_comment_a
        elif guard3;
        #guard_comment_b
        then body2;
        #body_comment_b
        else else;
        #else_comment
        fi
    ",
    );
    assert_eq!(correct, p.if_command().unwrap());
}

#[test]
fn test_if_command_valid_without_else() {
    let guard1 = cmd("guard1");
    let guard2 = cmd("guard2");
    let guard3 = cmd("guard3");

    let body1 = cmd("body1");
    let body2 = cmd("body2");

    let correct = IfFragments {
        conditionals: vec![
            GuardBodyPairGroup {
                guard: CommandGroup {
                    commands: vec![guard1, guard2],
                    trailing_comments: vec![Newline(Some("#guard_comment_a".into()))],
                },
                body: CommandGroup {
                    commands: vec![body1],
                    trailing_comments: vec![Newline(Some("#body_comment_a".into()))],
                },
            },
            GuardBodyPairGroup {
                guard: CommandGroup {
                    commands: vec![guard3],
                    trailing_comments: vec![Newline(Some("#guard_comment_b".into()))],
                },
                body: CommandGroup {
                    commands: vec![body2],
                    trailing_comments: vec![Newline(Some("#body_comment_b".into()))],
                },
            },
        ],
        else_branch: None,
    };
    let mut p = make_parser(
        "\
        if guard1; guard2;
        #guard_comment_a
        then body1
        #body_comment_a
        elif guard3;
        #guard_comment_b
        then body2;
        #body_comment_b
        fi
    ",
    );
    assert_eq!(correct, p.if_command().unwrap());
}

#[test]
fn test_if_command_invalid_missing_separator() {
    let mut p = make_parser("if guard; then body1; elif guard2; then body2; else else fi");
    assert_eq!(
        Err(IncompleteCmd("if", src(0, 1, 1), "fi", src(59, 1, 60))),
        p.if_command()
    );
}

#[test]
fn test_if_command_invalid_missing_keyword() {
    let mut p = make_parser("guard1; then body1; elif guard2; then body2; else else; fi");
    assert_eq!(
        Err(Unexpected(
            Token::Name(String::from("guard1")),
            src(0, 1, 1)
        )),
        p.if_command()
    );
    let mut p = make_parser("if guard1; then body1; elif guard2; then body2; else else;");
    assert_eq!(
        Err(IncompleteCmd("if", src(0, 1, 1), "fi", src(58, 1, 59))),
        p.if_command()
    );
}

#[test]
fn test_if_command_invalid_missing_guard() {
    let mut p = make_parser("if; then body1; elif guard2; then body2; else else; fi");
    assert_eq!(Err(Unexpected(Token::Semi, src(2, 1, 3))), p.if_command());
}

#[test]
fn test_if_command_invalid_missing_body() {
    let mut p = make_parser("if guard; then; elif guard2; then body2; else else; fi");
    assert_eq!(Err(Unexpected(Token::Semi, src(14, 1, 15))), p.if_command());
    let mut p = make_parser("if guard1; then body1; elif; then body2; else else; fi");
    assert_eq!(Err(Unexpected(Token::Semi, src(27, 1, 28))), p.if_command());
    let mut p = make_parser("if guard1; then body1; elif guard2; then body2; else; fi");
    assert_eq!(Err(Unexpected(Token::Semi, src(52, 1, 53))), p.if_command());
}

#[test]
fn test_if_command_invalid_quoted() {
    let cmds = [
        (
            "'if' guard1; then body1; elif guard2; then body2; else else; fi",
            Unexpected(Token::SingleQuote, src(0, 1, 1)),
        ),
        (
            "if guard1; then body1; elif guard2; then body2; else else; 'fi'",
            IncompleteCmd("if", src(0, 1, 1), "fi", src(63, 1, 64)),
        ),
        (
            "\"if\" guard1; then body1; elif guard2; then body2; else else; fi",
            Unexpected(Token::DoubleQuote, src(0, 1, 1)),
        ),
        (
            "if guard1; then body1; elif guard2; then body2; else else; \"fi\"",
            IncompleteCmd("if", src(0, 1, 1), "fi", src(63, 1, 64)),
        ),
    ];

    for (s, e) in &cmds {
        match make_parser(s).if_command() {
            Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", s, result),
            Err(ref err) => {
                if err != e {
                    panic!(
                        "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`",
                        s, e, err
                    );
                }
            }
        }
    }
}

#[test]
fn test_if_command_invalid_concat() {
    let mut p = make_parser_from_tokens(vec![
        Token::Literal(String::from("i")),
        Token::Literal(String::from("f")),
        Token::Newline,
        Token::Literal(String::from("guard1")),
        Token::Newline,
        Token::Literal(String::from("then")),
        Token::Newline,
        Token::Literal(String::from("body1")),
        Token::Newline,
        Token::Literal(String::from("elif")),
        Token::Newline,
        Token::Literal(String::from("guard2")),
        Token::Newline,
        Token::Literal(String::from("then")),
        Token::Newline,
        Token::Literal(String::from("body2")),
        Token::Newline,
        Token::Literal(String::from("else")),
        Token::Newline,
        Token::Literal(String::from("else part")),
        Token::Newline,
        Token::Literal(String::from("fi")),
    ]);
    assert_eq!(
        Err(Unexpected(Token::Literal(String::from("i")), src(0, 1, 1))),
        p.if_command()
    );

    // Splitting up `then`, `elif`, and `else` tokens makes them
    // get interpreted as arguments, so an explicit error may not be raised
    // although the command will be malformed

    let mut p = make_parser_from_tokens(vec![
        Token::Literal(String::from("if")),
        Token::Newline,
        Token::Literal(String::from("guard1")),
        Token::Newline,
        Token::Literal(String::from("then")),
        Token::Newline,
        Token::Literal(String::from("body1")),
        Token::Newline,
        Token::Literal(String::from("elif")),
        Token::Newline,
        Token::Literal(String::from("guard2")),
        Token::Newline,
        Token::Literal(String::from("then")),
        Token::Newline,
        Token::Literal(String::from("body2")),
        Token::Newline,
        Token::Literal(String::from("else")),
        Token::Newline,
        Token::Literal(String::from("else part")),
        Token::Newline,
        Token::Literal(String::from("f")),
        Token::Literal(String::from("i")),
    ]);
    assert_eq!(
        Err(IncompleteCmd("if", src(0, 1, 1), "fi", src(61, 11, 3))),
        p.if_command()
    );
}

#[test]
fn test_if_command_should_recognize_literals_and_names() {
    for if_tok in vec![
        Token::Literal(String::from("if")),
        Token::Name(String::from("if")),
    ] {
        for then_tok in vec![
            Token::Literal(String::from("then")),
            Token::Name(String::from("then")),
        ] {
            for elif_tok in vec![
                Token::Literal(String::from("elif")),
                Token::Name(String::from("elif")),
            ] {
                for else_tok in vec![
                    Token::Literal(String::from("else")),
                    Token::Name(String::from("else")),
                ] {
                    for fi_tok in vec![
                        Token::Literal(String::from("fi")),
                        Token::Name(String::from("fi")),
                    ] {
                        let mut p = make_parser_from_tokens(vec![
                            if_tok.clone(),
                            Token::Whitespace(String::from(" ")),
                            Token::Literal(String::from("guard1")),
                            Token::Newline,
                            then_tok.clone(),
                            Token::Newline,
                            Token::Literal(String::from("body1")),
                            elif_tok.clone(),
                            Token::Whitespace(String::from(" ")),
                            Token::Literal(String::from("guard2")),
                            Token::Newline,
                            then_tok.clone(),
                            Token::Whitespace(String::from(" ")),
                            Token::Literal(String::from("body2")),
                            else_tok.clone(),
                            Token::Whitespace(String::from(" ")),
                            Token::Whitespace(String::from(" ")),
                            Token::Literal(String::from("else part")),
                            Token::Newline,
                            fi_tok,
                        ]);
                        p.if_command().unwrap();
                    }
                }
            }
        }
    }
}


#![deny(rust_2018_idioms)]
use conch_parser::lexer::Lexer;
use conch_parser::token::Token::*;
use conch_parser::token::{Positional, Token};

macro_rules! check_tok {
    ($fn_name:ident, $tok:expr) => {
        #[test]
        #[allow(non_snake_case)]
        fn $fn_name() {
            let s = format!("{}", $tok);
            let mut lex = Lexer::new(s.chars());
            assert_eq!($tok, lex.next().unwrap());
        }
    };
}

macro_rules! lex_str {
    ($fn_name:ident, $s:expr, $($tok:expr),+ ) => {
        #[test]
        #[allow(non_snake_case)]
        fn $fn_name() {
            let lex = Lexer::new($s.chars());
            let tokens: Vec<Token> = lex.collect();
            assert_eq!(tokens, vec!( $($tok),+ ));
        }
    }
}

check_tok!(check_Newline, Newline);
check_tok!(check_ParenOpen, ParenOpen);
check_tok!(check_ParenClose, ParenClose);
check_tok!(check_CurlyOpen, CurlyOpen);
check_tok!(check_CurlyClose, CurlyClose);
check_tok!(check_SquareOpen, SquareOpen);
check_tok!(check_SquareClose, SquareClose);
check_tok!(check_Dollar, Dollar);
check_tok!(check_Bang, Bang);
check_tok!(check_Semi, Semi);
check_tok!(check_Amp, Amp);
check_tok!(check_Less, Less);
check_tok!(check_Great, Great);
check_tok!(check_Pipe, Pipe);
check_tok!(check_Tilde, Tilde);
check_tok!(check_Star, Star);
check_tok!(check_Question, Question);
check_tok!(check_Percent, Percent);
check_tok!(check_Dash, Dash);
check_tok!(check_Equals, Equals);
check_tok!(check_Plus, Plus);
check_tok!(check_Colon, Colon);
check_tok!(check_At, At);
check_tok!(check_Caret, Caret);
check_tok!(check_Slash, Slash);
check_tok!(check_Comma, Comma);
check_tok!(check_Pound, Pound);
check_tok!(check_DoubleQuote, DoubleQuote);
check_tok!(check_Backtick, Backtick);
check_tok!(check_AndIf, AndIf);
check_tok!(check_OrIf, OrIf);
check_tok!(check_DSemi, DSemi);
check_tok!(check_DLess, DLess);
check_tok!(check_DGreat, DGreat);
check_tok!(check_GreatAnd, GreatAnd);
check_tok!(check_LessAnd, LessAnd);
check_tok!(check_DLessDash, DLessDash);
check_tok!(check_Clobber, Clobber);
check_tok!(check_LessGreat, LessGreat);
check_tok!(check_Whitespace, Whitespace(String::from(" \t\r")));
check_tok!(check_Name, Name(String::from("abc_23_defg")));
check_tok!(check_Literal, Literal(String::from("5abcdefg80hijklmnop")));
check_tok!(check_ParamPositional, ParamPositional(Positional::Nine));

lex_str!(check_greedy_Amp, "&&&", AndIf, Amp);
lex_str!(check_greedy_Pipe, "|||", OrIf, Pipe);
lex_str!(check_greedy_Semi, ";;;", DSemi, Semi);
lex_str!(check_greedy_Less, "<<<", DLess, Less);
lex_str!(check_greedy_Great, ">>>", DGreat, Great);
lex_str!(check_greedy_Less2, "<<<-", DLess, Less, Dash);

lex_str!(
    check_bad_Assigmnent_and_value,
    "5foobar=test",
    Literal(String::from("5foobar")),
    Equals,
    Name(String::from("test"))
);

lex_str!(
    check_Literal_and_Name_combo,
    "hello 5asdf5_ 6world __name ^.abc _test2",
    Name(String::from("hello")),
    Whitespace(String::from(" ")),
    Literal(String::from("5asdf5_")),
    Whitespace(String::from(" ")),
    Literal(String::from("6world")),
    Whitespace(String::from(" ")),
    Name(String::from("__name")),
    Whitespace(String::from(" ")),
    Caret,
    Literal(String::from(".abc")),
    Whitespace(String::from(" ")),
    Name(String::from("_test2"))
);

lex_str!(check_escape_Backslash, "\\\\", Backslash, Backslash);
lex_str!(check_escape_AndIf, "\\&&", Backslash, Amp, Amp);
lex_str!(check_escape_DSemi, "\\;;", Backslash, Semi, Semi);
lex_str!(check_escape_DLess, "\\<<", Backslash, Less, Less);
lex_str!(check_escape_DLessDash, "\\<<-", Backslash, Less, Less, Dash);
lex_str!(
    check_escape_ParamPositional,
    "\\$0",
    Backslash,
    Dollar,
    Literal(String::from("0"))
);
lex_str!(
    check_escape_Whitespace,
    "\\  ",
    Backslash,
    Whitespace(String::from(" ")),
    Whitespace(String::from(" "))
);
lex_str!(
    check_escape_Name,
    "\\ab",
    Backslash,
    Name(String::from("a")),
    Name(String::from("b"))
);
lex_str!(
    check_escape_Literal,
    "\\13",
    Backslash,
    Literal(String::from("1")),
    Literal(String::from("3"))
);

lex_str!(
    check_no_tokens_lost,
    "word\\'",
    Name(String::from("word")),
    Backslash,
    SingleQuote
);


#![deny(rust_2018_idioms)]
use conch_parser::ast::builder::*;
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_loop_command_while_valid() {
    let mut p = make_parser(
        "while guard1; guard2;\n#guard_comment\n do foo\nbar; baz\n#body_comment\n done",
    );
    let (until, GuardBodyPairGroup { guard, body }) = p.loop_command().unwrap();

    let correct_guard = CommandGroup {
        commands: vec![cmd("guard1"), cmd("guard2")],
        trailing_comments: vec![Newline(Some("#guard_comment".into()))],
    };
    let correct_body = CommandGroup {
        commands: vec![cmd("foo"), cmd("bar"), cmd("baz")],
        trailing_comments: vec![Newline(Some("#body_comment".into()))],
    };

    assert_eq!(until, LoopKind::While);
    assert_eq!(correct_guard, guard);
    assert_eq!(correct_body, body);
}

#[test]
fn test_loop_command_until_valid() {
    let mut p = make_parser(
        "until guard1; guard2;\n#guard_comment\n do foo\nbar; baz\n#body_comment\n done",
    );
    let (until, GuardBodyPairGroup { guard, body }) = p.loop_command().unwrap();

    let correct_guard = CommandGroup {
        commands: vec![cmd("guard1"), cmd("guard2")],
        trailing_comments: vec![Newline(Some("#guard_comment".into()))],
    };
    let correct_body = CommandGroup {
        commands: vec![cmd("foo"), cmd("bar"), cmd("baz")],
        trailing_comments: vec![Newline(Some("#body_comment".into()))],
    };

    assert_eq!(until, LoopKind::Until);
    assert_eq!(correct_guard, guard);
    assert_eq!(correct_body, body);
}

#[test]
fn test_loop_command_invalid_missing_separator() {
    let mut p = make_parser("while guard do foo\nbar; baz; done");
    assert_eq!(
        Err(IncompleteCmd("while", src(0, 1, 1), "do", src(33, 2, 15))),
        p.loop_command()
    );
    let mut p = make_parser("while guard; do foo\nbar; baz done");
    assert_eq!(
        Err(IncompleteCmd("do", src(13, 1, 14), "done", src(33, 2, 14))),
        p.loop_command()
    );
}

#[test]
fn test_loop_command_invalid_missing_keyword() {
    let mut p = make_parser("guard; do foo\nbar; baz; done");
    assert_eq!(
        Err(Unexpected(Token::Name(String::from("guard")), src(0, 1, 1))),
        p.loop_command()
    );
}

#[test]
fn test_loop_command_invalid_missing_guard() {
    // With command separator between loop and do keywords
    let mut p = make_parser("while; do foo\nbar; baz; done");
    assert_eq!(Err(Unexpected(Token::Semi, src(5, 1, 6))), p.loop_command());
    let mut p = make_parser("until; do foo\nbar; baz; done");
    assert_eq!(Err(Unexpected(Token::Semi, src(5, 1, 6))), p.loop_command());

    // Without command separator between loop and do keywords
    let mut p = make_parser("while do foo\nbar; baz; done");
    assert_eq!(
        Err(Unexpected(Token::Name(String::from("do")), src(6, 1, 7))),
        p.loop_command()
    );
    let mut p = make_parser("until do foo\nbar; baz; done");
    assert_eq!(
        Err(Unexpected(Token::Name(String::from("do")), src(6, 1, 7))),
        p.loop_command()
    );
}

#[test]
fn test_loop_command_invalid_quoted() {
    let cmds = [
        (
            "'while' guard do foo\nbar; baz; done",
            Unexpected(Token::SingleQuote, src(0, 1, 1)),
        ),
        (
            "'until' guard do foo\nbar; baz; done",
            Unexpected(Token::SingleQuote, src(0, 1, 1)),
        ),
        (
            "\"while\" guard do foo\nbar; baz; done",
            Unexpected(Token::DoubleQuote, src(0, 1, 1)),
        ),
        (
            "\"until\" guard do foo\nbar; baz; done",
            Unexpected(Token::DoubleQuote, src(0, 1, 1)),
        ),
    ];

    for (c, e) in &cmds {
        match make_parser(c).loop_command() {
            Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result),
            Err(ref err) => {
                if err != e {
                    panic!(
                        "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`",
                        c, e, err
                    );
                }
            }
        }
    }
}

#[test]
fn test_loop_command_invalid_concat() {
    let mut p = make_parser_from_tokens(vec![
        Token::Literal(String::from("whi")),
        Token::Literal(String::from("le")),
        Token::Newline,
        Token::Literal(String::from("guard")),
        Token::Newline,
        Token::Literal(String::from("do")),
        Token::Literal(String::from("foo")),
        Token::Newline,
        Token::Literal(String::from("done")),
    ]);
    assert_eq!(
        Err(Unexpected(
            Token::Literal(String::from("whi")),
            src(0, 1, 1)
        )),
        p.loop_command()
    );
    let mut p = make_parser_from_tokens(vec![
        Token::Literal(String::from("un")),
        Token::Literal(String::from("til")),
        Token::Newline,
        Token::Literal(String::from("guard")),
        Token::Newline,
        Token::Literal(String::from("do")),
        Token::Literal(String::from("foo")),
        Token::Newline,
        Token::Literal(String::from("done")),
    ]);
    assert_eq!(
        Err(Unexpected(Token::Literal(String::from("un")), src(0, 1, 1))),
        p.loop_command()
    );
}

#[test]
fn test_loop_command_should_recognize_literals_and_names() {
    for kw in vec![
        Token::Literal(String::from("while")),
        Token::Name(String::from("while")),
        Token::Literal(String::from("until")),
        Token::Name(String::from("until")),
    ] {
        let mut p = make_parser_from_tokens(vec![
            kw,
            Token::Newline,
            Token::Literal(String::from("guard")),
            Token::Newline,
            Token::Literal(String::from("do")),
            Token::Newline,
            Token::Literal(String::from("foo")),
            Token::Newline,
            Token::Literal(String::from("done")),
        ]);
        p.loop_command().unwrap();
    }
}


#![deny(rust_2018_idioms)]
use conch_parser::ast::Parameter::*;
use conch_parser::ast::ParameterSubstitution::*;
use conch_parser::parse::ParseError::*;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_parameter_short() {
    let words = vec![At, Star, Pound, Question, Dash, Dollar, Bang, Positional(3)];

    let mut p = make_parser("$@$*$#$?$-$$$!$3$");
    for param in words {
        assert_eq!(p.parameter().unwrap(), word_param(param));
    }

    assert_eq!(word("$"), p.parameter().unwrap());
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_short_in_curlies() {
    let words = vec![
        At,
        Star,
        Pound,
        Question,
        Dash,
        Dollar,
        Bang,
        Var(String::from("foo")),
        Positional(3),
        Positional(1000),
    ];

    let mut p = make_parser("${@}${*}${#}${?}${-}${$}${!}${foo}${3}${1000}");
    for param in words {
        assert_eq!(p.parameter().unwrap(), word_param(param));
    }

    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_command_substitution() {
    let correct = word_subst(Command(vec![
        cmd_args("echo", &["hello"]),
        cmd_args("echo", &["world"]),
    ]));

    assert_eq!(
        correct,
        make_parser("$(echo hello; echo world)")
            .parameter()
            .unwrap()
    );
}

#[test]
fn test_parameter_command_substitution_valid_empty_substitution() {
    let correct = word_subst(Command(vec![]));
    assert_eq!(correct, make_parser("$()").parameter().unwrap());
    assert_eq!(correct, make_parser("$(     )").parameter().unwrap());
    assert_eq!(correct, make_parser("$(\n\n)").parameter().unwrap());
}

#[test]
fn test_parameter_literal_dollar_if_no_param() {
    let mut p = make_parser("$%asdf");
    assert_eq!(word("$"), p.parameter().unwrap());
    assert_eq!(p.word().unwrap().unwrap(), word("%asdf"));
}


#![deny(rust_2018_idioms)]
#![recursion_limit = "128"]

use conch_parser::ast::builder::*;
use conch_parser::parse::*;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_parser_should_yield_none_after_error() {
    let mut iter = make_parser("foo && ||").into_iter();
    let _ = iter.next().expect("failed to get error").unwrap_err();
    assert_eq!(iter.next(), None);
}

#[test]
fn test_linebreak_valid_with_comments_and_whitespace() {
    let mut p = make_parser("\n\t\t\t\n # comment1\n#comment2\n   \n");
    assert_eq!(
        p.linebreak(),
        vec!(
            Newline(None),
            Newline(None),
            Newline(Some(String::from("# comment1"))),
            Newline(Some(String::from("#comment2"))),
            Newline(None)
        )
    );
}

#[test]
fn test_linebreak_valid_empty() {
    let mut p = make_parser("");
    assert_eq!(p.linebreak(), vec!());
}

#[test]
fn test_linebreak_valid_nonnewline() {
    let mut p = make_parser("hello world");
    assert_eq!(p.linebreak(), vec!());
}

#[test]
fn test_linebreak_valid_eof_instead_of_newline() {
    let mut p = make_parser("#comment");
    assert_eq!(p.linebreak(), vec!(Newline(Some(String::from("#comment")))));
}

#[test]
fn test_linebreak_single_quote_insiginificant() {
    let mut p = make_parser("#unclosed quote ' comment");
    assert_eq!(
        p.linebreak(),
        vec!(Newline(Some(String::from("#unclosed quote ' comment"))))
    );
}

#[test]
fn test_linebreak_double_quote_insiginificant() {
    let mut p = make_parser("#unclosed quote \" comment");
    assert_eq!(
        p.linebreak(),
        vec!(Newline(Some(String::from("#unclosed quote \" comment"))))
    );
}

#[test]
fn test_linebreak_escaping_newline_insignificant() {
    let mut p = make_parser("#comment escapes newline\\\n");
    assert_eq!(
        p.linebreak(),
        vec!(Newline(Some(String::from("#comment escapes newline\\"))))
    );
}

#[test]
fn test_skip_whitespace_preserve_newline() {
    let mut p = make_parser("    \t\t \t \t\n   ");
    p.skip_whitespace();
    assert_eq!(p.linebreak().len(), 1);
}

#[test]
fn test_skip_whitespace_preserve_comments() {
    let mut p = make_parser("    \t\t \t \t#comment\n   ");
    p.skip_whitespace();
    assert_eq!(
        p.linebreak().pop().unwrap(),
        Newline(Some(String::from("#comment")))
    );
}

#[test]
fn test_skip_whitespace_comments_capture_all_up_to_newline() {
    let mut p = make_parser("#comment&&||;;()<<-\n");
    assert_eq!(
        p.linebreak().pop().unwrap(),
        Newline(Some(String::from("#comment&&||;;()<<-")))
    );
}

#[test]
fn test_skip_whitespace_comments_may_end_with_eof() {
    let mut p = make_parser("#comment");
    assert_eq!(
        p.linebreak().pop().unwrap(),
        Newline(Some(String::from("#comment")))
    );
}

#[test]
fn test_skip_whitespace_skip_escapes_dont_affect_newlines() {
    let mut p = make_parser("  \t \\\n  \\\n#comment\n");
    p.skip_whitespace();
    assert_eq!(
        p.linebreak().pop().unwrap(),
        Newline(Some(String::from("#comment")))
    );
}

#[test]
fn test_skip_whitespace_skips_escaped_newlines() {
    let mut p = make_parser("\\\n\\\n   \\\n");
    p.skip_whitespace();
    assert_eq!(p.linebreak(), vec!());
}

#[test]
fn test_comment_cannot_start_mid_whitespace_delimited_word() {
    let mut p = make_parser("hello#world");
    let w = p.word().unwrap().expect("no valid word was discovered");
    assert_eq!(w, word("hello#world"));
}

#[test]
fn test_comment_can_start_if_whitespace_before_pound() {
    let mut p = make_parser("hello #world");
    p.word().unwrap().expect("no valid word was discovered");
    let comment = p.linebreak();
    assert_eq!(comment, vec!(Newline(Some(String::from("#world")))));
}

#[test]
fn test_braces_literal_unless_brace_group_expected() {
    let source = "echo {} } {";
    let mut p = make_parser(source);
    assert_eq!(p.word().unwrap().unwrap(), word("echo"));
    assert_eq!(p.word().unwrap().unwrap(), word("{}"));
    assert_eq!(p.word().unwrap().unwrap(), word("}"));
    assert_eq!(p.word().unwrap().unwrap(), word("{"));

    let correct = Some(cmd_args("echo", &["{}", "}", "{"]));
    assert_eq!(correct, make_parser(source).complete_command().unwrap());
}

#[test]
fn ensure_parse_errors_are_send_and_sync() {
    fn send_and_sync<T: Send + Sync>() {}
    send_and_sync::<ParseError<()>>();
}

#[test]
fn ensure_parser_could_be_send_and_sync() {
    use conch_parser::token::Token;

    fn send_and_sync<T: Send + Sync>() {}
    send_and_sync::<Parser<std::vec::IntoIter<Token>, ArcBuilder>>();
}


// Certain helpers may only be used by specific tests,
// suppress dead_code warnings since the compiler can't
// see our intent
#![allow(dead_code)]

use conch_parser::ast::Command::*;
use conch_parser::ast::ComplexWord::*;
use conch_parser::ast::PipeableCommand::*;
use conch_parser::ast::SimpleWord::*;
use conch_parser::ast::*;
use conch_parser::lexer::Lexer;
use conch_parser::parse::*;
use conch_parser::token::Token;

pub fn lit(s: &str) -> DefaultWord {
    Word::Simple(Literal(String::from(s)))
}

pub fn escaped(s: &str) -> DefaultWord {
    Word::Simple(Escaped(String::from(s)))
}

pub fn subst(s: DefaultParameterSubstitution) -> DefaultWord {
    Word::Simple(Subst(Box::new(s)))
}

pub fn single_quoted(s: &str) -> TopLevelWord<String> {
    TopLevelWord(Single(Word::SingleQuoted(String::from(s))))
}

pub fn double_quoted(s: &str) -> TopLevelWord<String> {
    TopLevelWord(Single(Word::DoubleQuoted(vec![Literal(String::from(s))])))
}

pub fn word(s: &str) -> TopLevelWord<String> {
    TopLevelWord(Single(lit(s)))
}

pub fn word_escaped(s: &str) -> TopLevelWord<String> {
    TopLevelWord(Single(escaped(s)))
}

pub fn word_subst(s: DefaultParameterSubstitution) -> TopLevelWord<String> {
    TopLevelWord(Single(subst(s)))
}

pub fn word_param(p: DefaultParameter) -> TopLevelWord<String> {
    TopLevelWord(Single(Word::Simple(Param(p))))
}

pub fn make_parser(src: &str) -> DefaultParser<Lexer<std::str::Chars<'_>>> {
    DefaultParser::new(Lexer::new(src.chars()))
}

pub fn make_parser_from_tokens(src: Vec<Token>) -> DefaultParser<std::vec::IntoIter<Token>> {
    DefaultParser::new(src.into_iter())
}

pub fn cmd_args_simple(cmd: &str, args: &[&str]) -> Box<DefaultSimpleCommand> {
    let mut cmd_args = Vec::with_capacity(args.len() + 1);
    cmd_args.push(RedirectOrCmdWord::CmdWord(word(cmd)));
    cmd_args.extend(args.iter().map(|&a| RedirectOrCmdWord::CmdWord(word(a))));

    Box::new(SimpleCommand {
        redirects_or_env_vars: vec![],
        redirects_or_cmd_words: cmd_args,
    })
}

pub fn cmd_simple(cmd: &str) -> Box<DefaultSimpleCommand> {
    cmd_args_simple(cmd, &[])
}

pub fn cmd_args(cmd: &str, args: &[&str]) -> TopLevelCommand<String> {
    TopLevelCommand(List(CommandList {
        first: ListableCommand::Single(Simple(cmd_args_simple(cmd, args))),
        rest: vec![],
    }))
}

pub fn cmd(cmd: &str) -> TopLevelCommand<String> {
    cmd_args(cmd, &[])
}

pub fn cmd_from_simple(cmd: DefaultSimpleCommand) -> TopLevelCommand<String> {
    TopLevelCommand(List(CommandList {
        first: ListableCommand::Single(Simple(Box::new(cmd))),
        rest: vec![],
    }))
}

pub fn src(byte: usize, line: usize, col: usize) -> SourcePos {
    SourcePos { byte, line, col }
}


#![deny(rust_2018_idioms)]
use conch_parser::ast::PipeableCommand::*;
use conch_parser::ast::*;
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_pipeline_valid_bang() {
    let mut p = make_parser("! foo | bar | baz");
    let correct = CommandList {
        first: ListableCommand::Pipe(
            true,
            vec![
                Simple(cmd_simple("foo")),
                Simple(cmd_simple("bar")),
                Simple(cmd_simple("baz")),
            ],
        ),
        rest: vec![],
    };
    assert_eq!(correct, p.and_or_list().unwrap());
}

#[test]
fn test_pipeline_valid_bangs_in_and_or() {
    let mut p = make_parser("! foo | bar || ! baz && ! foobar");
    let correct = CommandList {
        first: ListableCommand::Pipe(
            true,
            vec![Simple(cmd_simple("foo")), Simple(cmd_simple("bar"))],
        ),
        rest: vec![
            AndOr::Or(ListableCommand::Pipe(true, vec![Simple(cmd_simple("baz"))])),
            AndOr::And(ListableCommand::Pipe(
                true,
                vec![Simple(cmd_simple("foobar"))],
            )),
        ],
    };
    assert_eq!(correct, p.and_or_list().unwrap());
}

#[test]
fn test_pipeline_no_bang_single_cmd_optimize_wrapper_out() {
    let mut p = make_parser("foo");
    let parse = p.pipeline().unwrap();
    if let ListableCommand::Pipe(..) = parse {
        panic!("Parser::pipeline should not create a wrapper if no ! present and only a single command");
    }
}

#[test]
fn test_pipeline_invalid_multiple_bangs_in_same_pipeline() {
    let mut p = make_parser("! foo | bar | ! baz");
    assert_eq!(Err(Unexpected(Token::Bang, src(14, 1, 15))), p.pipeline());
}


#![deny(rust_2018_idioms)]
use conch_parser::token::Positional;

#[test]
fn test_positional_conversions() {
    for i in 0..10u8 {
        let positional =
            Positional::from_num(i).unwrap_or_else(|| panic!("failed to convert {}", i));
        assert_eq!(positional.as_num(), i);
    }

    assert_eq!(Positional::from_num(10), None);
}


#![deny(rust_2018_idioms)]
use conch_parser::ast::ComplexWord::*;
use conch_parser::ast::PipeableCommand::*;
use conch_parser::ast::SimpleWord::*;
use conch_parser::ast::*;
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use crate::parse_support::*;

fn simple_command_with_redirect(cmd: &str, redirect: DefaultRedirect) -> DefaultPipeableCommand {
    Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word(cmd)),
            RedirectOrCmdWord::Redirect(redirect),
        ],
    }))
}

#[test]
fn test_redirect_valid_close_without_whitespace() {
    let mut p = make_parser(">&-");
    assert_eq!(
        Some(Ok(Redirect::DupWrite(None, word("-")))),
        p.redirect().unwrap()
    );
}

#[test]
fn test_redirect_valid_close_with_whitespace() {
    let mut p = make_parser("<&       -");
    assert_eq!(
        Some(Ok(Redirect::DupRead(None, word("-")))),
        p.redirect().unwrap()
    );
}

#[test]
fn test_redirect_valid_start_with_dash_if_not_dup() {
    let path = word("-test");
    let cases = vec![
        ("4<-test", Redirect::Read(Some(4), path.clone())),
        ("4>-test", Redirect::Write(Some(4), path.clone())),
        ("4<>-test", Redirect::ReadWrite(Some(4), path.clone())),
        ("4>>-test", Redirect::Append(Some(4), path.clone())),
        ("4>|-test", Redirect::Clobber(Some(4), path)),
    ];

    for (s, correct) in cases.into_iter() {
        match make_parser(s).redirect() {
            Ok(Some(Ok(ref r))) if *r == correct => {}
            Ok(r) => panic!(
                "Unexpectedly parsed the source \"{}\" as\n{:?} instead of\n{:?}",
                s, r, correct
            ),
            Err(err) => panic!("Failed to parse the source \"{}\": {}", s, err),
        }
    }
}

#[test]
fn test_redirect_valid_return_word_if_no_redirect() {
    let mut p = make_parser("hello");
    assert_eq!(Some(Err(word("hello"))), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_return_word_if_src_fd_is_definitely_non_numeric() {
    let mut p = make_parser("123$$'foo'>out");
    let correct = TopLevelWord(Concat(vec![
        lit("123"),
        Word::Simple(Param(Parameter::Dollar)),
        Word::SingleQuoted(String::from("foo")),
    ]));
    assert_eq!(Some(Err(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_return_word_if_src_fd_has_escaped_numerics() {
    let mut p = make_parser("\\2>");
    let correct = word_escaped("2");
    assert_eq!(Some(Err(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_dst_fd_can_have_escaped_numerics() {
    let mut p = make_parser(">\\2");
    let correct = Redirect::Write(None, word_escaped("2"));
    assert_eq!(Some(Ok(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_invalid_dup_if_dst_fd_is_definitely_non_numeric() {
    assert_eq!(
        Err(BadFd(src(2, 1, 3), src(12, 1, 13))),
        make_parser(">&123$$'foo'").redirect()
    );
}

#[test]
fn test_redirect_valid_dup_return_redirect_if_dst_fd_is_possibly_numeric() {
    let mut p = make_parser(">&123$$$(echo 2)`echo bar`");
    let correct = Redirect::DupWrite(
        None,
        TopLevelWord(Concat(vec![
            lit("123"),
            Word::Simple(Param(Parameter::Dollar)),
            subst(ParameterSubstitution::Command(vec![cmd_args(
                "echo",
                &["2"],
            )])),
            subst(ParameterSubstitution::Command(vec![cmd_args(
                "echo",
                &["bar"],
            )])),
        ])),
    );
    assert_eq!(Some(Ok(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_invalid_close_without_whitespace() {
    assert_eq!(
        Err(BadFd(src(2, 1, 3), src(7, 1, 8))),
        make_parser(">&-asdf").redirect()
    );
}

#[test]
fn test_redirect_invalid_close_with_whitespace() {
    assert_eq!(
        Err(BadFd(src(9, 1, 10), src(14, 1, 15))),
        make_parser("<&       -asdf").redirect()
    );
}

#[test]
fn test_redirect_fd_immediately_preceeding_redirection() {
    let mut p = make_parser("foo 1>>out");
    let cmd = p.simple_command().unwrap();
    assert_eq!(
        cmd,
        simple_command_with_redirect("foo", Redirect::Append(Some(1), word("out")))
    );
}

#[test]
fn test_redirect_fd_must_immediately_preceed_redirection() {
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("1")),
            RedirectOrCmdWord::Redirect(Redirect::ReadWrite(None, word("out"))),
        ],
    }));

    let mut p = make_parser("foo 1 <>out");
    assert_eq!(p.simple_command().unwrap(), correct);
}

#[test]
fn test_redirect_valid_dup_with_fd() {
    let mut p = make_parser("foo 1>&2");
    let cmd = p.simple_command().unwrap();
    assert_eq!(
        cmd,
        simple_command_with_redirect("foo", Redirect::DupWrite(Some(1), word("2")))
    );
}

#[test]
fn test_redirect_valid_dup_without_fd() {
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("1")),
            RedirectOrCmdWord::Redirect(Redirect::DupRead(None, word("2"))),
        ],
    }));

    let mut p = make_parser("foo 1 <&2");
    assert_eq!(p.simple_command().unwrap(), correct);
}

#[test]
fn test_redirect_valid_dup_with_whitespace() {
    let mut p = make_parser("foo 1<& 2");
    let cmd = p.simple_command().unwrap();
    assert_eq!(
        cmd,
        simple_command_with_redirect("foo", Redirect::DupRead(Some(1), word("2")))
    );
}

#[test]
fn test_redirect_valid_single_quoted_dup_fd() {
    let correct = Redirect::DupWrite(Some(1), single_quoted("2"));
    assert_eq!(Some(Ok(correct)), make_parser("1>&'2'").redirect().unwrap());
}

#[test]
fn test_redirect_valid_double_quoted_dup_fd() {
    let correct = Redirect::DupWrite(None, double_quoted("2"));
    assert_eq!(
        Some(Ok(correct)),
        make_parser(">&\"2\"").redirect().unwrap()
    );
}

#[test]
fn test_redirect_src_fd_need_not_be_single_token() {
    let mut p = make_parser_from_tokens(vec![
        Token::Literal(String::from("foo")),
        Token::Whitespace(String::from(" ")),
        Token::Literal(String::from("12")),
        Token::Literal(String::from("34")),
        Token::LessAnd,
        Token::Dash,
    ]);

    let cmd = p.simple_command().unwrap();
    assert_eq!(
        cmd,
        simple_command_with_redirect("foo", Redirect::DupRead(Some(1234), word("-")))
    );
}

#[test]
fn test_redirect_dst_fd_need_not_be_single_token() {
    let mut p = make_parser_from_tokens(vec![
        Token::GreatAnd,
        Token::Literal(String::from("12")),
        Token::Literal(String::from("34")),
    ]);

    let correct = Redirect::DupWrite(None, word("1234"));
    assert_eq!(Some(Ok(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_accept_literal_and_name_tokens() {
    let mut p = make_parser_from_tokens(vec![Token::GreatAnd, Token::Literal(String::from("12"))]);
    assert_eq!(
        Some(Ok(Redirect::DupWrite(None, word("12")))),
        p.redirect().unwrap()
    );

    let mut p = make_parser_from_tokens(vec![Token::GreatAnd, Token::Name(String::from("12"))]);
    assert_eq!(
        Some(Ok(Redirect::DupWrite(None, word("12")))),
        p.redirect().unwrap()
    );
}

#[test]
fn test_redirect_list_valid() {
    let mut p = make_parser("1>>out <& 2 2>&-");
    let io = p.redirect_list().unwrap();
    assert_eq!(
        io,
        vec!(
            Redirect::Append(Some(1), word("out")),
            Redirect::DupRead(None, word("2")),
            Redirect::DupWrite(Some(2), word("-")),
        )
    );
}

#[test]
fn test_redirect_list_rejects_non_fd_words() {
    assert_eq!(
        Err(BadFd(src(16, 1, 17), src(19, 1, 20))),
        make_parser("1>>out <in 2>&- abc").redirect_list()
    );
    assert_eq!(
        Err(BadFd(src(7, 1, 8), src(10, 1, 11))),
        make_parser("1>>out abc<in 2>&-").redirect_list()
    );
    assert_eq!(
        Err(BadFd(src(7, 1, 8), src(10, 1, 11))),
        make_parser("1>>out abc <in 2>&-").redirect_list()
    );
}


#![deny(rust_2018_idioms)]
use conch_parser::ast::PipeableCommand::*;
use conch_parser::ast::Redirect::*;
use conch_parser::ast::*;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_simple_command_valid_assignments_at_start_of_command() {
    let mut p = make_parser("var=val ENV=true BLANK= foo bar baz");
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![
            RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))),
            RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))),
            RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None),
        ],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("bar")),
            RedirectOrCmdWord::CmdWord(word("baz")),
        ],
    }));
    assert_eq!(correct, p.simple_command().unwrap());
}

#[test]
fn test_simple_command_assignments_after_start_of_command_should_be_args() {
    let mut p = make_parser("var=val ENV=true BLANK= foo var2=val2 bar baz var3=val3");
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![
            RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))),
            RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))),
            RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None),
        ],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("var2=val2")),
            RedirectOrCmdWord::CmdWord(word("bar")),
            RedirectOrCmdWord::CmdWord(word("baz")),
            RedirectOrCmdWord::CmdWord(word("var3=val3")),
        ],
    }));
    assert_eq!(correct, p.simple_command().unwrap());
}

#[test]
fn test_simple_command_redirections_at_start_of_command() {
    let mut p = make_parser("2>|clob 3<>rw <in var=val ENV=true BLANK= foo bar baz");
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![
            RedirectOrEnvVar::Redirect(Clobber(Some(2), word("clob"))),
            RedirectOrEnvVar::Redirect(ReadWrite(Some(3), word("rw"))),
            RedirectOrEnvVar::Redirect(Read(None, word("in"))),
            RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))),
            RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))),
            RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None),
        ],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("bar")),
            RedirectOrCmdWord::CmdWord(word("baz")),
        ],
    }));
    assert_eq!(correct, p.simple_command().unwrap());
}

#[test]
fn test_simple_command_redirections_at_end_of_command() {
    let mut p = make_parser("var=val ENV=true BLANK= foo bar baz 2>|clob 3<>rw <in");
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![
            RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))),
            RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))),
            RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None),
        ],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("bar")),
            RedirectOrCmdWord::CmdWord(word("baz")),
            RedirectOrCmdWord::Redirect(Clobber(Some(2), word("clob"))),
            RedirectOrCmdWord::Redirect(ReadWrite(Some(3), word("rw"))),
            RedirectOrCmdWord::Redirect(Read(None, word("in"))),
        ],
    }));
    assert_eq!(correct, p.simple_command().unwrap());
}

#[test]
fn test_simple_command_redirections_throughout_the_command() {
    let mut p = make_parser("2>|clob var=val 3<>rw ENV=true BLANK= foo bar <in baz 4>&-");
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec![
            RedirectOrEnvVar::Redirect(Clobber(Some(2), word("clob"))),
            RedirectOrEnvVar::EnvVar("var".to_owned(), Some(word("val"))),
            RedirectOrEnvVar::Redirect(ReadWrite(Some(3), word("rw"))),
            RedirectOrEnvVar::EnvVar("ENV".to_owned(), Some(word("true"))),
            RedirectOrEnvVar::EnvVar("BLANK".to_owned(), None),
        ],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("bar")),
            RedirectOrCmdWord::Redirect(Read(None, word("in"))),
            RedirectOrCmdWord::CmdWord(word("baz")),
            RedirectOrCmdWord::Redirect(DupWrite(Some(4), word("-"))),
        ],
    }));

    assert_eq!(correct, p.simple_command().unwrap());
}


#![deny(rust_2018_idioms)]
use conch_parser::ast::builder::*;
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_subshell_valid() {
    let mut p = make_parser("( foo\nbar; baz\n#comment\n )");
    let correct = CommandGroup {
        commands: vec![cmd("foo"), cmd("bar"), cmd("baz")],
        trailing_comments: vec![Newline(Some("#comment".into()))],
    };
    assert_eq!(correct, p.subshell().unwrap());
}

#[test]
fn test_subshell_valid_separator_not_needed() {
    let correct = CommandGroup {
        commands: vec![cmd("foo")],
        trailing_comments: vec![],
    };
    assert_eq!(correct, make_parser("( foo )").subshell().unwrap());

    let correct_with_comment = CommandGroup {
        commands: vec![cmd("foo")],
        trailing_comments: vec![Newline(Some("#comment".into()))],
    };
    assert_eq!(
        correct_with_comment,
        make_parser("( foo\n#comment\n )").subshell().unwrap()
    );
}

#[test]
fn test_subshell_space_between_parens_not_needed() {
    let mut p = make_parser("(foo )");
    p.subshell().unwrap();
    let mut p = make_parser("( foo)");
    p.subshell().unwrap();
    let mut p = make_parser("(foo)");
    p.subshell().unwrap();
}

#[test]
fn test_subshell_invalid_missing_keyword() {
    assert_eq!(
        Err(Unmatched(Token::ParenOpen, src(0, 1, 1))),
        make_parser("( foo\nbar; baz").subshell()
    );
    assert_eq!(
        Err(Unexpected(Token::Name(String::from("foo")), src(0, 1, 1))),
        make_parser("foo\nbar; baz; )").subshell()
    );
}

#[test]
fn test_subshell_invalid_quoted() {
    let cmds = [
        (
            "'(' foo\nbar; baz; )",
            Unexpected(Token::SingleQuote, src(0, 1, 1)),
        ),
        (
            "( foo\nbar; baz; ')'",
            Unmatched(Token::ParenOpen, src(0, 1, 1)),
        ),
        (
            "\"(\" foo\nbar; baz; )",
            Unexpected(Token::DoubleQuote, src(0, 1, 1)),
        ),
        (
            "( foo\nbar; baz; \")\"",
            Unmatched(Token::ParenOpen, src(0, 1, 1)),
        ),
    ];

    for (c, e) in &cmds {
        match make_parser(c).subshell() {
            Ok(result) => panic!("Unexpectedly parsed \"{}\" as\n{:#?}", c, result),
            Err(ref err) => {
                if err != e {
                    panic!(
                        "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`",
                        c, e, err
                    );
                }
            }
        }
    }
}

#[test]
fn test_subshell_invalid_missing_body() {
    assert_eq!(
        Err(Unexpected(Token::ParenClose, src(2, 2, 1))),
        make_parser("(\n)").subshell()
    );
    assert_eq!(
        Err(Unexpected(Token::ParenClose, src(1, 1, 2))),
        make_parser("()").subshell()
    );
}


#![deny(rust_2018_idioms)]
use conch_parser::ast::ComplexWord::*;
use conch_parser::ast::Parameter::*;
use conch_parser::ast::ParameterSubstitution::*;
use conch_parser::ast::{RedirectOrCmdWord, SimpleCommand, SimpleWord, TopLevelWord, Word};
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_parameter_substitution() {
    let words = vec![
        Len(At),
        Len(Star),
        Len(Pound),
        Len(Question),
        Len(Dash),
        Len(Dollar),
        Len(Bang),
        Len(Var(String::from("foo"))),
        Len(Positional(3)),
        Len(Positional(1000)),
        Command(vec![cmd_args("echo", &["foo"])]),
    ];

    let mut p = make_parser("${#@}${#*}${##}${#?}${#-}${#$}${#!}${#foo}${#3}${#1000}$(echo foo)");
    for param in words {
        let correct = word_subst(param);
        assert_eq!(correct, p.parameter().unwrap());
    }

    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_smallest_suffix() {
    let word = word("foo");

    let substs = vec![
        RemoveSmallestSuffix(At, Some(word.clone())),
        RemoveSmallestSuffix(Star, Some(word.clone())),
        RemoveSmallestSuffix(Pound, Some(word.clone())),
        RemoveSmallestSuffix(Question, Some(word.clone())),
        RemoveSmallestSuffix(Dash, Some(word.clone())),
        RemoveSmallestSuffix(Dollar, Some(word.clone())),
        RemoveSmallestSuffix(Bang, Some(word.clone())),
        RemoveSmallestSuffix(Positional(0), Some(word.clone())),
        RemoveSmallestSuffix(Positional(10), Some(word.clone())),
        RemoveSmallestSuffix(Positional(100), Some(word.clone())),
        RemoveSmallestSuffix(Var(String::from("foo_bar123")), Some(word)),
        RemoveSmallestSuffix(At, None),
        RemoveSmallestSuffix(Star, None),
        RemoveSmallestSuffix(Pound, None),
        RemoveSmallestSuffix(Question, None),
        RemoveSmallestSuffix(Dash, None),
        RemoveSmallestSuffix(Dollar, None),
        RemoveSmallestSuffix(Bang, None),
        RemoveSmallestSuffix(Positional(0), None),
        RemoveSmallestSuffix(Positional(10), None),
        RemoveSmallestSuffix(Positional(100), None),
        RemoveSmallestSuffix(Var(String::from("foo_bar123")), None),
    ];

    let src = "${@%foo}${*%foo}${#%foo}${?%foo}${-%foo}${$%foo}${!%foo}${0%foo}${10%foo}${100%foo}${foo_bar123%foo}${@%}${*%}${#%}${?%}${-%}${$%}${!%}${0%}${10%}${100%}${foo_bar123%}";
    let mut p = make_parser(src);

    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }

    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_largest_suffix() {
    let word = word("foo");

    let substs = vec![
        RemoveLargestSuffix(At, Some(word.clone())),
        RemoveLargestSuffix(Star, Some(word.clone())),
        RemoveLargestSuffix(Pound, Some(word.clone())),
        RemoveLargestSuffix(Question, Some(word.clone())),
        RemoveLargestSuffix(Dash, Some(word.clone())),
        RemoveLargestSuffix(Dollar, Some(word.clone())),
        RemoveLargestSuffix(Bang, Some(word.clone())),
        RemoveLargestSuffix(Positional(0), Some(word.clone())),
        RemoveLargestSuffix(Positional(10), Some(word.clone())),
        RemoveLargestSuffix(Positional(100), Some(word.clone())),
        RemoveLargestSuffix(Var(String::from("foo_bar123")), Some(word)),
        RemoveLargestSuffix(At, None),
        RemoveLargestSuffix(Star, None),
        RemoveLargestSuffix(Pound, None),
        RemoveLargestSuffix(Question, None),
        RemoveLargestSuffix(Dash, None),
        RemoveLargestSuffix(Dollar, None),
        RemoveLargestSuffix(Bang, None),
        RemoveLargestSuffix(Positional(0), None),
        RemoveLargestSuffix(Positional(10), None),
        RemoveLargestSuffix(Positional(100), None),
        RemoveLargestSuffix(Var(String::from("foo_bar123")), None),
    ];

    let src = "${@%%foo}${*%%foo}${#%%foo}${?%%foo}${-%%foo}${$%%foo}${!%%foo}${0%%foo}${10%%foo}${100%%foo}${foo_bar123%%foo}${@%%}${*%%}${#%%}${?%%}${-%%}${$%%}${!%%}${0%%}${10%%}${100%%}${foo_bar123%%}";
    let mut p = make_parser(src);

    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }

    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_smallest_prefix() {
    let word = word("foo");

    let substs = vec![
        RemoveSmallestPrefix(At, Some(word.clone())),
        RemoveSmallestPrefix(Star, Some(word.clone())),
        RemoveSmallestPrefix(Pound, Some(word.clone())),
        RemoveSmallestPrefix(Question, Some(word.clone())),
        RemoveSmallestPrefix(Dash, Some(word.clone())),
        RemoveSmallestPrefix(Dollar, Some(word.clone())),
        RemoveSmallestPrefix(Bang, Some(word.clone())),
        RemoveSmallestPrefix(Positional(0), Some(word.clone())),
        RemoveSmallestPrefix(Positional(10), Some(word.clone())),
        RemoveSmallestPrefix(Positional(100), Some(word.clone())),
        RemoveSmallestPrefix(Var(String::from("foo_bar123")), Some(word)),
        RemoveSmallestPrefix(At, None),
        RemoveSmallestPrefix(Star, None),
        //RemoveSmallestPrefix(Pound, None), // ${##} should parse as Len(#)
        RemoveSmallestPrefix(Question, None),
        RemoveSmallestPrefix(Dash, None),
        RemoveSmallestPrefix(Dollar, None),
        RemoveSmallestPrefix(Bang, None),
        RemoveSmallestPrefix(Positional(0), None),
        RemoveSmallestPrefix(Positional(10), None),
        RemoveSmallestPrefix(Positional(100), None),
        RemoveSmallestPrefix(Var(String::from("foo_bar123")), None),
    ];

    let src = "${@#foo}${*#foo}${##foo}${?#foo}${-#foo}${$#foo}${!#foo}${0#foo}${10#foo}${100#foo}${foo_bar123#foo}${@#}${*#}${?#}${-#}${$#}${!#}${0#}${10#}${100#}${foo_bar123#}";
    let mut p = make_parser(src);

    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }

    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_largest_prefix() {
    let word = word("foo");

    let substs = vec![
        RemoveLargestPrefix(At, Some(word.clone())),
        RemoveLargestPrefix(Star, Some(word.clone())),
        RemoveLargestPrefix(Pound, Some(word.clone())),
        RemoveLargestPrefix(Question, Some(word.clone())),
        RemoveLargestPrefix(Dash, Some(word.clone())),
        RemoveLargestPrefix(Dollar, Some(word.clone())),
        RemoveLargestPrefix(Bang, Some(word.clone())),
        RemoveLargestPrefix(Positional(0), Some(word.clone())),
        RemoveLargestPrefix(Positional(10), Some(word.clone())),
        RemoveLargestPrefix(Positional(100), Some(word.clone())),
        RemoveLargestPrefix(Var(String::from("foo_bar123")), Some(word)),
        RemoveLargestPrefix(At, None),
        RemoveLargestPrefix(Star, None),
        RemoveLargestPrefix(Pound, None),
        RemoveLargestPrefix(Question, None),
        RemoveLargestPrefix(Dash, None),
        RemoveLargestPrefix(Dollar, None),
        RemoveLargestPrefix(Bang, None),
        RemoveLargestPrefix(Positional(0), None),
        RemoveLargestPrefix(Positional(10), None),
        RemoveLargestPrefix(Positional(100), None),
        RemoveLargestPrefix(Var(String::from("foo_bar123")), None),
    ];

    let src = "${@##foo}${*##foo}${###foo}${?##foo}${-##foo}${$##foo}${!##foo}${0##foo}${10##foo}${100##foo}${foo_bar123##foo}${@##}${*##}${###}${?##}${-##}${$##}${!##}${0##}${10##}${100##}${foo_bar123##}";
    let mut p = make_parser(src);

    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }

    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_default() {
    let word = word("foo");

    let substs = vec![
        Default(true, At, Some(word.clone())),
        Default(true, Star, Some(word.clone())),
        Default(true, Pound, Some(word.clone())),
        Default(true, Question, Some(word.clone())),
        Default(true, Dash, Some(word.clone())),
        Default(true, Dollar, Some(word.clone())),
        Default(true, Bang, Some(word.clone())),
        Default(true, Positional(0), Some(word.clone())),
        Default(true, Positional(10), Some(word.clone())),
        Default(true, Positional(100), Some(word.clone())),
        Default(true, Var(String::from("foo_bar123")), Some(word.clone())),
        Default(true, At, None),
        Default(true, Star, None),
        Default(true, Pound, None),
        Default(true, Question, None),
        Default(true, Dash, None),
        Default(true, Dollar, None),
        Default(true, Bang, None),
        Default(true, Positional(0), None),
        Default(true, Positional(10), None),
        Default(true, Positional(100), None),
        Default(true, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@:-foo}${*:-foo}${#:-foo}${?:-foo}${-:-foo}${$:-foo}${!:-foo}${0:-foo}${10:-foo}${100:-foo}${foo_bar123:-foo}${@:-}${*:-}${#:-}${?:-}${-:-}${$:-}${!:-}${0:-}${10:-}${100:-}${foo_bar123:-}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted

    let substs = vec![
        Default(false, At, Some(word.clone())),
        Default(false, Star, Some(word.clone())),
        Default(false, Pound, Some(word.clone())),
        Default(false, Question, Some(word.clone())),
        Default(false, Dash, Some(word.clone())),
        Default(false, Dollar, Some(word.clone())),
        Default(false, Bang, Some(word.clone())),
        Default(false, Positional(0), Some(word.clone())),
        Default(false, Positional(10), Some(word.clone())),
        Default(false, Positional(100), Some(word.clone())),
        Default(false, Var(String::from("foo_bar123")), Some(word)),
        Default(false, At, None),
        Default(false, Star, None),
        //Default(false, Pound, None), // ${#-} should be a length check of the `-` parameter
        Default(false, Question, None),
        Default(false, Dash, None),
        Default(false, Dollar, None),
        Default(false, Bang, None),
        Default(false, Positional(0), None),
        Default(false, Positional(10), None),
        Default(false, Positional(100), None),
        Default(false, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@-foo}${*-foo}${#-foo}${?-foo}${--foo}${$-foo}${!-foo}${0-foo}${10-foo}${100-foo}${foo_bar123-foo}${@-}${*-}${?-}${--}${$-}${!-}${0-}${10-}${100-}${foo_bar123-}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_error() {
    let word = word("foo");

    let substs = vec![
        Error(true, At, Some(word.clone())),
        Error(true, Star, Some(word.clone())),
        Error(true, Pound, Some(word.clone())),
        Error(true, Question, Some(word.clone())),
        Error(true, Dash, Some(word.clone())),
        Error(true, Dollar, Some(word.clone())),
        Error(true, Bang, Some(word.clone())),
        Error(true, Positional(0), Some(word.clone())),
        Error(true, Positional(10), Some(word.clone())),
        Error(true, Positional(100), Some(word.clone())),
        Error(true, Var(String::from("foo_bar123")), Some(word.clone())),
        Error(true, At, None),
        Error(true, Star, None),
        Error(true, Pound, None),
        Error(true, Question, None),
        Error(true, Dash, None),
        Error(true, Dollar, None),
        Error(true, Bang, None),
        Error(true, Positional(0), None),
        Error(true, Positional(10), None),
        Error(true, Positional(100), None),
        Error(true, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@:?foo}${*:?foo}${#:?foo}${?:?foo}${-:?foo}${$:?foo}${!:?foo}${0:?foo}${10:?foo}${100:?foo}${foo_bar123:?foo}${@:?}${*:?}${#:?}${?:?}${-:?}${$:?}${!:?}${0:?}${10:?}${100:?}${foo_bar123:?}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted

    let substs = vec![
        Error(false, At, Some(word.clone())),
        Error(false, Star, Some(word.clone())),
        Error(false, Pound, Some(word.clone())),
        Error(false, Question, Some(word.clone())),
        Error(false, Dash, Some(word.clone())),
        Error(false, Dollar, Some(word.clone())),
        Error(false, Bang, Some(word.clone())),
        Error(false, Positional(0), Some(word.clone())),
        Error(false, Positional(10), Some(word.clone())),
        Error(false, Positional(100), Some(word.clone())),
        Error(false, Var(String::from("foo_bar123")), Some(word)),
        Error(false, At, None),
        Error(false, Star, None),
        //Error(false, Pound, None), // ${#?} should be a length check of the `?` parameter
        Error(false, Question, None),
        Error(false, Dash, None),
        Error(false, Dollar, None),
        Error(false, Bang, None),
        Error(false, Positional(0), None),
        Error(false, Positional(10), None),
        Error(false, Positional(100), None),
        Error(false, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@?foo}${*?foo}${#?foo}${??foo}${-?foo}${$?foo}${!?foo}${0?foo}${10?foo}${100?foo}${foo_bar123?foo}${@?}${*?}${??}${-?}${$?}${!?}${0?}${10?}${100?}${foo_bar123?}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_assign() {
    let word = word("foo");

    let substs = vec![
        Assign(true, At, Some(word.clone())),
        Assign(true, Star, Some(word.clone())),
        Assign(true, Pound, Some(word.clone())),
        Assign(true, Question, Some(word.clone())),
        Assign(true, Dash, Some(word.clone())),
        Assign(true, Dollar, Some(word.clone())),
        Assign(true, Bang, Some(word.clone())),
        Assign(true, Positional(0), Some(word.clone())),
        Assign(true, Positional(10), Some(word.clone())),
        Assign(true, Positional(100), Some(word.clone())),
        Assign(true, Var(String::from("foo_bar123")), Some(word.clone())),
        Assign(true, At, None),
        Assign(true, Star, None),
        Assign(true, Pound, None),
        Assign(true, Question, None),
        Assign(true, Dash, None),
        Assign(true, Dollar, None),
        Assign(true, Bang, None),
        Assign(true, Positional(0), None),
        Assign(true, Positional(10), None),
        Assign(true, Positional(100), None),
        Assign(true, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@:=foo}${*:=foo}${#:=foo}${?:=foo}${-:=foo}${$:=foo}${!:=foo}${0:=foo}${10:=foo}${100:=foo}${foo_bar123:=foo}${@:=}${*:=}${#:=}${?:=}${-:=}${$:=}${!:=}${0:=}${10:=}${100:=}${foo_bar123:=}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted

    let substs = vec![
        Assign(false, At, Some(word.clone())),
        Assign(false, Star, Some(word.clone())),
        Assign(false, Pound, Some(word.clone())),
        Assign(false, Question, Some(word.clone())),
        Assign(false, Dash, Some(word.clone())),
        Assign(false, Dollar, Some(word.clone())),
        Assign(false, Bang, Some(word.clone())),
        Assign(false, Positional(0), Some(word.clone())),
        Assign(false, Positional(10), Some(word.clone())),
        Assign(false, Positional(100), Some(word.clone())),
        Assign(false, Var(String::from("foo_bar123")), Some(word)),
        Assign(false, At, None),
        Assign(false, Star, None),
        Assign(false, Pound, None),
        Assign(false, Question, None),
        Assign(false, Dash, None),
        Assign(false, Dollar, None),
        Assign(false, Bang, None),
        Assign(false, Positional(0), None),
        Assign(false, Positional(10), None),
        Assign(false, Positional(100), None),
        Assign(false, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@=foo}${*=foo}${#=foo}${?=foo}${-=foo}${$=foo}${!=foo}${0=foo}${10=foo}${100=foo}${foo_bar123=foo}${@=}${*=}${#=}${?=}${-=}${$=}${!=}${0=}${10=}${100=}${foo_bar123=}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_alternative() {
    let word = word("foo");

    let substs = vec![
        Alternative(true, At, Some(word.clone())),
        Alternative(true, Star, Some(word.clone())),
        Alternative(true, Pound, Some(word.clone())),
        Alternative(true, Question, Some(word.clone())),
        Alternative(true, Dash, Some(word.clone())),
        Alternative(true, Dollar, Some(word.clone())),
        Alternative(true, Bang, Some(word.clone())),
        Alternative(true, Positional(0), Some(word.clone())),
        Alternative(true, Positional(10), Some(word.clone())),
        Alternative(true, Positional(100), Some(word.clone())),
        Alternative(true, Var(String::from("foo_bar123")), Some(word.clone())),
        Alternative(true, At, None),
        Alternative(true, Star, None),
        Alternative(true, Pound, None),
        Alternative(true, Question, None),
        Alternative(true, Dash, None),
        Alternative(true, Dollar, None),
        Alternative(true, Bang, None),
        Alternative(true, Positional(0), None),
        Alternative(true, Positional(10), None),
        Alternative(true, Positional(100), None),
        Alternative(true, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@:+foo}${*:+foo}${#:+foo}${?:+foo}${-:+foo}${$:+foo}${!:+foo}${0:+foo}${10:+foo}${100:+foo}${foo_bar123:+foo}${@:+}${*:+}${#:+}${?:+}${-:+}${$:+}${!:+}${0:+}${10:+}${100:+}${foo_bar123:+}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted

    let substs = vec![
        Alternative(false, At, Some(word.clone())),
        Alternative(false, Star, Some(word.clone())),
        Alternative(false, Pound, Some(word.clone())),
        Alternative(false, Question, Some(word.clone())),
        Alternative(false, Dash, Some(word.clone())),
        Alternative(false, Dollar, Some(word.clone())),
        Alternative(false, Bang, Some(word.clone())),
        Alternative(false, Positional(0), Some(word.clone())),
        Alternative(false, Positional(10), Some(word.clone())),
        Alternative(false, Positional(100), Some(word.clone())),
        Alternative(false, Var(String::from("foo_bar123")), Some(word)),
        Alternative(false, At, None),
        Alternative(false, Star, None),
        Alternative(false, Pound, None),
        Alternative(false, Question, None),
        Alternative(false, Dash, None),
        Alternative(false, Dollar, None),
        Alternative(false, Bang, None),
        Alternative(false, Positional(0), None),
        Alternative(false, Positional(10), None),
        Alternative(false, Positional(100), None),
        Alternative(false, Var(String::from("foo_bar123")), None),
    ];

    let src = "${@+foo}${*+foo}${#+foo}${?+foo}${-+foo}${$+foo}${!+foo}${0+foo}${10+foo}${100+foo}${foo_bar123+foo}${@+}${*+}${#+}${?+}${-+}${$+}${!+}${0+}${10+}${100+}${foo_bar123+}";
    let mut p = make_parser(src);
    for s in substs {
        assert_eq!(word_subst(s), p.parameter().unwrap());
    }
    assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
}

#[test]
fn test_parameter_substitution_words_can_have_spaces_and_escaped_curlies() {
    let var = Var(String::from("foo_bar123"));
    let word = TopLevelWord(Concat(vec![
        lit("foo{"),
        escaped("}"),
        lit(" \t\r "),
        escaped("\n"),
        lit("bar \t\r "),
    ]));

    let substs = vec![
        RemoveSmallestSuffix(var.clone(), Some(word.clone())),
        RemoveLargestSuffix(var.clone(), Some(word.clone())),
        RemoveSmallestPrefix(var.clone(), Some(word.clone())),
        RemoveLargestPrefix(var.clone(), Some(word.clone())),
        Default(true, var.clone(), Some(word.clone())),
        Default(false, var.clone(), Some(word.clone())),
        Assign(true, var.clone(), Some(word.clone())),
        Assign(false, var.clone(), Some(word.clone())),
        Error(true, var.clone(), Some(word.clone())),
        Error(false, var.clone(), Some(word.clone())),
        Alternative(true, var.clone(), Some(word.clone())),
        Alternative(false, var, Some(word)),
    ];

    let src = vec![
        "%", "%%", "#", "##", ":-", "-", ":=", "=", ":?", "?", ":+", "+",
    ];

    for (i, s) in substs.into_iter().enumerate() {
        let src = format!("${{foo_bar123{}foo{{\\}} \t\r \\\nbar \t\r }}", src[i]);
        let mut p = make_parser(&src);
        assert_eq!(word_subst(s), p.parameter().unwrap());
        assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
    }
}

#[test]
fn test_parameter_substitution_words_can_start_with_pound() {
    let var = Var(String::from("foo_bar123"));
    let word = TopLevelWord(Concat(vec![
        lit("#foo{"),
        escaped("}"),
        lit(" \t\r "),
        escaped("\n"),
        lit("bar \t\r "),
    ]));

    let substs = vec![
        RemoveSmallestSuffix(var.clone(), Some(word.clone())),
        RemoveLargestSuffix(var.clone(), Some(word.clone())),
        //RemoveSmallestPrefix(var.clone(), Some(word.clone())),
        RemoveLargestPrefix(var.clone(), Some(word.clone())),
        Default(true, var.clone(), Some(word.clone())),
        Default(false, var.clone(), Some(word.clone())),
        Assign(true, var.clone(), Some(word.clone())),
        Assign(false, var.clone(), Some(word.clone())),
        Error(true, var.clone(), Some(word.clone())),
        Error(false, var.clone(), Some(word.clone())),
        Alternative(true, var.clone(), Some(word.clone())),
        Alternative(false, var, Some(word)),
    ];

    let src = vec![
        "%", "%%", //"#", // Let's not confuse the pound in the word with a substitution
        "##", ":-", "-", ":=", "=", ":?", "?", ":+", "+",
    ];

    for (i, s) in substs.into_iter().enumerate() {
        let src = format!("${{foo_bar123{}#foo{{\\}} \t\r \\\nbar \t\r }}", src[i]);
        let mut p = make_parser(&src);
        assert_eq!(word_subst(s), p.parameter().unwrap());
        assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
    }
}

#[test]
fn test_parameter_substitution_words_can_be_parameters_or_substitutions_as_well() {
    let var = Var(String::from("foo_bar123"));
    let word = TopLevelWord(Concat(vec![
        Word::Simple(SimpleWord::Param(At)),
        subst(RemoveLargestPrefix(
            Var(String::from("foo")),
            Some(word("bar")),
        )),
    ]));

    let substs = vec![
        RemoveSmallestSuffix(var.clone(), Some(word.clone())),
        RemoveLargestSuffix(var.clone(), Some(word.clone())),
        RemoveSmallestPrefix(var.clone(), Some(word.clone())),
        RemoveLargestPrefix(var.clone(), Some(word.clone())),
        Default(true, var.clone(), Some(word.clone())),
        Default(false, var.clone(), Some(word.clone())),
        Assign(true, var.clone(), Some(word.clone())),
        Assign(false, var.clone(), Some(word.clone())),
        Error(true, var.clone(), Some(word.clone())),
        Error(false, var.clone(), Some(word.clone())),
        Alternative(true, var.clone(), Some(word.clone())),
        Alternative(false, var, Some(word)),
    ];

    let src = vec![
        "%", "%%", "#", "##", ":-", "-", ":=", "=", ":?", "?", ":+", "+",
    ];

    for (i, s) in substs.into_iter().enumerate() {
        let src = format!("${{foo_bar123{}$@${{foo##bar}}}}", src[i]);
        let mut p = make_parser(&src);
        assert_eq!(word_subst(s), p.parameter().unwrap());
        assert_eq!(Err(UnexpectedEOF), p.parameter()); // Stream should be exhausted
    }
}

#[test]
fn test_parameter_substitution_command_close_paren_need_not_be_followed_by_word_delimeter() {
    let correct = Some(cmd_from_simple(SimpleCommand {
        redirects_or_env_vars: vec![],
        redirects_or_cmd_words: vec![
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(TopLevelWord(Single(Word::DoubleQuoted(vec![
                SimpleWord::Subst(Box::new(Command(vec![cmd("bar")]))),
            ])))),
        ],
    }));

    assert_eq!(
        correct,
        make_parser("foo \"$(bar)\"").complete_command().unwrap()
    );
}

#[test]
fn test_parameter_substitution_invalid() {
    let cases = vec![
        ("$(( x", UnexpectedEOF),
        ("${foo", Unmatched(Token::CurlyOpen, src(1, 1, 2))),
        (
            "${ foo}",
            BadSubst(Token::Whitespace(String::from(" ")), src(2, 1, 3)),
        ),
        (
            "${foo }",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo -}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo =}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo ?}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo +}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo :-}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo :=}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo :?}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo :+}",
            BadSubst(Token::Whitespace(String::from(" ")), src(5, 1, 6)),
        ),
        (
            "${foo: -}",
            BadSubst(Token::Whitespace(String::from(" ")), src(6, 1, 7)),
        ),
        (
            "${foo: =}",
            BadSubst(Token::Whitespace(String::from(" ")), src(6, 1, 7)),
        ),
        (
            "${foo: ?}",
            BadSubst(Token::Whitespace(String::from(" ")), src(6, 1, 7)),
        ),
        (
            "${foo: +}",
            BadSubst(Token::Whitespace(String::from(" ")), src(6, 1, 7)),
        ),
        (
            "${foo: %}",
            BadSubst(Token::Whitespace(String::from(" ")), src(6, 1, 7)),
        ),
        (
            "${foo: #}",
            BadSubst(Token::Whitespace(String::from(" ")), src(6, 1, 7)),
        ),
        ("${foo-bar", Unmatched(Token::CurlyOpen, src(1, 1, 2))),
        ("${'foo'}", BadSubst(Token::SingleQuote, src(2, 1, 3))),
        ("${\"foo\"}", BadSubst(Token::DoubleQuote, src(2, 1, 3))),
        ("${`foo`}", BadSubst(Token::Backtick, src(2, 1, 3))),
    ];

    for (s, correct) in cases.into_iter() {
        match make_parser(s).parameter() {
            Ok(w) => panic!("Unexpectedly parsed the source \"{}\" as\n{:?}", s, w),
            Err(ref err) => {
                if err != &correct {
                    panic!(
                        "Expected the source \"{}\" to return the error `{:?}`, but got `{:?}`",
                        s, correct, err
                    );
                }
            }
        }
    }
}

#[test]
fn test_parameter_substitution_nested_quoted() {
    let param = Var("foo".to_owned());
    let cases = vec![
        (
            "${foo:+'bar'}",
            Alternative(true, param.clone(), Some(single_quoted("bar"))),
        ),
        (
            "${foo:+\"bar\"}",
            Alternative(true, param.clone(), Some(double_quoted("bar"))),
        ),
        (
            "${foo:+`bar`}",
            Alternative(true, param, Some(word_subst(Command(vec![cmd("bar")])))),
        ),
    ];

    for (src, subst) in cases.into_iter() {
        let correct = word_subst(subst);
        let parsed = make_parser(src).parameter();
        if parsed.as_ref() != Ok(&correct) {
            panic!(
                "Expected \"{}\" to parse as `{:?}`, but got `{:?}`",
                src, correct, parsed
            );
        }
    }
}

#[test]
fn test_parameter_substitution_can_have_nested_substitution_and_parameter() {
    let param_foo = Var("foo".to_owned());
    let param_bar = Var("bar".to_owned());
    let correct = word_subst(Alternative(
        true,
        param_foo,
        Some(word_subst(Alternative(
            true,
            param_bar,
            Some(word_param(Dollar)),
        ))),
    ));

    let mut p = make_parser("${foo:+${bar:+$$}}");
    assert_eq!(Ok(correct), p.parameter());
}

#[test]
fn test_parameter_substitution_special_tokens_in_words_become_literals() {
    let correct = word_subst(Default(
        true,
        Var("foo".to_owned()),
        Some(TopLevelWord(Concat(vec![
            lit("#(bar);&|&&||;; << >> <& >& <<- "),
            escaped("\n"),
            lit("\n\t"),
        ]))),
    ));

    let mut p = make_parser("${foo:-#(bar);&|&&||;; << >> <& >& <<- \\\n\n\t}");
    assert_eq!(Ok(correct), p.parameter());
}


#![deny(rust_2018_idioms)]
use conch_parser::ast::ComplexWord::*;
use conch_parser::ast::SimpleWord::*;
use conch_parser::ast::*;
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use crate::parse_support::*;

#[test]
fn test_word_single_quote_valid() {
    let correct = single_quoted("abc&&||\n\n#comment\nabc");
    assert_eq!(
        Some(correct),
        make_parser("'abc&&||\n\n#comment\nabc'").word().unwrap()
    );
}

#[test]
fn test_word_single_quote_valid_slash_remains_literal() {
    let correct = single_quoted("\\\n");
    assert_eq!(Some(correct), make_parser("'\\\n'").word().unwrap());
}

#[test]
fn test_word_single_quote_valid_does_not_quote_single_quotes() {
    let correct = single_quoted("hello \\");
    assert_eq!(Some(correct), make_parser("'hello \\'").word().unwrap());
}

#[test]
fn test_word_single_quote_invalid_missing_close_quote() {
    assert_eq!(
        Err(Unmatched(Token::SingleQuote, src(0, 1, 1))),
        make_parser("'hello").word()
    );
}

#[test]
fn test_word_double_quote_valid() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![Literal(String::from(
        "abc&&||\n\n#comment\nabc",
    ))])));
    assert_eq!(
        Some(correct),
        make_parser("\"abc&&||\n\n#comment\nabc\"").word().unwrap()
    );
}

#[test]
fn test_word_double_quote_valid_recognizes_parameters() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("test asdf")),
        Param(Parameter::Var(String::from("foo"))),
        Literal(String::from(" $")),
    ])));

    assert_eq!(
        Some(correct),
        make_parser("\"test asdf$foo $\"").word().unwrap()
    );
}

#[test]
fn test_word_double_quote_valid_recognizes_backticks() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("test asdf ")),
        Subst(Box::new(ParameterSubstitution::Command(vec![cmd("foo")]))),
    ])));

    assert_eq!(
        Some(correct),
        make_parser("\"test asdf `foo`\"").word().unwrap()
    );
}

#[test]
fn test_word_double_quote_valid_slash_escapes_dollar() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("test")),
        Escaped(String::from("$")),
        Literal(String::from("foo ")),
        Param(Parameter::At),
    ])));

    assert_eq!(
        Some(correct),
        make_parser("\"test\\$foo $@\"").word().unwrap()
    );
}

#[test]
fn test_word_double_quote_valid_slash_escapes_backtick() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("test")),
        Escaped(String::from("`")),
        Literal(String::from(" ")),
        Param(Parameter::Star),
    ])));

    assert_eq!(Some(correct), make_parser("\"test\\` $*\"").word().unwrap());
}

#[test]
fn test_word_double_quote_valid_slash_escapes_double_quote() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("test")),
        Escaped(String::from("\"")),
        Literal(String::from(" ")),
        Param(Parameter::Pound),
    ])));

    assert_eq!(
        Some(correct),
        make_parser("\"test\\\" $#\"").word().unwrap()
    );
}

#[test]
fn test_word_double_quote_valid_slash_escapes_newline() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("test")),
        Escaped(String::from("\n")),
        Literal(String::from(" ")),
        Param(Parameter::Question),
        Literal(String::from("\n")),
    ])));

    assert_eq!(
        Some(correct),
        make_parser("\"test\\\n $?\n\"").word().unwrap()
    );
}

#[test]
fn test_word_double_quote_valid_slash_escapes_slash() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("test")),
        Escaped(String::from("\\")),
        Literal(String::from(" ")),
        Param(Parameter::Positional(0)),
    ])));

    assert_eq!(
        Some(correct),
        make_parser("\"test\\\\ $0\"").word().unwrap()
    );
}

#[test]
fn test_word_double_quote_valid_slash_remains_literal_in_general_case() {
    let correct = TopLevelWord(Single(Word::DoubleQuoted(vec![
        Literal(String::from("t\\est ")),
        Param(Parameter::Dollar),
    ])));

    assert_eq!(Some(correct), make_parser("\"t\\est $$\"").word().unwrap());
}

#[test]
fn test_word_double_quote_slash_invalid_missing_close_quote() {
    assert_eq!(
        Err(Unmatched(Token::DoubleQuote, src(0, 1, 1))),
        make_parser("\"hello").word()
    );
    assert_eq!(
        Err(Unmatched(Token::DoubleQuote, src(0, 1, 1))),
        make_parser("\"hello\\\"").word()
    );
}

#[test]
fn test_word_delegate_parameters() {
    let params = [
        "$@", "$*", "$#", "$?", "$-", "$$", "$!", "$3", "${@}", "${*}", "${#}", "${?}", "${-}",
        "${$}", "${!}", "${foo}", "${3}", "${1000}",
    ];

    for p in &params {
        match make_parser(p).word() {
            Ok(Some(TopLevelWord(Single(Word::Simple(w))))) => {
                if let Param(_) = w {
                } else {
                    panic!(
                        "Unexpectedly parsed \"{}\" as a non-parameter word:\n{:#?}",
                        p, w
                    );
                }
            }
            Ok(Some(w)) => panic!(
                "Unexpectedly parsed \"{}\" as a non-parameter word:\n{:#?}",
                p, w
            ),
            Ok(None) => panic!("Did not parse \"{}\" as a parameter", p),
            Err(e) => panic!("Did not parse \"{}\" as a parameter: {}", p, e),
        }
    }
}

#[test]
fn test_word_literal_dollar_if_not_param() {
    let correct = word("$%asdf");
    assert_eq!(correct, make_parser("$%asdf").word().unwrap().unwrap());
}

#[test]
fn test_word_does_not_capture_comments() {
    assert_eq!(Ok(None), make_parser("#comment\n").word());
    assert_eq!(Ok(None), make_parser("  #comment\n").word());
    let mut p = make_parser("word   #comment\n");
    p.word().unwrap().unwrap();
    assert_eq!(Ok(None), p.word());
}

#[test]
fn test_word_pound_in_middle_is_not_comment() {
    let correct = word("abc#def");
    assert_eq!(Ok(Some(correct)), make_parser("abc#def\n").word());
}

#[test]
fn test_word_tokens_which_become_literal_words() {
    let words = ["{", "}", "!", "name", "1notname"];

    for w in &words {
        match make_parser(w).word() {
            Ok(Some(res)) => {
                let correct = word(*w);
                if correct != res {
                    panic!(
                        "Unexpectedly parsed \"{}\": expected:\n{:#?}\ngot:\n{:#?}",
                        w, correct, res
                    );
                }
            }
            Ok(None) => panic!("Did not parse \"{}\" as a word", w),
            Err(e) => panic!("Did not parse \"{}\" as a word: {}", w, e),
        }
    }
}

#[test]
fn test_word_concatenation_works() {
    let correct = TopLevelWord(Concat(vec![
        lit("foo=bar"),
        Word::DoubleQuoted(vec![Literal(String::from("double"))]),
        Word::SingleQuoted(String::from("single")),
    ]));

    assert_eq!(
        Ok(Some(correct)),
        make_parser("foo=bar\"double\"'single'").word()
    );
}

#[test]
fn test_word_special_words_recognized_as_such() {
    assert_eq!(
        Ok(Some(TopLevelWord(Single(Word::Simple(Star))))),
        make_parser("*").word()
    );
    assert_eq!(
        Ok(Some(TopLevelWord(Single(Word::Simple(Question))))),
        make_parser("?").word()
    );
    assert_eq!(
        Ok(Some(TopLevelWord(Single(Word::Simple(Tilde))))),
        make_parser("~").word()
    );
    assert_eq!(
        Ok(Some(TopLevelWord(Single(Word::Simple(SquareOpen))))),
        make_parser("[").word()
    );
    assert_eq!(
        Ok(Some(TopLevelWord(Single(Word::Simple(SquareClose))))),
        make_parser("]").word()
    );
    assert_eq!(
        Ok(Some(TopLevelWord(Single(Word::Simple(Colon))))),
        make_parser(":").word()
    );
}

#[test]
fn test_word_backslash_makes_things_literal() {
    let lit = ["a", "&", ";", "(", "*", "?", "$"];

    for l in &lit {
        let src = format!("\\{}", l);
        match make_parser(&src).word() {
            Ok(Some(res)) => {
                let correct = word_escaped(l);
                if correct != res {
                    panic!(
                        "Unexpectedly parsed \"{}\": expected:\n{:#?}\ngot:\n{:#?}",
                        src, correct, res
                    );
                }
            }
            Ok(None) => panic!("Did not parse \"{}\" as a word", src),
            Err(e) => panic!("Did not parse \"{}\" as a word: {}", src, e),
        }
    }
}

#[test]
fn test_word_escaped_newline_becomes_whitespace() {
    let mut p = make_parser("foo\\\nbar");
    assert_eq!(Ok(Some(word("foo"))), p.word());
    assert_eq!(Ok(Some(word("bar"))), p.word());
}


//! Actions.

use std::sync::{Arc, Mutex};

use crate::error::Error;
use crate::event::{Event, EventSender};

/// Actions that can be performed on the pager.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum Action {
    /// Quit the pager.
    Quit,

    /// Refresh the screen.
    Refresh,

    /// Show the help screen.
    Help,

    /// Cancel the current action.
    Cancel,

    /// Switch to the previous file.
    PreviousFile,

    /// Switch to the next file.
    NextFile,

    /// Toggle visiblity of the ruler.
    ToggleRuler,

    /// Scroll up *n* lines.
    ScrollUpLines(usize),

    /// Scroll down *n* lines.
    ScrollDownLines(usize),

    /// Scroll up 1/*n* of the screen height.
    ScrollUpScreenFraction(usize),

    /// Scroll down 1/*n* of the screen height.
    ScrollDownScreenFraction(usize),

    /// Scroll to the top of the file.
    ScrollToTop,

    /// Scroll to the bottom of the file, and start following it.
    ScrollToBottom,

    /// Scroll left *n* columns.
    ScrollLeftColumns(usize),

    /// Scroll right *n* columns.
    ScrollRightColumns(usize),

    /// Scroll left 1/*n* of the screen width.
    ScrollLeftScreenFraction(usize),

    /// Scroll right 1/*n* of the screen width.
    ScrollRightScreenFraction(usize),

    /// Toggle display of line numbers.
    ToggleLineNumbers,

    /// Toggle line wrapping mode.
    ToggleLineWrapping,

    /// Prompt the user for a line to move to.
    PromptGoToLine,

    /// Prompt the user for a search term.  The search will start at the beginning of the file.
    PromptSearchFromStart,

    /// Prompt the user for a search term.  The search will start at the top of the screen.
    PromptSearchForwards,

    /// Prompt the user for a search term.  The search will start from the bottom of the screen and
    /// proceed backwards.
    PromptSearchBackwards,

    /// Move to the previous match.
    PreviousMatch,

    /// Move to the next match.
    NextMatch,

    /// Move the previous line that contains a match.
    PreviousMatchLine,

    /// Move to the next line that contains a match.
    NextMatchLine,

    /// Move to the previous match, follow the current screen.
    PreviousMatchScreen,

    /// Move to the next match, follow the current screen.
    NextMatchScreen,

    /// Move to the first match.
    FirstMatch,

    /// Move to the last match.
    LastMatch,

    /// Append a digit to the "repeat count".
    /// The count defines how many times to do the next operation.
    AppendDigitToRepeatCount(usize),
}

impl std::fmt::Display for Action {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use Action::*;
        match *self {
            Quit => write!(f, "Quit"),
            Refresh => write!(f, "Refresh the screen"),
            Help => write!(f, "Show this help"),
            Cancel => write!(f, "Close help or any open prompt"),
            PreviousFile => write!(f, "Switch to the previous file"),
            NextFile => write!(f, "Switch to the next file"),
            ToggleRuler => write!(f, "Toggle the ruler"),
            ScrollUpLines(1) => write!(f, "Scroll up"),
            ScrollUpLines(n) => write!(f, "Scroll up {} lines", n),
            ScrollDownLines(1) => write!(f, "Scroll down"),
            ScrollDownLines(n) => write!(f, "Scroll down {} lines", n),
            ScrollUpScreenFraction(1) => write!(f, "Scroll up one screen"),
            ScrollUpScreenFraction(n) => write!(f, "Scroll up 1/{} screen", n),
            ScrollDownScreenFraction(1) => write!(f, "Scroll down one screen"),
            ScrollDownScreenFraction(n) => write!(f, "Scroll down 1/{} screen", n),
            ScrollToTop => write!(f, "Move to the start of the file"),
            ScrollToBottom => write!(f, "Move to and follow the end of the file"),
            ScrollLeftColumns(1) => write!(f, "Scroll left"),
            ScrollLeftColumns(n) => write!(f, "Scroll left {} columns", n),
            ScrollRightColumns(1) => write!(f, "Scroll right"),
            ScrollRightColumns(n) => write!(f, "Scroll right {} columns", n),
            ScrollLeftScreenFraction(1) => write!(f, "Scroll left one screen"),
            ScrollLeftScreenFraction(n) => write!(f, "Scroll left 1/{} screen", n),
            ScrollRightScreenFraction(1) => write!(f, "Scroll right one screen"),
            ScrollRightScreenFraction(n) => write!(f, "Scroll right 1/{} screen", n),
            ToggleLineNumbers => write!(f, "Toggle line numbers"),
            ToggleLineWrapping => write!(f, "Cycle through line wrapping modes"),
            PromptGoToLine => write!(f, "Go to position in file"),
            PromptSearchFromStart => write!(f, "Search from the start of the file"),
            PromptSearchForwards => write!(f, "Search forwards"),
            PromptSearchBackwards => write!(f, "Search backwards"),
            PreviousMatch => write!(f, "Move to the previous match"),
            NextMatch => write!(f, "Move to the next match"),
            PreviousMatchLine => write!(f, "Move to the previous matching line"),
            NextMatchLine => write!(f, "Move the the next matching line"),
            PreviousMatchScreen => write!(f, "Move to the previous match following the screen"),
            NextMatchScreen => write!(f, "Move to the next match following the screen"),
            FirstMatch => write!(f, "Move to the first match"),
            LastMatch => write!(f, "Move to the last match"),
            AppendDigitToRepeatCount(n) => write!(f, "Append digit {} to repeat count", n),
        }
    }
}

/// A handle that can be used to send actions to the pager.
#[derive(Clone)]
pub struct ActionSender(Arc<Mutex<EventSender>>);

impl ActionSender {
    /// Create an action sender for an event sender.
    pub(crate) fn new(event_sender: EventSender) -> ActionSender {
        ActionSender(Arc::new(Mutex::new(event_sender)))
    }

    /// Send an action to the pager.
    pub fn send(&self, action: Action) -> Result<(), Error> {
        let sender = self.0.lock().unwrap();
        sender.send(Event::Action(action))?;
        Ok(())
    }
}


//! A horizontal bar on the screen.

use std::cmp::min;
use std::sync::Arc;

use termwiz::cell::CellAttributes;
use termwiz::color::AnsiColor;
use termwiz::surface::change::Change;
use termwiz::surface::Position;
use unicode_width::UnicodeWidthStr;

use crate::util;

/// A horizontal bar on the screen, e.g. the ruler or search bar.
pub(crate) struct Bar {
    left_items: Vec<Arc<dyn BarItem>>,
    right_items: Vec<Arc<dyn BarItem>>,
    style: BarStyle,
}

/// An item in a bar.
pub(crate) trait BarItem {
    fn width(&self) -> usize;
    fn render(&self, changes: &mut Vec<Change>, width: usize);
}

/// The style of the bar.  This mostly affects the default background color.
#[allow(unused)]
#[derive(Clone, Copy, Debug)]
pub(crate) enum BarStyle {
    // A normal bar with a silver background.
    Normal,

    // An informational bar with a teal background.
    Information,

    // A warning bar with a yellow background.
    Warning,

    // An error bar with a red background.
    Error,
}

impl BarStyle {
    fn background_color(self) -> AnsiColor {
        match self {
            BarStyle::Normal => AnsiColor::Silver,
            BarStyle::Information => AnsiColor::Teal,
            BarStyle::Warning => AnsiColor::Olive,
            BarStyle::Error => AnsiColor::Maroon,
        }
    }
}

impl Bar {
    pub(crate) fn new(style: BarStyle) -> Self {
        let left_items = Vec::new();
        let right_items = Vec::new();
        Bar {
            left_items,
            right_items,
            style,
        }
    }

    pub(crate) fn add_left_item(&mut self, item: Arc<dyn BarItem>) {
        self.left_items.push(item);
    }

    pub(crate) fn add_right_item(&mut self, item: Arc<dyn BarItem>) {
        self.right_items.push(item);
    }

    /// Render the bar to the given row on screen.
    pub(crate) fn render(&self, changes: &mut Vec<Change>, row: usize, width: usize) {
        changes.push(Change::CursorPosition {
            x: Position::Absolute(0),
            y: Position::Absolute(row),
        });
        let bar_attribs = CellAttributes::default()
            .set_foreground(AnsiColor::Black)
            .set_background(self.style.background_color())
            .clone();

        if width < 8 {
            // The area is too small to write anything useful, just write a blank bar.
            changes.push(Change::AllAttributes(bar_attribs));
            changes.push(Change::ClearToEndOfLine(
                self.style.background_color().into(),
            ));
            return;
        }

        let padded_item_width = |item: &Arc<dyn BarItem>| match item.width() {
            0 => 0,
            w => w + 2,
        };
        let mut left_items_width = self.left_items.iter().map(padded_item_width).sum();
        let mut right_items_width = self.right_items.iter().map(padded_item_width).sum();

        // The right-hand side is shown only if it can fit.
        if right_items_width + 2 > width {
            // Show only left items.
            right_items_width = 0;
            left_items_width = min(left_items_width, width.saturating_sub(2));
        } else {
            // Show both items, truncating or padding the left items to the remaining width.
            left_items_width = width.saturating_sub(right_items_width + 2);
        }

        changes.push(Change::AllAttributes(bar_attribs.clone()));
        changes.push(Change::Text(String::from("  ")));
        let rendered_left_width = self.render_items(
            changes,
            self.left_items.as_slice(),
            left_items_width.saturating_sub(2),
        );
        if right_items_width > 0 {
            changes.push(Change::AllAttributes(bar_attribs));
            let gap = left_items_width.saturating_sub(rendered_left_width);
            changes.push(Change::Text(" ".repeat(gap)));
            self.render_items(changes, self.right_items.as_slice(), right_items_width);
        }
        changes.push(Change::ClearToEndOfLine(
            self.style.background_color().into(),
        ));
    }

    fn render_items(
        &self,
        changes: &mut Vec<Change>,
        items: &[Arc<dyn BarItem>],
        width: usize,
    ) -> usize {
        let mut rendered_width = 0;
        for item in items.iter() {
            let item_width = item.width().min(width.saturating_sub(rendered_width));
            if item_width > 0 {
                item.render(changes, item_width);
                rendered_width += item_width;
                let pad = min(width - rendered_width, 2);
                changes.push(Change::Text(" ".repeat(pad)));
                rendered_width += pad;
                if rendered_width >= width {
                    break;
                }
            }
        }
        rendered_width
    }
}

pub(crate) struct BarString(String);

impl BarString {
    pub(crate) fn new(s: impl Into<String>) -> Self {
        BarString(s.into())
    }
}

impl BarItem for BarString {
    fn width(&self) -> usize {
        self.0.as_str().width()
    }

    fn render(&self, changes: &mut Vec<Change>, width: usize) {
        changes.push(Change::Text(util::truncate_string(
            self.0.as_str(),
            0,
            width,
        )));
    }
}