use super::*;
use shuck_ast::Command;

#[test]
fn function_header_fact_span_in_source_stops_at_header() {
    let source = "#!/bin/bash\nfunction wrapped()\n{\n  printf '%s\\n' hi\n}\n";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .first()
            .expect("expected function header fact");

        assert_eq!(
            header.span_in_source(source).slice(source),
            "function wrapped()"
        );
    });
}

#[test]
fn function_header_fact_tracks_binding_scope_and_call_arity() {
    let source = "#!/bin/sh\ngreet ok\ngreet() { echo \"$1\"; }\ngreet\n";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "greet")
            })
            .expect("expected greet header fact");

        assert!(header.binding_id().is_some());
        assert!(header.function_scope().is_some());
        assert_eq!(header.call_arity().call_count(), 2);
        assert_eq!(header.call_arity().min_arg_count(), Some(0));
        assert_eq!(header.call_arity().max_arg_count(), Some(1));
        assert_eq!(header.call_arity().zero_arg_call_spans().len(), 1);
        assert_eq!(
            header.call_arity().zero_arg_call_spans()[0].slice(source),
            "greet"
        );
    });
}

#[test]
fn function_definition_command_lookup_returns_header_command() {
    let source = "#!/bin/sh\ngreet() { echo hi; }\n";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "greet")
            })
            .expect("expected greet header fact");
        let scope = header
            .function_scope()
            .expect("expected greet function scope");
        let definition_command = facts
            .command_facts()
            .function_definition_command(scope)
            .expect("expected definition command");

        assert_eq!(definition_command.id(), header.command_id());
        assert!(matches!(definition_command.command(), Command::Function(_)));
    });
}

#[test]
fn command_for_name_word_span_resolves_definition_and_call_commands() {
    let source = "#!/bin/sh\ngreet() { echo hi; }\ngreet\n";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "greet")
            })
            .expect("expected greet header fact");
        let definition_command = facts.command_facts().command(header.command_id());
        let call_name_span = header.call_arity().zero_arg_call_spans()[0];

        assert_eq!(
            facts
                .command_facts()
                .command_for_name_word_span(definition_command.span())
                .map(|command| command.id()),
            Some(header.command_id())
        );
        assert_eq!(
            facts
                .command_facts()
                .command_for_name_word_span(call_name_span)
                .and_then(|command| command.body_name_word().map(|word| word.span.slice(source))),
            Some("greet")
        );
    });
}

#[test]
fn function_header_fact_tracks_call_arity_inside_parameter_expansion_defaults() {
    let source = "\
#!/usr/bin/env bash
GetBuildVersion() {
  local build_revision=\"${1}\"
  printf '%s\n' \"$build_revision\"
}
BUILD_VERSION=\"${BUILD_VERSION:-\"$(GetBuildVersion \"${BUILD_REVISION}\")\"}\"
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "GetBuildVersion")
            })
            .expect("expected GetBuildVersion header fact");

        assert_eq!(header.call_arity().call_count(), 1);
        assert_eq!(header.call_arity().min_arg_count(), Some(1));
        assert_eq!(header.call_arity().max_arg_count(), Some(1));
        assert!(header.call_arity().zero_arg_call_spans().is_empty());
    });
}

#[test]
fn function_header_fact_ignores_wrapper_resolved_targets_for_call_arity() {
    let source = "\
#!/usr/bin/env bash
greet() { printf '%s\n' \"$1\"; }
command greet ok
greet
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "greet")
            })
            .expect("expected greet header fact");

        assert_eq!(header.call_arity().call_count(), 1);
        assert_eq!(header.call_arity().min_arg_count(), Some(0));
        assert_eq!(header.call_arity().max_arg_count(), Some(0));
        assert_eq!(header.call_arity().zero_arg_call_spans().len(), 1);
        assert_eq!(
            header.call_arity().zero_arg_call_spans()[0].slice(source),
            "greet"
        );
    });
}

