### ----------------------------------------------------
### -- Config::BINDish
### -- Licenses: Artistic-2.0
### -- Authors: Vadim Belman <vrurg@lflat.org>
### -- File: t/010-grammar.rakutest
### ----------------------------------------------------
use v6.d;
use Test::Async <Config::BINDish::Test Base>;
use Config::BINDish::Grammar;
use Config::BINDish::X;

plan 3, :!parallel;

subtest "Grammar Basics", {
    my @tests =
        "empty config" => %(
            source => ";;",
            top => %(),
        ),

        "int number" => %(
            source => '1234',
            top => statements => 0 => value => "1234",
        ),

        "int number prettified" => %(
            source => '1_000_000',
            top => statements => 0 => value => "1_000_000",
        ),

        "K-int" => %(
            source => '1_000_000K',
            top => statements => 0 => value => "1_000_000K",
        ),

        "k-int" => %(
            source => '1_000_000k',
            top => statements => 0 => value => "1_000_000k",
        ),

        "M-int" => %(
            source => '1_000_000M',
            top => statements => 0 => value => "1_000_000M",
        ),

        "G-int" => %(
            source => '1_000_000G',
            top => statements => 0 => value => "1_000_000G",
        ),

        "T-int" => %(
            source => '1_000_000T',
            top => statements => 0 => value => "1_000_000T",
        ),

        "P-int" => %(
            source => '1_000_000P',
            top => statements => 0 => value => "1_000_000P",
        ),

        "bin number" => %(
            source => '0b1_100_100',
            top => statements => 0 => value => "0b1_100_100",
        ),

        "octal number" => %(
            source => '0o1_000_000',
            top => statements => 0 => value => "0o1_000_000",
        ),

        "hex number" => %(
            source => '0xBeef_000',
            top => statements => 0 => value => "0xBeef_000",
        ),

        "rat number full" => %(
            source => '1234.5678',
            top => statements => 0 => value => "1234.5678",
        ),

        "rat number denominator" => %(
            source => '.5678',
            top => statements => 0 => value => ".5678",
        ),

        "rat number numerator" => %(
            source => '1234.',
            top => statements => 0 => value => "1234.",
        ),

        "rat number prettified" => %(
            source => '1_000.234_567',
            top => statements => 0 => value => "1_000.234_567",
        ),

        "rat number numerator prettified" => %(
            source => '1_000.',
            top => statements => 0 => value => "1_000.",
        ),

        "rat number denumerator prettified" => %(
            source => '.000_1',
            top => statements => 0 => value => ".000_1",
        ),

        "K-rat" => %(
            source => '3.1415926K',
            top => statements => 0 => value => "3.1415926K",
        ),

        "k-rat" => %(
            source => '3.1415926k',
            top => statements => 0 => value => "3.1415926k",
        ),

        "M-rat" => %(
            source => '3.1415926M',
            top => statements => 0 => value => "3.1415926M",
        ),

        "G-rat" => %(
            source => '3.1415926G',
            top => statements => 0 => value => "3.1415926G",
        ),

        "T-rat" => %(
            source => '3.1415926T',
            top => statements => 0 => value => "3.1415926T",
        ),

        "P-rat" => %(
            source => '3.1415926P',
            top => statements => 0 => value => "3.1415926P",
        ),

        "bin int" => %(
            source => '0b1001',
            top => statements => 0 => value => "0b1001",
        ),

        "oct int" => %(
            source => '0o777',
            top => statements => 0 => value => "0o777",
        ),

        "hex int" => %(
            source => '0xbEEf',
            top => statements => 0 => value => "0xbEEf",
        ),

        "relative file path" => %(
            source => './dir/file',
            top => statements => 0 => value => "./dir/file",
        ),

        "relative to parent file path" => %(
            source => '../dir/file',
            top => statements => 0 => value => "../dir/file",
        ),

        "root-based file path" => %(
            source => '/dir/file',
            top => statements => 0 => value => "/dir/file",
        ),

        "relative dir path" => %(
            source => './dir1/dir2/',
            top => statements => 0 => value => "./dir1/dir2/",
        ),

        "relative to parent dir path" => %(
            source => '../dir1/dir2/',
            top => statements => 0 => value => "../dir1/dir2/",
        ),

        "root-based dir path" => %(
            source => '/dir1/dir2/',
            top => statements => 0 => value => "/dir1/dir2/",
        ),

        "bare option" => %(
            source => "opt",
#            plans => 0 => 5,
            top => %(
                statements => 0 => %(
                    option-name => "opt",
                    statement-terminate => statement-terminator => ""))),

        "terminated bare option" => %(
            source => "opt;",
            plans => 0 => 5,
            top => statements => 0 => %(
                option-name => "opt",
                statement-terminate => statement-terminator => ";")),

        "multi-option" => %(
            source => q:to/CFG/,
                        option1;
                        option2
                        CFG
            top => %(
                stmt-count => 2,
                statements => %(
                    0 => %(
                        option-name => 'option1',
                        statement-terminate => statement-terminator => ';'),
                    1 => %(
                        option-name => 'option2',
                        statement-terminate => statement-terminator => '')))),

        "string value option" => %(
            source => "opt \"foo\"",
            top => %(
                plans => 0 => 9,
                statements => 0 => %(
                    option-name => "opt",
                    option-value => dq-string => string => "foo",
                    statement-terminate => statement-terminator => ""))),

        "keyword value option" => %(
            source => "opt foo",
            top => %(
                statements => 0 => %(
                    option-name => "opt",
                    option-value => keyword => "foo",
                    statement-terminate => statement-terminator => "" ))),

        "terminated string value option" => %(
            source => "opt \"foo\";",
            top => %(
                plans => 0 => 9,
                statements => 0 => %(
                    option-name => "opt",
                    option-value => dq-string => string => "foo",
                    statement-terminate => statement-terminator => ";"))),

        "int value option" => %(
            source => "opt 42",
            top => statements => 0 => %(
                    option-name => "opt",
                    option-value => 42,
                    statement-terminate => statement-terminator => "")),

        "terminated int value option" => %(
            source => "opt -13;",
            top => statements => 0 => %(
                    option-name => "opt",
                    option-value => -13,
                    statement-terminate => statement-terminator => ";")),

        "rat value option" => %(
            source => "opt 4.2",
            top => structs => 0 => %(
                    option-name => "opt",
                    option-value => 4.2,
                    statement-terminate => statement-terminator => "")),

        "terminated rat value option" => %(
            source => q:to/CFG/,
                      opt1 -1.3;
                      opt2 42.;
                      opt3 .42;
                      opt4 -42.;
                      opt5 -.42;
                      CFG
            top => %(
                stmt-count => 5,
                statements => %(
                    0 => %(
                        option-name => "opt1",
                        option-value => -1.3,
                        statement-terminate => statement-terminator => ";"),
                    1 => %(
                        option-name => "opt2",
                        option-value => "42.",
                        statement-terminate => statement-terminator => ";"),
                    2 => %(
                        option-name => "opt3",
                        option-value => ".42",
                        statement-terminate => statement-terminator => ";"),
                    3 => %(
                        option-name => "opt4",
                        option-value => "-42.",
                        statement-terminate => statement-terminator => ";"),
                    4 => %(
                        option-name => "opt5",
                        option-value => "-.42",
                        statement-terminate => statement-terminator => ";")))),

        "num value option" => %(
            source => "opt 4.2e1",
            top => statements => 0 => %(
                    option-name => "opt",
                    option-value => "4.2e1",
                    statement-terminate => statement-terminator => "")),

        "terminated num value option" => %(
            source => q:to/CFG/,
                      opt1 -1.3e0;
                      opt2 42e-1;
                      opt3 .42e1;
                      opt4 -42e-2;
                      opt5 -.42e2;
                      CFG
            top => %(
                stmt-count => 5,
                statements => %(
                    0 => %(
                        option-name => "opt1",
                        option-value => "-1.3e0",
                        statement-terminate => statement-terminator => ";"),
                    1 => %(
                        option-name => "opt2",
                        option-value => "42e-1",
                        statement-terminate => statement-terminator => ";"),
                    2 => %(
                        option-name => "opt3",
                        option-value => ".42e1",
                        statement-terminate => statement-terminator => ";"),
                    3 => %(
                        option-name => "opt4",
                        option-value => "-42e-2",
                        statement-terminate => statement-terminator => ";"),
                    4 => %(
                        option-name => "opt5",
                        option-value => "-.42e2",
                        statement-terminate => statement-terminator => ";")))),

        "num in a block" => %(
            source => q:to/CFG/,
                        multi "special" class {
                            num 3.14e0;
                        }
                        CFG
            top => statements => 0 => %(
                block-head => block-type => 'multi',
                block-body => token-statements(
                        statements => %(
                            0 => %(
                                option-name => "num",
                                option-value => "3.14e0"))))),

        "comments" => %(
            source => q:to/CFG/,
                        # Unix style
                        opt 1;
                        // C++ style
                        opt 2;
                        /*
                         * C style
                         */
                        opt 3;
                        CFG
            top => statements => %(
                0 => UNIX-comment => comment-body => ' Unix style',
                2 => CPP-comment => comment-body => ' C++ style',
                4 => C-comment => comment-body => "\n * C style\n ",
            ),
        ),

        "empty block" => %(
            source => 'block {}',
            top => statements => 0 => %(
                block-head => block-type => "block",
                block-body => '{}' )),

        "terminated empty block" => %(
            source => 'block {};',
            top => statements => 0 => %(
                block-head => block-type => "block",
                block-body => '{};')),

        "empty named block" => %(
            source => q:to/CFG/,
                      blk "foo" { }
                      CFG
            # Keys of expect hash are indicies in <statements> list
            # Values represent the structure
            top => statements => 0 => %(
                    block-head => %(
                        block-type => 'blk',
                        block-name => value => dq-string => string => "foo",
                    ),
                    block-body => "\{ \}\n" )),

        "empty named and classified block" => %(
            source => q:to/CFG/,
                      blk "foo" BAR { }
                      CFG
            # Keys of expect hash are indicies in <statements> list
            # Values represent the structure
            top => statements => 0 => %(
                    block-head => %(
                        block-type => 'blk',
                        block-name => value => dq-string => string => "foo",
                        block-class => 'BAR',
                    ),
                    block-body => "\{ \}\n" )),

        "nested blocks" => %(
            source => q:to/CFG/,
                        block {
                            foo 42;
                            subblock "nested" 1 {
                                nested-foo 13;
                                subsubblock "nested" 2 {
                                    option "deep one";
                                    nested-foo 0
                                }
                            }
                        }
                        CFG
            top => statements => 0 => %(
                block-head => block-type => "block",
                block-body => token-statements(
                    message => "top block",
                    stmt-count => 2,
                    statements => %(
                        0 => %(
                            option-name => "foo",
                            option-value => "42",
                            statement-terminate => statement-terminator => ";"),
                        1 => %(
                            block-head => %(
                                block-type => "subblock",
                                block-name => q<"nested">,
                                block-class => "1",
                            ),
                            block-body => token-statements(
                                message => "subblock, nesting 1",
                                stmt-count => 2,
                                statements => %(
                                    0 => %(
                                        option-name => "nested-foo",
                                        option-value => "13",
                                        statement-terminate => statement-terminator => ";"),
                                    1 => %(
                                        block-head => %(
                                            block-type => "subsubblock",
                                            block-name => q<"nested">,
                                            block-class => "2",
                                        ),
                                        block-body => token-statements(
                                            message => "subblock, nesting 2",
                                            stmt-count => 2,
                                            statements => %(
                                                0 => %(
                                                    option-name => "option",
                                                    option-value => q<"deep one">,
                                                    statement-terminate => statement-terminator => ";",
                                                ),
                                                1 => %(
                                                    option-name => "nested-foo",
                                                    option-value => "0",
                                                    statement-terminate => statement-terminator => ""))))))))))),

        "multi-block" => %(
            source => q:to/CFG/,
                        multi "1" {
                            opt1 1;
                        }
                        multi "2" {
                            opt2 "2";
                        }
                        multi "1" {
                            opt1 3;
                        }
                        CFG
            top => %(
                stmt-count => 3,
                statements => %(
                    0 => %(
                        block-head => %(
                            block-type => 'multi',
                            block-name => value => dq-string => string => "1",
                        ),
                        block-body => token-statements(
                            message => 'first multi "1"',
                            stmt-count => 1,
                            statements => 0 => %(
                                option-name => "opt1",
                                option-value => "1" ))),
                    1 => %(
                        block-head => %(
                            block-type => 'multi',
                            block-name => value => dq-string => string => "2",
                        ),
                        block-body => token-statements(
                            message => 'multi "2"',
                            stmt-count => 1,
                            statements => 0 => %(
                                option-name => "opt2",
                                option-value => q<"2"> ))),
                    2 => %(
                        block-head => %(
                            block-type => 'multi',
                            block-name => value => dq-string => string => "1",
                        ),
                        block-body => token-statements(
                            message => 'second multi "1"',
                            stmt-count => 1,
                            statements => 0 => %(
                                option-name => "opt1",
                                option-value => "3" )))))),

        "value list block" => %(
            source => q:to/CFG/,
                        blist "some data" {
                            1.1; 2.2; 3.3;
                        }
                        CFG
            top => statements => 0 => %(
                block-head => block-type => 'blist',
                block-body => token-statements(
                        statements => %(
                            0 => value => "1.1",
                            1 => value => "2.2",
                            2 => value => "3.3")))),
        "file values" => %(
            source => q:to/CFG/,
                        bmix {
                            ./file1.ext;
                            # ./file2.ext;
                            flag on;
                        }
                        CFG
            top => statements => 0 => %(
                block-head => block-type => 'bmix',
                block-body => token-statements(
                    statements => %(
                        0 => value => "./file1.ext",
                        1 => UNIX-comment => "# ./file2.ext;",
                        2 => %(
                            option-name => 'flag',
                            option-value => "on",
                        )
                    )
                    )
            )
        ),
        ;

    plan +@tests, :parallel;

    run-grammar-tests Config::BINDish::Grammar, @tests;
}

