use crate::Token;
use crate::char_string::char_string;
use crate::expr::{All, Expr, LongestMatchOf, OwnedExprExt, SequenceExpr, UnlessStep};
use crate::patterns::DerivedFrom;
use crate::patterns::WordSet;

use super::{ExprLinter, Lint, LintKind, Suggestion};
use crate::linting::expr_linter::Chunk;

pub struct NeedToNoun {
    expr: All,
}

impl Default for NeedToNoun {
    fn default() -> Self {
        let postfix_exceptions = LongestMatchOf::new(vec![
            Box::new(|tok: &Token, _: &[char]| {
                tok.kind.is_adverb()
                    || tok.kind.is_determiner()
                    || tok.kind.is_unlintable()
                    || tok.kind.is_pronoun()
            }) as Box<dyn Expr>,
            Box::new(WordSet::new(&["about", "into", "it"])),
        ]);

        let exceptions = SequenceExpr::anything()
            .t_any()
            .t_any()
            .t_any()
            .then_word_set(&["be", "match"]);

        let a = SequenceExpr::default()
            .then_kind_where(|kind| kind.is_nominal() && !kind.is_likely_homograph())
            .t_ws()
            .then_unless(postfix_exceptions);

        // Bare words after infinitive `to` are the hardest cases to disambiguate.
        // If the token is a noun/verb homograph, prefer not linting over inserting
        // `the` into a potentially valid verb phrase.
        let b = SequenceExpr::default().then_kind_where(|kind| {
            kind.is_nominal() && !kind.is_verb() && !kind.is_likely_homograph()
        });

        let expr = SequenceExpr::with(DerivedFrom::new_from_str("need"))
            .t_ws()
            .t_aco("to")
            .t_ws()
            .then(a.or(b));

        Self {
            expr: expr.and(UnlessStep::new(exceptions, |_: &Token, _: &[char]| true)),
        }
    }
}

impl ExprLinter for NeedToNoun {
    type Unit = Chunk;

    fn expr(&self) -> &dyn Expr {
        &self.expr
    }

    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
        let to_idx = 2;
        let to_token = &matched_tokens[to_idx];

        let noun_idx = 4;
        let noun_token = &matched_tokens[noun_idx];

        let noun_text = noun_token.get_str(source);
        let span = to_token.span;

        Some(Lint {
            span,
            lint_kind: LintKind::Grammar,
            suggestions: vec![Suggestion::ReplaceWith(char_string!("the").to_vec())],
            message: format!(
                "`need to` should be followed by a verb, not a noun or pronoun like `{noun_text}`."
            ),
            priority: 48,
        })
    }

    fn description(&self) -> &'static str {
        "Flags `need to` when it is immediately followed by a noun, which usually means the infinitive verb is missing."
    }
}

#[cfg(test)]
mod tests {
    use crate::linting::pooled_linter::for_tests::create_test_pool;

    create_test_pool!(NeedToNoun, NeedToNoun, NeedToNoun::default());
    use super::NeedToNoun;
    use crate::linting::tests::{assert_lint_count, assert_no_lints, assert_suggestion_result};

    #[test]
    fn flags_need_to_noun() {
        assert_suggestion_result(
            "I need to information now.",
            test_linter(),
            "I need the information now.",
        );
    }

    #[test]
    fn allows_need_to_verb() {
        assert_lint_count("I need to leave now.", test_linter(), 0);
    }

