use crate::linting::expr_linter::Chunk;
use crate::{
    Token,
    expr::{Expr, ExprMap, SequenceExpr},
    linting::{ExprLinter, Lint, LintKind, Suggestion},
};

pub struct BeAllowed {
    expr: ExprMap<usize>,
}

impl Default for BeAllowed {
    fn default() -> Self {
        let mut map = ExprMap::default();

        map.insert(
            SequenceExpr::default()
                .t_aco("will")
                .t_ws()
                .then_word_set(&["not"])
                .t_ws()
                .t_aco("allowed")
                .t_ws()
                .t_aco("to")
                .t_ws()
                .then_verb(),
            4,
        );

        map.insert(
            SequenceExpr::default()
                .t_aco("won't")
                .t_ws()
                .t_aco("allowed")
                .t_ws()
                .t_aco("to")
                .t_ws()
                .then_verb(),
            2,
        );

        Self { expr: map }
    }
}

impl ExprLinter for BeAllowed {
    type Unit = Chunk;

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

    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
        let allowed_index = *self.expr.lookup(0, matched_tokens, source)?;
        let allowed_token = matched_tokens.get(allowed_index)?;
        let span = allowed_token.span;
        let template = span.get_content(source);

        Some(Lint {
            span,
            lint_kind: LintKind::Grammar,
            suggestions: vec![Suggestion::replace_with_match_case(
                "be allowed".chars().collect(),
                template,
            )],
            message: "Add `be` so this reads `be allowed`.".to_owned(),
            priority: 31,
        })
    }

    fn description(&self) -> &'static str {
        "Ensures the passive form uses `be allowed` after future negatives."
    }
}

#[cfg(test)]
mod tests {
    use crate::linting::tests::{assert_lint_count, assert_suggestion_result};

    use super::BeAllowed;

    #[test]
    fn corrects_basic_sentence() {
        assert_suggestion_result(
            "You will not allowed to enter the lab.",
            BeAllowed::default(),
            "You will not be allowed to enter the lab.",
        );
    }

    #[test]
    fn corrects_first_person_subject() {
        assert_suggestion_result(
            "I will not allowed to go tonight.",
            BeAllowed::default(),
            "I will not be allowed to go tonight.",
        );
    }

    #[test]
    fn corrects_plural_subject() {
        assert_suggestion_result(
            "Students will not allowed to submit late work.",
            BeAllowed::default(),
            "Students will not be allowed to submit late work.",
        );
    }

    #[test]
    fn corrects_with_intro_clause() {
        assert_suggestion_result(
            "Because of policy, workers will not allowed to take photos.",
            BeAllowed::default(),
            "Because of policy, workers will not be allowed to take photos.",
        );
    }

    #[test]
    fn corrects_contracted_form() {
        assert_suggestion_result(
            "They won't allowed to park here during events.",
            BeAllowed::default(),
            "They won't be allowed to park here during events.",
        );
    }

    #[test]
    fn corrects_all_caps() {
        assert_suggestion_result(
            "THEY WILL NOT ALLOWED TO ENTER.",
            BeAllowed::default(),
            "THEY WILL NOT BE ALLOWED TO ENTER.",
        );
    }

    #[test]
    fn corrects_with_trailing_clause() {
        assert_suggestion_result(
            "Without a permit, guests will not allowed to stay overnight at the cabin.",
            BeAllowed::default(),
            "Without a permit, guests will not be allowed to stay overnight at the cabin.",
        );
    }

    #[test]
    fn corrects_with_modal_context() {
        assert_suggestion_result(
            "Even with approval, contractors will not allowed to access production.",
            BeAllowed::default(),
            "Even with approval, contractors will not be allowed to access production.",
        );
    }

    #[test]
    fn leaves_correct_phrase_untouched() {
        assert_suggestion_result(
            "They will not be allowed to park here during events.",
            BeAllowed::default(),
            "They will not be allowed to park here during events.",
        );
    }

    #[test]
    fn leaves_other_verbs_alone() {
        assert_lint_count(
            "We will not allow visitors after nine.",
            BeAllowed::default(),
            0,
        );
    }

    #[test]
    fn leaves_similar_sequence_without_to() {
        assert_lint_count(
            "They won't be allowed to park here during events.",
            BeAllowed::default(),
            0,
        );
    }
}
