use std::ffi::CStr;

use super::ast;
use super::char_stream::CharStream;
use super::*;
use basic_parser::{parse_eval, parse_lines, parse_string};

impl<T> Qualified<T> {
    pub fn as_allow(&self) -> Option<&T> {
        if let Self::Allow(v) = self {
            Some(v)
        } else {
            None
        }
    }
}

impl<T> Meta<T> {
    pub fn is_alias(&self) -> bool {
        matches!(self, Self::Alias(..))
    }
}

impl Sudo {
    pub fn is_spec(&self) -> bool {
        matches!(self, Self::Spec(..))
    }

    pub fn is_decl(&self) -> bool {
        matches!(self, Self::Decl(..))
    }

    pub fn is_line_comment(&self) -> bool {
        matches!(self, Self::LineComment)
    }

    pub fn is_include(&self) -> bool {
        matches!(self, Self::Include(..))
    }

    pub fn is_include_dir(&self) -> bool {
        matches!(self, Self::IncludeDir(..))
    }

    pub fn as_include(&self) -> &str {
        if let Self::Include(v, _) = self {
            v
        } else {
            panic!()
        }
    }

    pub fn as_spec(&self) -> Option<&PermissionSpec> {
        if let Self::Spec(v) = self {
            Some(v)
        } else {
            None
        }
    }
}