#[test]
fn function_header_fact_counts_quoted_static_calls_in_call_arity() {
    let source = "\
#!/usr/bin/env bash
greet() { printf '%s\n' \"$1\"; }
\"greet\" ok
greet
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "greet")
            })
            .expect("expected greet header fact");

        assert_eq!(header.call_arity().call_count(), 2);
        assert_eq!(header.call_arity().min_arg_count(), Some(0));
        assert_eq!(header.call_arity().max_arg_count(), Some(1));
        assert_eq!(header.call_arity().zero_arg_call_spans().len(), 1);
        assert_eq!(
            header.call_arity().zero_arg_call_spans()[0].slice(source),
            "greet"
        );
    });
}

#[test]
fn function_header_fact_tracks_zero_arg_backtick_calls() {
    let source = "\
#!/bin/sh
greet() { printf '%s\n' \"$1\"; }
value=\"`greet`\"
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "greet")
            })
            .expect("expected greet header fact");

        assert_eq!(header.call_arity().call_count(), 1);
        assert_eq!(header.call_arity().min_arg_count(), Some(0));
        assert_eq!(header.call_arity().max_arg_count(), Some(0));
        assert_eq!(header.call_arity().zero_arg_call_spans().len(), 1);
        assert_eq!(
            header.call_arity().zero_arg_call_spans()[0].slice(source),
            "greet"
        );
    });
}

#[test]
fn function_header_fact_expands_zero_arg_call_spans_over_redirects() {
    let source = "\
#!/bin/sh
greet() { printf '%s\n' \"$1\"; }
greet >out
greet <<EOF
hi
EOF
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "greet")
            })
            .expect("expected greet header fact");

        let spans = header
            .call_arity()
            .zero_arg_diagnostic_spans()
            .iter()
            .map(|span| span.slice(source))
            .collect::<Vec<_>>();

        assert_eq!(spans, vec!["greet >out", "greet <<EOF"]);
    });
}

#[test]
fn function_cli_dispatch_facts_track_case_positional_entrypoints_with_top_level_exit() {
    let source = "\
#!/bin/sh
start() { echo hi; }
stop() { echo bye; }
case \"$1\" in
  start) $1 ;;
  stop) \"$1\" ;;
esac
exit $?
";

    with_facts(source, None, |_, facts| {
        for name in ["start", "stop"] {
            let header = facts
                .command_facts()
                .function_headers()
                .iter()
                .find(|header| {
                    header
                        .static_name_entry()
                        .is_some_and(|(candidate, _)| candidate == name)
                })
                .expect("expected dispatched function header");
            let scope = header.function_scope().expect("expected function scope");
            let dispatch = facts.command_facts().function_cli_dispatch_facts(scope);

            assert!(
                dispatch.exported_from_case_cli(),
                "expected {name} to be marked"
            );
            assert_eq!(
                dispatch
                    .dispatcher_span()
                    .expect("expected dispatcher span")
                    .slice(source),
                if name == "start" { "$1" } else { "\"$1\"" }
            );
        }
    });
}

#[test]
fn case_cli_reachable_function_scopes_track_dispatchable_functions() {
    let source = "\
#!/bin/sh
start() { echo hi; }
case \"$1\" in
  start) \"$1\" ;;
esac
exit $?
late() { echo later; }
";

    with_facts(source, None, |_, facts| {
        let start = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "start")
            })
            .expect("expected start header fact");
        let late = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(name, _)| name == "late")
            })
            .expect("expected late header fact");

        assert!(facts.command_facts().is_case_cli_reachable_function_scope(
            start.function_scope().expect("expected start scope")
        ));
        assert!(!facts.command_facts().is_case_cli_reachable_function_scope(
            late.function_scope().expect("expected late scope")
        ));
    });
}

#[test]
fn case_cli_reachable_function_scopes_count_anonymous_parent_functions() {
    let source = "\
#!/bin/zsh
case \"$1\" in
  start) \"$1\" ;;