subtest "Errors" => {
    plan 8;

    throws-like { Config::BINDish::Grammar.parse: '{}'; },
                Config::BINDish::X::Parse::General,
                "config syntax error",
                message => /:s Unrecognized statement/;

    throws-like { Config::BINDish::Grammar.parse: '0o1239'; },
                Config::BINDish::X::Parse::BadNum,
                "bad oct number",
                message => /:s Bad number/;

    throws-like { Config::BINDish::Grammar.parse: '0x123SA'; },
                Config::BINDish::X::Parse::BadNum,
                "bad hex number",
                message => /:s Bad number/;

    throws-like { Config::BINDish::Grammar.parse: 'keyword-ends-with-dash-'; },
                Config::BINDish::X::Parse::General,
                "a keyword must not end with a dash",
                message => /:s Unrecognized statement/;

    throws-like { Config::BINDish::Grammar.parse: 'block {}', strict => :syntax },
                Config::BINDish::X::Parse::General,
                "block needs a terminator in strict mode",
                message => /:s Missing semicolon/;

    throws-like { Config::BINDish::Grammar.parse: 'block { option };', strict => {:syntax} },
                Config::BINDish::X::Parse::General,
                "option needs a terminator in strict mode",
                message => /:s Missing semicolon/;

    throws-like { Config::BINDish::Grammar.parse: 'block { option !foo; };', :!strict },
                Config::BINDish::X::Parse::General,
                "option value can't be parsed",
                message => /:s Unrecognized statement/;

    throws-like { Config::BINDish::Grammar.parse:
                        q:to/CFG/
                        block {
                            val 1;
                            opt "foo";
                            option !bar
                        }
                        CFG
                },
                Config::BINDish::X::Parse::General,
                "error line is reported",
                message => /"Unrecognized statement" .* "line 4"/;
}