#[derive(PartialEq)]
struct Named(&'static str);

fn dummy_cksum(name: &str) -> u32 {
    if name == "root" {
        0
    } else {
        1000 + name.chars().fold(0, |x, y| (x * 97 + y as u32) % 1361)
    }
}

impl UnixUser for Named {
    fn has_name(&self, name: &str) -> bool {
        self.0 == name
    }

    fn has_uid(&self, uid: UserId) -> bool {
        UserId::new(dummy_cksum(self.0)) == uid
    }

    fn in_group_by_name(&self, name: &CStr) -> bool {
        self.has_name(name.to_str().unwrap())
    }

    fn in_group_by_gid(&self, gid: GroupId) -> bool {
        GroupId::new(dummy_cksum(self.0)) == gid
    }

    fn is_root(&self) -> bool {
        self.0 == "root"
    }
    type Group = Named;
    fn group(&self) -> Named {
        Self(self.0)
    }
}

impl UnixGroup for Named {
    fn as_gid(&self) -> GroupId {
        GroupId::new(dummy_cksum(self.0))
    }
    fn try_as_name(&self) -> Option<&str> {
        Some(self.0)
    }
}

macro_rules! request {
    ($user:ident) => {
        (&Named(stringify!($user)), &Named(stringify!($user)))
    };
    ($user:ident, $group:ident) => {
        (&Named(stringify!($user)), &Named(stringify!($group)))
    };
}

macro_rules! sudoer {
    ($($e:expr),*) => {
        parse_lines(&mut CharStream::new(&[$($e),*, ""].join("\n")))
            .into_iter()
            .map(|x| Ok::<_,basic_parser::Status>(x.unwrap()))
    }
}

// alternative to parse_eval, but goes through sudoer! directly
#[must_use]
fn parse_line(s: &str) -> Sudo {
    sudoer![s].next().unwrap().unwrap()
}

/// Returns `None` if a syntax error is encountered
fn try_parse_line(s: &str) -> Option<Sudo> {
    parse_lines(&mut CharStream::new(&[s, ""].join("")))
        .into_iter()
        .next()?
        .ok()
}

#[test]
fn ambiguous_spec() {
    assert!(parse_eval::<ast::Sudo>("marc, User_Alias ALL = ALL").is_spec());
}

#[test]
fn permission_test() {
    let root = || (&Named("root"), &Named("root"));

    let realpath =
        |path: &Path| crate::common::resolve::canonicalize(path).unwrap_or(path.to_path_buf());

    macro_rules! FAIL {
        ([$($sudo:expr),*], $user:expr => $req:expr, $server:expr; $command:expr) => {
            let (Sudoers { rules,aliases,settings, customisers }, _) = analyze(Path::new("/etc/fakesudoers"), sudoer![$($sudo),*]);
            let cmdvec = $command.split_whitespace().map(OsString::from).collect::<Vec<_>>();
            let req = Request { user: $req.0, group: $req.1, command: &realpath(cmdvec[0].as_ref()), arguments: &cmdvec[1..].to_vec() };
            assert_eq!(Sudoers { rules, aliases, settings, customisers }.check(&Named($user), &system::Hostname::fake($server), req).flags, None);
        }
    }

    macro_rules! pass {
        ([$($sudo:expr),*], $user:expr => $req:expr, $server:expr; $command:expr $(=> [$($key:ident : $val:expr),*])?) => {
            let (Sudoers { rules,aliases,settings, customisers }, _) = analyze(Path::new("/etc/fakesudoers"), sudoer![$($sudo),*]);
            let cmdvec = $command.split_whitespace().map(OsString::from).collect::<Vec<_>>();
            let req = Request { user: $req.0, group: $req.1, command: &realpath(cmdvec[0].as_ref()), arguments: &cmdvec[1..].to_vec() };
            let result = Sudoers { rules, aliases, settings, customisers }.check(&Named($user), &system::Hostname::fake($server), req).flags;
            assert!(!result.is_none());
            $(
                let result = result.unwrap();
                $(assert_eq!(result.$key, $val);)*
            )?
        }
    }
    macro_rules! SYNTAX {
        ([$sudo:expr]) => {
            assert!(parse_string::<Sudo>($sudo).is_err())
        };
    }

    SYNTAX!(["ALL ALL = (;) ALL"]);
    FAIL!(["user ALL=(ALL:ALL) ALL"], "nobody"    => root(), "server"; "/bin/hello");
    pass!(["user ALL=(ALL:ALL) ALL"], "user"      => root(), "server"; "/bin/hello");
    pass!(["user ALL=(ALL:ALL) /bin/foo"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::None]);
    FAIL!(["user ALL=(ALL:ALL) /bin/foo"], "user" => root(), "server"; "/bin/hello");
    pass!(["user ALL=(ALL:ALL) PASSWD: /bin/foo"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::Passwd]);
    pass!(["user ALL=(ALL:ALL) NOPASSWD: PASSWD: /bin/foo"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::Passwd]);
    pass!(["user ALL=(ALL:ALL) PASSWD: NOPASSWD: /bin/foo"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::Nopasswd]);
    pass!(["user ALL=(ALL:ALL) /bin/foo, NOPASSWD: /bin/bar"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::None]);
    pass!(["user ALL=(ALL:ALL) /bin/foo, NOPASSWD: /bin/bar"], "user" => root(), "server"; "/bin/bar" => [authenticate: Authenticate::Nopasswd]);
    pass!(["user ALL=(ALL:ALL) NOPASSWD: /bin/foo, /bin/bar"], "user" => root(), "server"; "/bin/bar" => [authenticate: Authenticate::Nopasswd]);
    pass!(["user ALL=(ALL:ALL) CWD=/ /bin/foo, /bin/bar"], "user" => root(), "server"; "/bin/bar" => [cwd: Some(ChDir::Path("/".into()))]);
    pass!(["user ALL=(ALL:ALL) CWD=/ /bin/foo, CWD=* /bin/bar"], "user" => root(), "server"; "/bin/bar" => [cwd: Some(ChDir::Any)]);
    pass!(["user ALL=(ALL:ALL) CWD=/bin CWD=* /bin/foo"], "user" => root(), "server"; "/bin/foo" => [cwd: Some(ChDir::Any)]);
    pass!(["user ALL=(ALL:ALL) CWD=/usr/bin NOPASSWD: /bin/foo"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::Nopasswd, cwd: Some(ChDir::Path("/usr/bin".into()))]);
    //note: original sudo does not allow the below
    pass!(["user ALL=(ALL:ALL) NOPASSWD: CWD=/usr/bin /bin/foo"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::Nopasswd, cwd: Some(ChDir::Path("/usr/bin".into()))]);

    pass!(["user ALL=/bin/e##o"], "user" => root(), "vm"; "/bin/e");
    SYNTAX!(["ALL ALL=(ALL) /bin/\n/echo"]);

    pass!(["user server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    FAIL!(["user laptop=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");

    pass!(["user ALL=!/bin/hello", "user ALL=/bin/hello"], "user" => root(), "server"; "/bin/hello");
    FAIL!(["user ALL=/bin/hello", "user ALL=!/bin/hello"], "user" => root(), "server"; "/bin/hello");

    for alias in [
        "User_Alias GROUP=user1, user2",
        "User_Alias GROUP=ALL,!user3",
    ] {
        pass!([alias,"GROUP ALL=/bin/hello"], "user1" => root(), "server"; "/bin/hello");
        pass!([alias,"GROUP ALL=/bin/hello"], "user2" => root(), "server"; "/bin/hello");
        FAIL!([alias,"GROUP ALL=/bin/hello"], "user3" => root(), "server"; "/bin/hello");
    }
    pass!(["user ALL=/bin/hello arg"], "user" => root(), "server"; "/bin/hello arg");
    pass!(["user ALL=/bin/hello  arg"], "user" => root(), "server"; "/bin/hello arg");
    pass!(["user ALL=/bin/hello arg"], "user" => root(), "server"; "/bin/hello  arg");
    FAIL!(["user ALL=/bin/hello arg"], "user" => root(), "server"; "/bin/hello boo");
    // several test cases with globbing in the arguments are explicitly not supported by sudo-rs
    //pass!(["user ALL=/bin/hello a*g"], "user" => root(), "server"; "/bin/hello  aaaarg");
    //FAIL!(["user ALL=/bin/hello a*g"], "user" => root(), "server"; "/bin/hello boo");
    pass!(["user ALL=/bin/hello"], "user" => root(), "server"; "/bin/hello boo");
    FAIL!(["user ALL=/bin/hello \"\""], "user" => root(), "server"; "/bin/hello boo");
    pass!(["user ALL=/bin/hello \"\""], "user" => root(), "server"; "/bin/hello");
    pass!(["user ALL=/bin/hel*"], "user" => root(), "server"; "/bin/hello");
    pass!(["user ALL=/bin/hel*"], "user" => root(), "server"; "/bin/help");
    pass!(["user ALL=/bin/hel*"], "user" => root(), "server"; "/bin/help me");
    //pass!(["user ALL=/bin/hel* *"], "user" => root(), "server"; "/bin/help");
    FAIL!(["user ALL=/bin/hel* me"], "user" => root(), "server"; "/bin/help");
    pass!(["user ALL=/bin/hel* me"], "user" => root(), "server"; "/bin/help me");
    FAIL!(["user ALL=/bin/hel* me"], "user" => root(), "server"; "/bin/help me please");
    FAIL!(["user ALL=/bin/hel* me"], "user" => root(), "server"; "/bin/help only me");
    pass!(["user ALL=/bin/hel*"], "user" => root(), "server"; "/bin/help me please");
    pass!(["user ALL=/bin/hel* *"], "user" => root(), "server"; "/bin/help me please");
    pass!(["user ALL=/bin/hel* me *"], "user" => root(), "server"; "/bin/help me please");
    pass!(["user ALL=/bin/hel* me please *"], "user" => root(), "server"; "/bin/help me please");

    pass!(["user ALL=(ALL:ALL) /bin/foo"], "user" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::None]);
    pass!(["root ALL=(ALL:ALL) /bin/foo"], "root" => root(), "server"; "/bin/foo" => [authenticate: Authenticate::Nopasswd]);
    pass!(["user ALL=(ALL:ALL) /bin/foo"], "user" => request! { user, user }, "server"; "/bin/foo" => [authenticate: Authenticate::Nopasswd]);
    pass!(["user ALL=(ALL:ALL) /bin/foo"], "user" => request! { user, root }, "server"; "/bin/foo" => [authenticate: Authenticate::None]);

    assert_eq!(Named("user").as_gid(), GroupId::new(1466));
    pass!(["#1466 server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    pass!(["%#1466 server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    FAIL!(["#1466 server=(ALL:ALL) ALL"], "root" => root(), "server"; "/bin/hello");
    FAIL!(["%#1466 server=(ALL:ALL) ALL"], "root" => root(), "server"; "/bin/hello");
    pass!(["#1466,#1234,foo server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    pass!(["#1234,foo,#1466 server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    pass!(["foo,#1234,#1466 server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    FAIL!(["foo,#1234,#1366 server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    FAIL!(["#1366,#1234,foo server=(ALL:ALL) ALL"], "user" => root(), "server"; "/bin/hello");
    pass!(["user ALL=(ALL:#1466) /bin/foo"], "user" => request! { root, root }, "server"; "/bin/foo");
    FAIL!(["user ALL=(ALL:#1466) /bin/foo"], "user" => request! { root, other }, "server"; "/bin/foo");
    pass!(["user ALL=(ALL:#1466) /bin/foo"], "user" => request! { root, user }, "server"; "/bin/foo");
    pass!(["user ALL=(root,user:ALL) /bin/foo"], "user" => request! { root, wheel }, "server"; "/bin/foo");
    pass!(["user ALL=(root,user:ALL) /bin/foo"], "user" => request! { user, wheel }, "server"; "/bin/foo");
    FAIL!(["user ALL=(root,user:ALL) /bin/foo"], "user" => request! { sudo, wheel }, "server"; "/bin/foo");
    FAIL!(["user ALL=(#0:wheel) /bin/foo"], "user" => request! { sudo, wheel }, "server"; "/bin/foo");
    pass!(["user ALL=(#0:wheel) /bin/foo"], "user" => request! { root, root }, "server"; "/bin/foo");
    FAIL!(["user ALL=(%#1466:wheel) /bin/foo"], "user" => request! { root, root }, "server"; "/bin/foo");
    pass!(["user ALL=(%#1466:wheel) /bin/foo"], "user" => request! { user, user }, "server"; "/bin/foo");

    // tests with a 'singular' runas spec
    FAIL!(["user ALL=(ALL) /bin/foo"], "user" => request! { sudo, wheel }, "server"; "/bin/foo");
    pass!(["user ALL=(ALL) /bin/foo"], "user" => request! { sudo, sudo }, "server"; "/bin/foo");

    // tests without a runas spec
    FAIL!(["user ALL=/bin/foo"], "user" => request! { sudo, sudo }, "server"; "/bin/foo");
    FAIL!(["user ALL=/bin/foo"], "user" => request! { sudo, root }, "server"; "/bin/foo");
    FAIL!(["user ALL=/bin/foo"], "user" => request! { root, sudo }, "server"; "/bin/foo");
    pass!(["user ALL=/bin/foo"], "user" => request! { root, root }, "server"; "/bin/foo");

    // slightly counterintuitive test which simulates only -g being passed
    pass!(["user ALL=(sudo:sudo) /bin/foo"], "user" => request! { user, sudo }, "server"; "/bin/foo");

    // tests with multiple runas specs
    pass!(["user ALL=(root) /bin/ls, (sudo) /bin/true"], "user" => request! { root }, "server"; "/bin/ls");
    pass!(["user ALL=(root) NOPASSWD: /bin/ls, (sudo) /bin/true"], "user" => request! { sudo }, "server"; "/bin/true" => [authenticate: Authenticate::Nopasswd]);
    FAIL!(["user ALL=(root) /bin/ls, (sudo) /bin/true"], "user" => request! { sudo }, "server"; "/bin/ls");
    FAIL!(["user ALL=(root) /bin/ls, (sudo) /bin/true"], "user" => request! { root }, "server"; "/bin/true");
    pass!(["user ALL=(root) NOPASSWD: /bin/ls, (sudo) /bin/ls, /bin/true"], "user" => request! { sudo }, "server"; "/bin/true");

    SYNTAX!(["User_Alias, marc ALL = ALL"]);
    SYNTAX!(["Host_Alias CHROOT = server"]);
    SYNTAX!(["User_Alias CWD = user1"]);
    SYNTAX!(["User_Alias NOTAFTER = user1"]);
    SYNTAX!(["Runas_Alias NOTBEFORE = root"]);
    SYNTAX!(["Cmnd_Alias TIMEOUT = /bin/ls"]);
    SYNTAX!(["User_Alias ROLE = sudouser"]);
    SYNTAX!(["User_Alias TYPE = sudouser"]);

    pass!(["User_Alias FULLTIME=ALL,!marc","FULLTIME ALL=ALL"], "user" => root(), "server"; "/bin/bash");
    FAIL!(["User_Alias FULLTIME=ALL,!marc","FULLTIME ALL=ALL"], "marc" => root(), "server"; "/bin/bash");
    FAIL!(["User_Alias FULLTIME=ALL,!marc","ALL,!FULLTIME ALL=ALL"], "user" => root(), "server"; "/bin/bash");
    pass!(["User_Alias FULLTIME=ALL,!!!marc","ALL,!FULLTIME ALL=ALL"], "marc" => root(), "server"; "/bin/bash");
    pass!(["Host_Alias MACHINE=laptop,server","user MACHINE=ALL"], "user" => root(), "server"; "/bin/bash");
    pass!(["Host_Alias MACHINE=laptop,server","user MACHINE=ALL"], "user" => root(), "laptop"; "/bin/bash");
    FAIL!(["Host_Alias MACHINE=laptop,server","user MACHINE=ALL"], "user" => root(), "desktop"; "/bin/bash");
    pass!(["Cmnd_Alias WHAT=/bin/dd, /bin/rm","user ALL=WHAT"], "user" => root(), "server"; "/bin/rm");
    pass!(["Cmd_Alias WHAT=/bin/dd,/bin/rm","user ALL=WHAT"], "user" => root(), "laptop"; "/bin/dd");
    FAIL!(["Cmnd_Alias WHAT=/bin/dd,/bin/rm","user ALL=WHAT"], "user" => root(), "desktop"; "/bin/bash");

    pass!(["User_Alias A=B","User_Alias B=user","A ALL=ALL"], "user" => root(), "vm"; "/bin/ls");
    pass!(["Host_Alias A=B","Host_Alias B=vm","ALL A=ALL"], "user" => root(), "vm"; "/bin/ls");
    pass!(["Cmnd_Alias A=B","Cmnd_Alias B=/bin/ls","ALL ALL=A"], "user" => root(), "vm"; "/bin/ls");

    FAIL!(["Runas_Alias TIME=%wheel,!!sudo","user ALL=() ALL"], "user" => request!{ sudo, sudo }, "vm"; "/bin/ls");
    pass!(["Runas_Alias TIME=%wheel,!!sudo","user ALL=(TIME) ALL"], "user" => request! { sudo, sudo }, "vm"; "/bin/ls");
    FAIL!(["Runas_Alias TIME=%wheel,!!sudo","user ALL=(:TIME) ALL"], "user" => request! { sudo, sudo }, "vm"; "/bin/ls");
    pass!(["Runas_Alias TIME=%wheel,!!sudo","user ALL=(:TIME) ALL"], "user" => request! { user, sudo }, "vm"; "/bin/ls");
    pass!(["Runas_Alias TIME=%wheel,!!sudo","user ALL=(TIME) ALL"], "user" => request! { wheel, wheel }, "vm"; "/bin/ls");

    pass!(["Runas_Alias \\"," TIME=%wheel \\",",sudo # hallo","user ALL \\","=(TIME) ALL"], "user" => request! { wheel, wheel }, "vm"; "/bin/ls");

    // test the less-intuitive "substitution-like" alias mechanism
    FAIL!(["User_Alias FOO=!user", "ALL, FOO ALL=ALL"], "user" => root(), "vm"; "/bin/ls");
    pass!(["User_Alias FOO=!user", "!FOO ALL=ALL"], "user" => root(), "vm"; "/bin/ls");

    // quoting
    pass!(["a\\,b ALL=ALL"], "a,b" => request! { root, root }, "server"; "/bin/foo");
    pass!(["\"a,b\" ALL=ALL"], "a,b" => request! { root, root }, "server"; "/bin/foo");
    pass!(["\"a\\b\" ALL=ALL"], "a\\b" => request! { root, root }, "server"; "/bin/foo");

    // special chacters
    pass!(["foo@machine.name ALL=ALL"], "foo@machine.name" => request! { root, root }, "server"; "/bin/foo");
    pass!(["fnord$ ALL=ALL"], "fnord$" => request! { root, root }, "server"; "/bin/foo");
    pass!(["ALL ALL=/foo/command --bar=1"], "user" => request! { root, root }, "server"; "/foo/command --bar=1");

    // apparmor
    #[cfg(feature = "apparmor")]
    pass!(["ALL ALL=(ALL:ALL) APPARMOR_PROFILE=unconfined ALL"], "user" => root(), "server"; "/bin/bar" => [apparmor_profile: Some("unconfined".to_string())]);

    // list
    pass!(["ALL ALL=(ALL:ALL) /bin/ls, list"], "user" => root(), "server"; "list");
    FAIL!(["ALL ALL=(ALL:ALL) ALL, !list"], "user" => root(), "server"; "list");

    // line continuation
    pass!(["user ALL=/bin/hello \\", "arg"], "user" => root(), "server"; "/bin/hello arg");
    pass!(["user ALL=/bin/hello \\", "arg1 \\", "arg2"], "user" => root(), "server"; "/bin/hello arg1 arg2");
    pass!(
        ["user ALL=(root) NOPASSWD: /usr/bin/true \\", "arg1 \\", "arg2"],
        "user" => root(), "server";
        "/usr/bin/true arg1 arg2" => [authenticate: Authenticate::Nopasswd]
    );
    pass!(["user ALL=/bin/hello\\", "arg"], "user" => root(), "server"; "/bin/hello arg");
    pass!(
        ["user ALL=/bin/hello \\", "\\", "arg"],
        "user" => root(), "server";
        "/bin/hello arg"
    );
    SYNTAX!(["us\\\ner ALL=ALL"]);
    SYNTAX!(["user ALL=/bin/hello\\"]);
}

#[test]
fn default_bool_test() {
    let (mut sudoers, _) = analyze(
        Path::new("/etc/fakesudoers"),
        sudoer![
            "Defaults env_editor",
            "Defaults !use_pty",
            "Defaults use_pty",
            "Defaults !env_keep",
            "Defaults !secure_path",
            "Defaults !env_editor"
        ],
    );
    sudoers.specify_host_user_runas(
        &system::Hostname::fake("host"),
        &Named("user"),
        Some(&Named("root")),
    );

    assert!(!sudoers.settings.env_editor());
    assert!(sudoers.settings.use_pty());
    assert!(sudoers.settings.env_keep().is_empty());
    assert_eq!(sudoers.settings.secure_path(), None);
    assert!(!sudoers.settings.env_editor());
}

#[test]
fn default_set_test() {
    let (mut sudoers, _) = analyze(
        Path::new("/etc/fakesudoers"),
        sudoer![
            "Defaults env_keep = \"FOO HUK BAR\"",
            "Defaults env_keep -= HUK",
            "Defaults !env_check",
            "Defaults env_check += \"FOO\"",
            "Defaults env_check += \"XYZZY\"",
            "Defaults passwd_tries = 5",
            "Defaults secure_path = /etc"
        ],
    );
    sudoers.specify_host_user_runas(
        &system::Hostname::fake("host"),
        &Named("user"),
        Some(&Named("root")),
    );

    assert_eq!(
        sudoers.settings.env_keep(),
        &["FOO", "BAR"].into_iter().map(|x| x.to_string()).collect()
    );
    assert_eq!(
        sudoers.settings.env_check(),
        &["FOO", "XYZZY"]
            .into_iter()
            .map(|x| x.to_string())
            .collect()
    );
    assert_eq!(sudoers.settings.secure_path(), Some("/etc"));
    assert_eq!(sudoers.settings.passwd_tries(), 5);

    assert!(parse_string::<Sudo>("Defaults verifypw = \"sometimes\"").is_err());
    assert!(parse_string::<Sudo>("Defaults verifypw = sometimes").is_err());
    assert!(parse_string::<Sudo>("Defaults verifypw = never").is_ok());

    assert!(parse_string::<Sudo>("Defaults runcwd = *").is_ok());
    assert!(parse_string::<Sudo>("Defaults runcwd = /usr/local").is_ok());
    assert!(parse_string::<Sudo>("Defaults !runcwd").is_ok());
}

#[test]
fn default_multi_test() {
    let (mut sudoers, _) = analyze(
        Path::new("/etc/fakesudoers"),
        sudoer![
            "Defaults !env_editor, use_pty, env_keep = \"FOO BAR\", env_keep -= BAR, secure_path=/etc"
        ],
    );
    sudoers.specify_host_user_runas(
        &system::Hostname::fake("host"),
        &Named("user"),
        Some(&Named("root")),
    );

    assert!(!sudoers.settings.env_editor());
    assert!(sudoers.settings.use_pty());
    assert_eq!(sudoers.settings.secure_path(), Some("/etc"));
    assert_eq!(
        sudoers.settings.env_keep(),
        &["FOO".to_string()].into_iter().collect()
    );
}

#[test]
#[should_panic]
fn invalid_directive() {
    parse_eval::<ast::Sudo>("User_Alias, user Alias = user1, user2");
}

#[test]
#[should_panic = "embedded $ in username"]
fn invalid_username() {
    parse_eval::<ast::Sudo>("User_Alias FOO = $dollar");
}

#[test]
#[should_panic = "wildcards are not allowed in command arguments"]
fn wildcard_in_argument() {
    parse_eval::<ast::Sudo>("user ALL=/bin/hello w*");
}

#[test]
fn inclusive_username() {
    let UserSpecifier::User(Identifier::Name(sirin)) = parse_eval::<ast::UserSpecifier>("şirin")
    else {
        panic!();
    };

    assert_eq!(sirin, "şirin");
}

#[test]
fn sudoedit_recognized() {
    let CommandSpec(_, Qualified::Allow(Meta::Only((cmd, args)))) =
        parse_eval::<ast::CommandSpec>("sudoedit /etc/tmux.conf")
    else {
        panic!();
    };
    assert_eq!(cmd.as_str(), "sudoedit");
    let Args::Exact(args) = args else { panic!() };
    assert_eq!(args.as_ref(), &["/etc/tmux.conf"][..]);
}

#[test]
#[should_panic = "list does not take arguments"]
fn list_does_not_take_args() {
    parse_eval::<ast::CommandSpec>("list /etc/tmux.conf");
}

#[test]
fn directive_test() {
    let y = parse_eval::<Spec<UserSpecifier>>;
    match parse_eval::<ast::Sudo>("User_Alias HENK = user1, user2") {
        Sudo::Decl(Directive::UserAlias(defs)) => {
            let [Def(name, list)] = &defs[..] else {
                panic!("incorrectly parsed")
            };
            assert_eq!(name, "HENK");
            assert_eq!(*list, vec![y("user1"), y("user2")]);
        }
        _ => panic!("incorrectly parsed"),
    }

    match parse_eval::<ast::Sudo>("Runas_Alias FOO = foo : BAR = bar") {
        Sudo::Decl(Directive::RunasAlias(defs)) => {
            let [Def(name1, list1), Def(name2, list2)] = &defs[..] else {
                panic!("incorrectly parsed")
            };
            assert_eq!(name1, "FOO");
            assert_eq!(*list1, vec![y("foo")]);
            assert_eq!(name2, "BAR");
            assert_eq!(*list2, vec![y("bar")]);
        }
        _ => panic!("incorrectly parsed"),
    }
}

#[test]
// the overloading of '#' causes a lot of issues
fn hashsign_test() {
    assert!(parse_line("#42 ALL=ALL").is_spec());
    assert!(parse_line("ALL ALL=(#42) ALL").is_spec());
    assert!(parse_line("ALL ALL=(%#42) ALL").is_spec());
    assert!(parse_line("ALL ALL=(:#42) ALL").is_spec());
    assert!(parse_line("User_Alias FOO=#42, %#0, #3").is_decl());
    assert!(parse_line("").is_line_comment());
    assert!(parse_line("#this is a comment").is_line_comment());
    assert!(parse_line("#include foo").is_include());
    assert!(parse_line("#includedir foo").is_include_dir());
    assert_eq!("foo bar", parse_line("#include \"foo bar\"").as_include());
    // this is fine
    assert!(parse_line("#inlcudedir foo").is_line_comment());
    assert!(parse_line("@include foo").is_include());
    assert!(parse_line("@includedir foo").is_include_dir());
    assert_eq!("foo bar", parse_line("@include \"foo bar\"").as_include());
}

#[test]
fn gh674_at_include_quoted_backslash() {
    assert!(parse_line(r#"@include "/etc/sudo\ers" "#).is_include());
    assert!(parse_line(r#"@includedir "/etc/sudo\ers.d" "#).is_include_dir());
}

#[test]
fn gh676_percent_h_escape_unsupported() {
    let (_, errs) = analyze(
        Path::new("/etc/fakesudoers"),
        sudoer!(r#"@includedir "/etc/%h" "#),
    );
    assert_eq!(errs.len(), 1);
    assert_eq!(
        errs[0].message,
        "cannot open sudoers file /etc/%h: percent escape %h in includedir is unsupported"
    );
    assert_eq!(
        errs[0].location,
        Some(Span {
            start: (1, 2),
            end: (1, 23)
        })
    );
}

#[test]
fn gh1295_escaped_equal_argument_ok() {
    assert!(try_parse_line("Cmd_Alias FOO_CMD = /bin/foo --bar=1").is_some());
    assert!(try_parse_line(r"Cmd_Alias FOO_CMD = /bin/foo --bar\=1").is_some());
}

#[test]
fn gh1466_hostname_cannot_be_ip() {
    assert!(try_parse_line("ALL 192.168.0.foo=ALL").is_some());
    assert!(try_parse_line("ALL 1ba.168.0.0=ALL").is_some());
    assert!(try_parse_line("ALL 192.168.0.1.5=ALL").is_some());
    assert!(try_parse_line("ALL 192.1682.0.1=ALL").is_some());
    assert!(try_parse_line("ALL 192.168.0.1=ALL").is_none());
    assert!(try_parse_line("ALL 192.168.0=ALL").is_none());
    assert!(try_parse_line("ALL 192.168=ALL").is_none());
    assert!(try_parse_line("ALL 192=ALL").is_none());
}

#[test]
fn hashsign_error() {
    assert!(parse_line("#include foo bar").is_line_comment());
}

#[test]
fn include_regression() {
    assert!(try_parse_line("#4,#include foo").is_none());
}

#[test]
fn nullbyte_regression() {
    assert!(try_parse_line("ferris ALL=(ALL:ferris\0) ALL").is_none());
}

#[test]
fn reserved_alias_names_are_rejected_for_alias_definitions() {
    for line in [
        "User_Alias ALL = sudouser",
        "User_Alias CWD = sudouser",
        "Host_Alias CHROOT = server",
        "Cmnd_Alias TIMEOUT = /bin/ls",
        "Runas_Alias NOTBEFORE = root",
        "User_Alias NOTAFTER = sudouser",
    ] {
        assert!(try_parse_line(line).is_none(), "{line}");
    }
}

#[test]
fn defaults_regression() {
    assert!(try_parse_line("Defaults .mymachine=ALL").is_none())
}

#[test]
fn user_id_regression() {
    assert!(parse_line("#499999999999 ALL=ALL").is_line_comment());
    assert!(parse_line("#49999999999 ALL=ALL").is_line_comment());
    assert!(try_parse_line("Defaults:#49999999999 use_pty").is_none());
    assert!(try_parse_line("Defaults:#4999999999 use_pty").is_none());
    assert!(parse_line("#1999999999 ALL=ALL").is_spec());
    assert!(parse_line("Defaults:#1999999999 use_pty").is_decl());
}

#[test]
fn specific_defaults() {
    assert!(parse_line("Defaults !use_pty").is_decl());
    assert!(try_parse_line("Defaults!use_pty").is_none());
    assert!(parse_line("Defaults!/bin/bash !use_pty").is_decl());
    assert!(try_parse_line("Defaults!/bin/bash!use_pty").is_none());
    assert!(try_parse_line("Defaults !/bin/bash !use_pty").is_none());
    assert!(try_parse_line("Defaults !/bin/bash").is_none());
    assert!(parse_line("Defaults@host !use_pty").is_decl());
    assert!(parse_line("Defaults@host!use_pty").is_decl());
    assert!(parse_line("Defaults@host,!host2 !use_pty").is_decl());
    assert!(parse_line("Defaults@!host,host2!use_pty").is_decl());
    assert!(try_parse_line("Defaults @host!use_pty").is_none());
    assert!(try_parse_line("Defaults @host !use_pty").is_none());
    assert!(parse_line("Defaults:user !use_pty").is_decl());
    assert!(parse_line("Defaults:user!use_pty").is_decl());
    assert!(try_parse_line("Defaults :user!use_pty").is_none());
    assert!(try_parse_line("Defaults :user !use_pty").is_none());
    assert!(parse_line("Defaults>user !use_pty").is_decl());
    assert!(parse_line("Defaults>user!use_pty").is_decl());
    assert!(try_parse_line("Defaults >user!use_pty").is_none());
    assert!(try_parse_line("Defaults >user !use_pty").is_none());
}

#[test]
fn defaults_hostname_shenanigans() {
    assert!(parse_line("Defaults@ hostname use_pty").is_decl());
    assert!(try_parse_line("Defaults@ # comment\n use_pty").is_none());
    /* ogsudo also rejects the following, probably due to hack in the parser
    not too dissimilar from ours -- but sudo-rs has no problems with these */
    assert!(parse_line("Defaults@\\\nhost use_pty").is_decl());
    assert!(parse_line("Defaults:\\\nuser use_pty").is_decl());
    assert!(parse_line("Defaults>\\\nuser use_pty").is_decl());
    assert!(parse_line("Defaults!\\\nCMD use_pty").is_decl());
}

#[test]
fn at_sign_ambiguity() {
    assert!(parse_line("Defaults@host env_keep=ALL").is_decl());
    assert!(parse_line("defaults@host env_keep=ALL").is_spec());
}

#[test]
fn default_specific_test() {
    let sudoers = || {
        analyze(
            Path::new("/etc/fakesudoers"),
            sudoer![
                "Defaults!RR use_pty",
                "Defaults env_editor",
                "Defaults@host !env_editor",
                "Defaults !use_pty",
                "Defaults:user use_pty",
                "Defaults !secure_path",
                "Defaults>runas secure_path=\"/bin\"",
                "Defaults!/bin/foo !env_keep",
                "Cmnd_Alias RR=/usr/bin/rr twice"
            ],
        )
    };

    let (mut base_sudoers, _) = sudoers();
    base_sudoers.specify_host_user_runas(
        &system::Hostname::fake("generic"),
        &Named("generic"),
        Some(&Named("generic")),
    );

    assert!(base_sudoers.settings.env_editor());
    assert!(!base_sudoers.settings.use_pty());
    assert!(base_sudoers.settings.env_keep().contains("COLORS"));
    assert_eq!(base_sudoers.settings.secure_path(), None);

    let (mut mod_sudoers, _) = sudoers();
    mod_sudoers.specify_host_user_runas(
        &system::Hostname::fake("host"),
        &Named("user"),
        Some(&Named("root")),
    );
    assert!(!mod_sudoers.settings.env_editor());
    assert!(mod_sudoers.settings.use_pty());
    assert!(mod_sudoers.settings.env_keep().contains("COLORS"));
    assert_eq!(mod_sudoers.settings.secure_path(), None);

    let (mut mod_sudoers, _) = sudoers();
    mod_sudoers.specify_host_user_runas(
        &system::Hostname::fake("machine"),
        &Named("admin"),
        Some(&Named("runas")),
    );
    assert!(mod_sudoers.settings.env_editor());
    assert!(!mod_sudoers.settings.use_pty());
    assert!(mod_sudoers.settings.env_keep().contains("COLORS"));
    assert_eq!(mod_sudoers.settings.secure_path(), Some("/bin"));
    mod_sudoers.specify_command(Path::new("/bin/foo"), &["".into(), "a".into()]);
    assert!(mod_sudoers.settings.env_keep().is_empty());

    let (mut mod_sudoers, _) = sudoers();
    mod_sudoers.specify_host_user_runas(
        &system::Hostname::fake("machine"),
        &Named("admin"),
        Some(&Named("self")),
    );
    mod_sudoers.specify_command(Path::new("/usr/bin/rr"), &["thrice".into()]);
    assert!(mod_sudoers.settings.env_editor());
    assert!(!mod_sudoers.settings.use_pty());
    assert!(mod_sudoers.settings.env_keep().contains("COLORS"));
    assert_eq!(mod_sudoers.settings.secure_path(), None);

    let (mut mod_sudoers, _) = sudoers();
    mod_sudoers.specify_command(Path::new("/usr/bin/rr"), &["twice".into()]);
    assert!(mod_sudoers.settings.use_pty());
}

#[test]
fn useralias_underscore_regression() {
    let sudo = parse_line("FOO_BAR ALL=ALL");
    let spec = sudo.as_spec().expect("`Sudo::Spec`");
    assert!(
        spec.users[0]
            .as_allow()
            .expect("`Qualified::Allow`")
            .is_alias()
    );
}

#[test]
fn regression_check_recursion() {
    let (_, error) = analyze(
        Path::new("/etc/fakesudoers"),
        sudoer!["User_Alias A=user, B", "User_Alias B=A"],
    );

    assert!(!error.is_empty());
}

fn test_topo_sort(n: usize) {
    let alias = |s: &str| Qualified::Allow(Meta::<UserSpecifier>::Alias(s.to_string()));
    let stop = || Qualified::Allow(Meta::<UserSpecifier>::All);
    type Elem = Spec<UserSpecifier>;
    let test_case = |x1: Elem, x2: Elem, x3: Elem| {
        let table = vec![
            Def("AAP".to_string(), vec![x1]),
            Def("NOOT".to_string(), vec![x2]),
            Def("MIES".to_string(), vec![x3]),
        ];
        let mut err = vec![];
        let order = sanitize_alias_table(&table, &mut err);
        assert!(err.is_empty());
        let mut seen = HashSet::new();
        for Def(id, defns) in order.iter().map(|&i| &table[i]) {
            if defns.iter().any(|spec| {
                let Qualified::Allow(Meta::Alias(id2)) = spec else {
                    return false;
                };
                !seen.contains(id2)
            }) {
                panic!("forward reference encountered after sorting");
            }
            seen.insert(id);
        }
    };
    match n {
        0 => test_case(alias("AAP"), alias("NOOT"), stop()),
        1 => test_case(alias("AAP"), stop(), alias("NOOT")),
        2 => test_case(alias("NOOT"), alias("AAP"), stop()),
        3 => test_case(alias("NOOT"), stop(), alias("AAP")),
        4 => test_case(stop(), alias("AAP"), alias("NOOT")),
        5 => test_case(stop(), alias("NOOT"), alias("AAP")),
        _ => panic!("error in test case"),
    }
}

#[test]
fn test_topo_positive() {
    test_topo_sort(3);
    test_topo_sort(4);
}

#[test]
#[should_panic]
fn test_topo_fail0() {
    test_topo_sort(0);
}
#[test]
#[should_panic]
fn test_topo_fail1() {
    test_topo_sort(1);
}
#[test]
#[should_panic]
fn test_topo_fail2() {
    test_topo_sort(2);
}
#[test]
#[should_panic]
fn test_topo_fail5() {
    test_topo_sort(5);
}

fn fuzz_topo_sort(siz: usize) {
    for mut n in 0..(1..siz).reduce(|x, y| x * y).unwrap() {
        let name = |s: u8| std::str::from_utf8(&[65 + s]).unwrap().to_string();
        let alias = |s: String| Qualified::Allow(Meta::<UserSpecifier>::Alias(s));
        let stop = || Qualified::Allow(Meta::<UserSpecifier>::All);

        let mut data = (0..siz - 1)
            .map(|i| alias(name(i as u8)))
            .collect::<Vec<_>>();
        data.push(stop());

        for i in (1..=siz).rev() {
            data.swap(i - 1, n % i);
            n /= i;
        }

        let table = data
            .into_iter()
            .enumerate()
            .map(|(i, x)| Def(name(i as u8), vec![x]))
            .collect();

        let mut err = vec![];
        let order = sanitize_alias_table(&table, &mut err);
        if !err.is_empty() {
            return;
        }

        let mut seen = HashSet::new();
        for Def(id, defns) in order.iter().map(|&i| &table[i]) {
            if defns.iter().any(|spec| {
                let Qualified::Allow(Meta::Alias(id2)) = spec else {
                    return false;
                };
                !seen.contains(id2)
            }) {
                panic!("forward reference encountered after sorting");
            }
            seen.insert(id);
        }
        assert!(seen.len() == siz);
    }
}

#[test]
fn fuzz_topo_sort7() {
    fuzz_topo_sort(7)
}