esac
exit $?
function {
  helper() { echo helper; }
}
";

    with_facts_dialect(
        source,
        None,
        ParseShellDialect::Zsh,
        ShellDialect::Zsh,
        |_, facts| {
            let helper = facts
                .command_facts()
                .function_headers()
                .iter()
                .find(|header| {
                    header
                        .static_name_entry()
                        .is_some_and(|(name, _)| name == "helper")
                })
                .expect("expected helper header fact");

            assert!(facts.command_facts().is_case_cli_reachable_function_scope(
                helper.function_scope().expect("expected helper scope")
            ));
        },
    );
}

#[test]
fn function_cli_dispatch_facts_track_case_positional_entrypoints_with_literal_top_level_exit() {
    let source = "\
#!/bin/sh
start() { echo hi; }
case \"$1\" in
  start) $1 ;;
esac
exit 0
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(candidate, _)| candidate == "start")
            })
            .expect("expected start header");
        let scope = header.function_scope().expect("expected function scope");

        assert!(
            facts
                .command_facts()
                .function_cli_dispatch_facts(scope)
                .exported_from_case_cli()
        );
    });
}

#[test]
fn function_cli_dispatch_facts_ignore_case_positional_entrypoints_without_top_level_exit() {
    let source = "\
#!/bin/sh
start() { echo hi; }
case \"$1\" in
  start) $1 ;;
esac
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(candidate, _)| candidate == "start")
            })
            .expect("expected start header");
        let scope = header.function_scope().expect("expected function scope");

        assert!(
            !facts
                .command_facts()
                .function_cli_dispatch_facts(scope)
                .exported_from_case_cli()
        );
    });
}

#[test]
fn function_cli_dispatch_facts_ignore_static_case_calls() {
    let source = "\
#!/bin/sh
start() { echo hi; }
case \"$1\" in
  start) start ;;
esac
exit $?
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(candidate, _)| candidate == "start")
            })
            .expect("expected start header");
        let scope = header.function_scope().expect("expected function scope");

        assert!(
            !facts
                .command_facts()
                .function_cli_dispatch_facts(scope)
                .exported_from_case_cli()
        );
    });
}

#[test]
fn function_cli_dispatch_facts_ignore_later_defined_functions() {
    let source = "\
#!/bin/sh
case \"$1\" in
  start) $1 ;;
esac
exit $?

start() { echo hi; }
";

    with_facts(source, None, |_, facts| {
        let header = facts
            .command_facts()
            .function_headers()
            .iter()
            .find(|header| {
                header
                    .static_name_entry()
                    .is_some_and(|(candidate, _)| candidate == "start")
            })
            .expect("expected start header");
        let scope = header.function_scope().expect("expected function scope");

        assert!(
            !facts
                .command_facts()
                .function_cli_dispatch_facts(scope)
                .exported_from_case_cli()
        );
    });
}

#[test]
fn builds_function_style_spans() {
    let source = "\
#!/bin/bash
f() [[ -n \"$x\" ]]
p() if true; then :; fi
q() case x in x) :;; esac
r() for x in y; do :; done
s() while true; do :; done
t() until false; do :; done
u() ( echo hi )
v() (( x++ ))
ok() {
  false
  return $?
}
g() {
  if cond; then
    false
    return $?
  fi
}
h() {
  if cond; then
    false
    return $?
  fi
  echo done
}
i() {
  false
  return $? 5
}
j() {
  false
  x=1 return $?
}
k() {
  false
  return $? >out
}
l() {
  ! {
    false
    return $?
  }
}
m() {
  {
    false
    return $?
  } &
}
n() {
  if cond; then
    false
  fi
  return $?
}
o() {
  : | false
  return $?
}
";

    with_facts(source, None, |_, facts| {
        assert_eq!(
            facts
                .command_facts()
                .function_body_without_braces_spans()
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec![
                "[[ -n \"$x\" ]]",
                "if true; then :; fi\n",
                "case x in x) :;; esac\n",
                "for x in y; do :; done",
                "while true; do :; done\n",
                "until false; do :; done\n",
            ]
        );
        assert_eq!(
            facts
                .command_facts()
                .redundant_return_status_spans()
                .iter()
                .map(|span| span.slice(source))
                .collect::<Vec<_>>(),
            vec!["  return $?\n"]
        );
    });
}

#[test]
fn marks_commands_inside_completion_registration_chain_functions() {
    let source = "\
#!/bin/bash
_comp_cmd_hostname() {
  [[ $cur == -* ]] && _comp_compgen_help || _comp_compgen_usage
} &&
  complete -F _comp_cmd_hostname hostname
_comp_cmd_later() {
  [[ $cur == -* ]] && _comp_compgen_help || _comp_compgen_usage
}
complete -F _comp_cmd_later later
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert!(!flagged_lines.is_empty());
        assert!(flagged_lines.iter().all(|line| *line == 3));
    });
}