    #[test]
    fn allows_need_to_finish() {
        assert_lint_count(
            "I need to finish this report by tomorrow.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn allows_need_to_call() {
        assert_lint_count("You need to call your mother tonight.", test_linter(), 0);
    }

    #[test]
    fn allows_need_to_talk() {
        assert_lint_count("We need to talk about the budget.", test_linter(), 0);
    }

    #[test]
    fn allows_need_to_leave() {
        assert_lint_count(
            "They need to leave early to catch the train.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn allows_need_to_practice() {
        assert_lint_count(
            "She needs to practice her German more often.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn allows_need_to_fix() {
        assert_lint_count(
            "He needs to fix his bike before the weekend.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn allows_need_to_decide() {
        assert_lint_count(
            "We need to decide where to go for dinner.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn allows_need_to_update() {
        assert_lint_count("You need to update your password.", test_linter(), 0);
    }

    #[test]
    fn allows_need_to_take() {
        assert_lint_count(
            "I need to take a break and get some fresh air.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn allows_need_to_clean() {
        assert_lint_count(
            "They need to clean the house before guests arrive.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn avoids_false_positive_for_need_to_verify() {
        assert_lint_count(
            "I need to verify the expenses before submission.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn flags_need_to_compiler() {
        assert_suggestion_result(
            "We simply don't need to compiler to do as much work anymore.",
            test_linter(),
            "We simply don't need the compiler to do as much work anymore.",
        );
    }

    #[test]
    fn flags_need_to_verification() {
        assert_suggestion_result(
            "I need to verification before logging in.",
            test_linter(),
            "I need the verification before logging in.",
        );
    }

    #[test]
    fn allows_need_to_report() {
        assert_no_lints(
            "We need to report before the meeting starts.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_password() {
        assert_no_lints("You need to password to access the server.", test_linter());
    }

    #[test]
    fn flags_need_to_data() {
        assert_suggestion_result(
            "They need to data analyzed by tomorrow.",
            test_linter(),
            "They need the data analyzed by tomorrow.",
        );
    }

    #[test]
    fn flags_need_to_approval() {
        assert_suggestion_result(
            "She will need to approval of her manager first.",
            test_linter(),
            "She will need the approval of her manager first.",
        );
    }

    #[test]
    fn allows_need_to_backup() {
        assert_no_lints(
            "We might need to backup if the main system fails.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_permit() {
        assert_no_lints(
            "He didn’t realize he would need to permit to film there.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_tools() {
        assert_no_lints("You’ll need to right tools to fix that.", test_linter());
    }

    #[test]
    fn allows_need_to_context() {
        assert_no_lints(
            "We need to context to make sense of his decision.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_funds() {
        assert_no_lints(
            "They need to funds released before construction begins.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_silence() {
        assert_no_lints("I need to silence to think clearly.", test_linter());
    }

    #[test]
    fn flags_needs_to_approval() {
        assert_suggestion_result(
            "She needs to approval from her advisor.",
            test_linter(),
            "She needs the approval from her advisor.",
        );
    }

    #[test]
    fn avoids_false_positive_for_needs_to_coordinate() {
        assert_lint_count(
            "She needs to collaborate with everyone on the plan.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn flags_needs_to_verification() {
        assert_suggestion_result(
            "He needs to verification ready before the audit.",
            test_linter(),
            "He needs the verification ready before the audit.",
        );
    }

    #[test]
    fn allows_needs_to_finalize() {
        assert_lint_count("She needs to finalize the schedule.", test_linter(), 0);
    }

    #[test]
    fn allows_needed_to_permit() {
        assert_no_lints(
            "They needed to permit before entering the site.",
            test_linter(),
        );
    }

    #[test]
    fn avoids_false_positive_for_needed_to_explain() {
        assert_lint_count(
            "They needed to explain the new policy carefully.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn catches_false_negative_for_needed_to_authorization() {
        assert_suggestion_result(
            "They needed to authorization before proceeding.",
            test_linter(),
            "They needed the authorization before proceeding.",
        );
    }

    #[test]
    fn allows_needed_to_file() {
        assert_lint_count(
            "They needed to file the paperwork before noon.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn flags_needing_to_documentation() {
        assert_suggestion_result(
            "Needing to documentation slowed the entire process.",
            test_linter(),
            "Needing the documentation slowed the entire process.",
        );
    }

    #[test]
    fn avoids_false_positive_for_needing_to_calibrate() {
        assert_lint_count(
            "Needing to calibrate the equipment delayed us slightly.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn catches_false_negative_for_needing_to_confirmation() {
        assert_suggestion_result(
            "Needing to confirmation from legal stalled the launch.",
            test_linter(),
            "Needing the confirmation from legal stalled the launch.",
        );
    }

    #[test]
    fn allows_needing_to_call() {
        assert_lint_count(
            "Needing to call your mother is stressful.",
            test_linter(),
            0,
        );
    }

    #[test]
    fn allows_issue_2252() {
        assert_no_lints("Things I need to do today:", test_linter());
    }

    #[test]
    fn allows_install() {
        assert_no_lints(
            "You need to install it separately, as it's a standalone application.",
            test_linter(),
        );
    }

    #[test]
    fn allows_lay() {
        assert_no_lints(
            "Okay, this is a long one, but I feel like I need to lay everything out.",
            test_linter(),
        );
    }

    #[test]
    fn allows_overcome() {
        assert_no_lints(
            "We believe every family deserves the opportunity to flourish, and we are committed to providing the resources they need to overcome adversity.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_run_into_2433() {
        assert_no_lints(
            "So that they don't need to run into this problem in the future.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_match_2446() {
        assert_no_lints(
            "You don't need to match string errors explicitly.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_match_exactly_2446() {
        assert_no_lints("They need to match exactly.", test_linter());
    }

    #[test]
    fn allows_need_to_use_php_code_fuzz() {
        assert_no_lints(
            "To display the custom field data on your website, you'll likely need to use PHP code within your theme files.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_display_images_fuzz() {
        assert_no_lints(
            "I'm building a photography portfolio site for a client and need to display images in a responsive gallery.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_build_brighter_futures_fuzz() {
        assert_no_lints(
            "At Haven House, our mission is to provide families with the resources they need to build brighter futures.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_redefine_success_fuzz() {
        assert_no_lints(
            "We need to redefine success to include wellbeing and sustainability.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_shift_from_fuzz() {
        assert_no_lints(
            "We need to shift from a deficit model to an abundance model.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_research_and_choose_fuzz() {
        assert_no_lints(
            "This means you need to research and choose adapters carefully.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_model_healthy_habits_fuzz() {
        assert_no_lints(
            "Leaders need to model healthy work habits and create a safe space for employees.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_start_2320() {
        assert_no_lints(
            "You need to start the server before running tests.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_have_2320() {
        assert_no_lints(
            "You need to have a valid license to use this software.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_configure_2320() {
        assert_no_lints(
            "You need to configure the database connection first.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_set_2320() {
        assert_no_lints(
            "You need to set the environment variable before deploying.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_send_2320() {
        assert_no_lints(
            "You need to send the request with the correct headers.",
            test_linter(),
        );
    }

    #[test]
    fn allows_need_to_receive_2320() {
        assert_no_lints(
            "You need to receive confirmation before proceeding.",
            test_linter(),
        );
    }
}