subtest "Explicit Structure" => {
    plan 14;

    my @blocks = :block => {:in<.TOP>, :named},
                 :subblock => {:in<block>, :named},
                 parent-block => {:in<.TOP>},
                 ;
    my @options = count => { :type(Int) },
                  description => { :in<.TOP>, :type(Str) },
                  subopt => { :in<subblock>, :type-name<int>  },
                  ;
    my $cfg-source = q:to/CFG/;
                        description "mixed pre-declared and undeclared elements";
                        free-form-option;
                        count 1;
                        block 'top one' {
                            count 2;
                            subblock "inner one" {
                                subopt 42;
                                other-option "is ok too";
                            }
                        }
                        foo "undeclared" { }
                        CFG

    my $match = Config::BINDish::Grammar.parse: $cfg-source, :@blocks, :@options, :!strict;
    is-cfg-stmt-list $match<body>,
                     %(
                        statements => %(
                            0 => %(
                                option-name => "description",
                                option-value => dq-string => q<"mixed pre-declared and undeclared elements">,
                            ),
                            1 => option-name => "free-form-option",
                            2 => %(
                                option-name => "count",
                                option-value => "1",
                            ),
                            3 => %(
                                block-head => %(
                                    block-type => "block",
                                    block-name => value => sq-string => q<'top one'>,
                                ),
                                block-body => token-statements(
                                        message => "structured block",
                                        statements => %(
                                            0 => %(
                                                option-name => "count",
                                                option-value => "2",
                                            ),
                                            1 => %(
                                                block-head => %(
                                                    block-type => "subblock",
                                                    block-name => value => dq-string => q<"inner one">,
                                                ),
                                                block-body => token-statements(
                                                        statements => %(
                                                            0 => %(
                                                                option-name => "subopt",
                                                                option-value => "42",
                                                            ),
                                                            1 => %(
                                                                option-name => "other-option",
                                                                option-value => q<"is ok too">,
                                                            ),
                                                        ),
                                                    ),
                                            ),
                                        ),
                                    ),
                            ),
                            4 => %(
                                block-head => %(
                                    block-type => "foo",
                                    block-name => value => dq-string => q<"undeclared">,
                                ),
                                block-body => token-statements(statements => %()),
                            ),
                        ),
                     ),
                     "predefined elements can be mixed with free-form ones in non-strict mode";

    throws-like {
                    Config::BINDish::Grammar.parse("bad-option 1;", :@options, :strict).sink;
                },
                Config::BINDish::X::Parse::Unknown,
                "undeclared option in strict mode throws",
                message => /:s Unknown option/;
    throws-like {
                    Config::BINDish::Grammar.parse(q<foo 'bar' { };>, :@blocks, :strict).sink;
                },
                Config::BINDish::X::Parse::Unknown,
                "undeclared block in strict mode throws",
                message => /:s Unknown block/;

    throws-like {
                    Config::BINDish::Grammar.parse(q<subblock 'not here' { };>, :@blocks, :strict).sink;
                },
                Config::BINDish::X::Parse::Context,
                "a subblock at the top level in strict mode throws",
                message => /:s Block .* cannot be used in global context/;

    for True, False -> $strict {
        my $mode = ($strict ?? 'strict' !! 'non-strict') ~ " mode";
        throws-like {
                        Config::BINDish::Grammar.parse(q<parent-block { subblock 'not here' { }; };>,
                                                       :@blocks,
                                                       :$strict).sink;
                    },
                    Config::BINDish::X::Parse::Context,
                    "a subblock under wrong parent block in $mode throws",
                    message => /:s Block .* cannot be used in block \'parent\-block\'/;
        throws-like {
                        Config::BINDish::Grammar.parse(q<parent-block { block 'not here' { }; };>,
                                                       :@blocks,
                                                       :$strict).sink;
                    },
                    Config::BINDish::X::Parse::Context,
                    "a top-level block under a parent block in $mode throws",
                    message => /Block .* "cannot be used in block"/;
        throws-like {
                        Config::BINDish::Grammar.parse(q<parent-block { subopt 13; };>,
                                                       :@blocks,
                                                       :@options,
                                                       :$strict).sink;
                    },
                    Config::BINDish::X::Parse::Context,
                    "a option in a wrong block in $mode throws",
                    message => /:s Option .* cannot be used in block/;
        throws-like {
                        Config::BINDish::Grammar.parse(q<parent-block { description 'any'; };>,
                                                       :@blocks,
                                                       :@options,
                                                       :$strict).sink;
                    },
                    Config::BINDish::X::Parse::Context,
                    "a top-level option can't be used in a block in $mode",
                    message => /:s Option .* cannot be used in block/;
    }

    throws-like {
                    Config::BINDish::Grammar.parse(q:to/CFG/,
                                                        # Make it not the first line
                                                        description 12;
                                                        CFG
                                                   :@blocks,
                                                   :@options).sink;
                },
                Config::BINDish::X::Parse::Value,
                "correct line number reported for an option with bad value in .TOP",
                line => 2;

    throws-like {
                    Config::BINDish::Grammar.parse(q:to/CFG/,
                                                        block "bad one" {
                                                            subblock "because of bad option" {
                                                                subopt "can't be a string"
                                                            }
                                                        }
                                                        CFG
                                                   :@blocks,
                                                   :@options).sink;
                },
                Config::BINDish::X::Parse::Value,
                "correct line number reported for an option with bad value in a subblock",
                line => 3;
}

done-testing;