#[test]
fn marks_commands_inside_zsh_compdef_registered_functions() {
    let source = "\
#!/bin/zsh
__grunt() {
  print -r -- $verbose
}
compdef __grunt grunt
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert!(!flagged_lines.is_empty());
        assert!(flagged_lines.iter().all(|line| *line == 3));
    });
}

#[test]
fn ignores_zsh_compdef_registrations_inside_function_bodies() {
    let source = "\
#!/bin/zsh
__grunt() {
  print -r -- $verbose
}
setup_completion() {
  compdef __grunt grunt
}
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert_eq!(flagged_lines, Vec::<usize>::new());
    });
}

#[test]
fn ignores_zsh_compdef_for_conditionally_defined_functions() {
    let source = "\
#!/bin/zsh
if [[ -n $commands[grunt] ]]; then
  __grunt() {
    print -r -- $verbose
  }
fi
compdef __grunt grunt
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert_eq!(flagged_lines, Vec::<usize>::new());
    });
}

#[test]
fn ignores_zsh_compdef_service_aliases_for_function_matching() {
    let source = "\
#!/bin/zsh
grunt() {
  print -r -- $verbose
}
compdef __grunt=grunt
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert_eq!(flagged_lines, Vec::<usize>::new());
    });
}

#[test]
fn ignores_zsh_compdef_deletion_modes_for_function_matching() {
    let source = "\
#!/bin/zsh
grunt() {
  print -r -- $verbose
}
compdef -d grunt
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert_eq!(flagged_lines, Vec::<usize>::new());
    });
}

#[test]
fn ignores_conditionally_executed_zsh_compdef_registrations() {
    let source = "\
#!/bin/zsh
__grunt() {
  print -r -- $verbose
}
[[ -n $commands[grunt] ]] && compdef __grunt grunt
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert_eq!(flagged_lines, Vec::<usize>::new());
    });
}

#[test]
fn marks_zsh_compdef_functions_after_option_operands() {
    let source = "\
#!/bin/zsh
__grunt() {
  print -r -- $verbose
}
compdef -P 'grunt-*' __grunt grunt
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert!(!flagged_lines.is_empty());
        assert!(flagged_lines.iter().all(|line| *line == 3));
    });
}

#[test]
fn marks_zsh_compdef_functions_after_pattern_and_name_mode_options() {
    let source = "\
#!/bin/zsh
__grunt() {
  print -r -- $verbose
}
compdef -P 'grunt-*' -N __grunt grunt
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert!(!flagged_lines.is_empty());
        assert!(flagged_lines.iter().all(|line| *line == 3));
    });
}

#[test]
fn marks_later_zsh_functions_registered_by_earlier_compdef() {
    let source = "\
#!/bin/zsh
compdef __grunt grunt
__grunt() {
  print -r -- $verbose
}
";

    with_facts(source, None, |_, facts| {
        let flagged_lines = facts
            .commands()
            .iter()
            .filter(|command| {
                facts
                    .command_facts()
                    .command_is_in_completion_registered_function(command.id())
            })
            .map(|command| command.span().start.line())
            .collect::<Vec<_>>();

        assert!(!flagged_lines.is_empty());
        assert!(flagged_lines.iter().all(|line| *line == 4));
    });
}

#[test]
fn marks_zsh_widget_and_hook_functions_as_external_entrypoints() {
    let source = "\
#!/bin/zsh
single_operand_widget() { print -r -- \"$1\"; }
widget_impl() { print -r -- \"$1\"; }
precmd_refresh() { (( $# )) && print -r -- \"$1\"; }
precmd() { print -r -- \"$1\"; }
zsh_directory_name() { print -r -- \"$1\"; }
not_external() { print -r -- \"$1\"; }
dynamic_widget() { print -r -- \"$1\"; }
removed_precmd() { print -r -- \"$1\"; }
removed_chpwd() { print -r -- \"$1\"; }
deleted_widget_impl() { print -r -- \"$1\"; }
shared_widget_impl() { print -r -- \"$1\"; }
latent_widget_impl() { print -r -- \"$1\"; }
latent_hook_impl() { print -r -- \"$1\"; }
pattern_removed_hook() { print -r -- \"$1\"; }
pattern_kept_hook() { print -r -- \"$1\"; }
zle_hook_widget_impl() { print -r -- \"$1\"; }
zle_hook_backing_impl() { print -r -- \"$1\"; }
bracket_removed_hook() { print -r -- \"$1\"; }
number_removed_hook_12() { print -r -- \"$1\"; }
negated_class_removed_hook() { print -r -- \"$1\"; }
negated_class_kept_hook() { print -r -- \"$1\"; }
zle -N single_operand_widget
zle -N widget-name widget_impl
zle -N deleted-widget deleted_widget_impl
zle -D deleted-widget
zle -N first-widget shared_widget_impl
zle -N second-widget shared_widget_impl
zle -D first-widget
add-zsh-hook -Uz precmd precmd_refresh
add-zsh-hook chpwd removed_chpwd
add-zsh-hook -d precmd removed_precmd
add-zsh-hook -UD chpwd removed_chpwd
add-zsh-hook precmd pattern_removed_hook
add-zsh-hook precmd pattern_kept_hook
add-zsh-hook -D precmd 'pattern_removed_*'
add-zle-hook-widget line-init zle_hook_widget_impl
zle -N zle-hook-widget-alias zle_hook_backing_impl
add-zle-hook-widget line-init zle-hook-widget-alias
add-zsh-hook precmd bracket_removed_hook
add-zsh-hook precmd number_removed_hook_12
add-zsh-hook precmd negated_class_removed_hook
add-zsh-hook precmd negated_class_kept_hook
add-zsh-hook -D precmd 'bracket_removed_[hr]ook'
add-zsh-hook -D precmd 'number_removed_hook_<->'
add-zsh-hook -D precmd 'negated_class_[^k]*'
zle -N \"$widget_name\" dynamic_widget
setup_widget() { zle -N latent-widget latent_widget_impl; }
setup_hook() { add-zsh-hook precmd latent_hook_impl; }
";

    with_facts_dialect(
        source,
        None,
        ParseShellDialect::Zsh,
        ShellDialect::Zsh,
        |_, facts| {
            let external_names = facts
                .command_facts()
                .function_headers()
                .iter()
                .filter_map(|header| {
                    let scope = header.function_scope()?;
                    facts
                        .command_facts()
                        .function_is_external_entrypoint(scope)
                        .then(|| header.static_name_entry().map(|(name, _)| name.as_str()))
                        .flatten()
                })
                .collect::<Vec<_>>();

            assert_eq!(
                external_names,
                vec![
                    "single_operand_widget",
                    "widget_impl",
                    "precmd_refresh",
                    "precmd",
                    "zsh_directory_name",
                    "shared_widget_impl",
                    "pattern_kept_hook",
                    "zle_hook_widget_impl",
                    "zle_hook_backing_impl",
                    "negated_class_kept_hook"
                ]
            );
        },
    );
}
