use oxc_ast::{
    AstKind,
    ast::{BinaryExpression, Expression},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

use crate::{AstNode, context::LintContext, rule::Rule};

fn bad_comparison_sequence_diagnostic(
    comparison_result: Span,
    compared_against: Span,
) -> OxcDiagnostic {
    OxcDiagnostic::warn("Bad comparison sequence")
        .with_help("Comparison result should not be used directly as an operand of another comparison. If you need to compare three or more operands, you should connect each comparison operation with logical AND operator (`&&`)")
        .with_labels([
            comparison_result.label("This comparison expression produces a boolean"),
            compared_against.label("That boolean is then compared with this operand"),
        ])
}

#[derive(Debug, Default, Clone)]
pub struct BadComparisonSequence;

declare_oxc_lint!(
    /// ### What it does
    ///
    /// This rule applies when the comparison operator is applied two or more times in a row.
    ///
    /// ### Why is this bad?
    ///
    /// Because comparison operator is a binary operator, it is impossible to compare three or more operands at once.
    /// If comparison operator is used to compare three or more operands, only the first two operands are compared and the rest is compared with its result of boolean type.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```javascript
    /// if (a == b == c) {
    ///  console.log("a, b, and c are the same");
    /// }
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```javascript
    /// if (a == b && b == c) {
    ///  console.log("a, b, and c are the same");
    /// }
    /// ```
    BadComparisonSequence,
    oxc,
    correctness,
    version = "0.0.3",
    short_description = "This rule applies when the comparison operator is applied two or more times in a row.",
);

impl Rule for BadComparisonSequence {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        let AstKind::BinaryExpression(expr) = node.kind() else {
            return;
        };
        if let Some((comparison_result, compared_against)) = bad_comparison_sequence(expr)
            && has_no_bad_comparison_in_parents(node, ctx)
        {
            ctx.diagnostic(bad_comparison_sequence_diagnostic(comparison_result, compared_against));
        }
    }
}

fn has_no_bad_comparison_in_parents<'a, 'b>(
    node: &'b AstNode<'a>,
    ctx: &'b LintContext<'a>,
) -> bool {
    for ancestor_kind in ctx.nodes().ancestor_kinds(node.id()) {
        // `a === b === c === d === e` only produce one error, since `(a === b === c) === d === e` will produce two errors.
        // So we should treat Parenthesized Expression as a boundary.
        if matches!(ancestor_kind, AstKind::ParenthesizedExpression(_))
            || ancestor_kind.is_declaration()
            || ancestor_kind.is_statement()
        {
            return true;
        }

        if matches!(ancestor_kind, AstKind::BinaryExpression(expr) if bad_comparison_sequence(expr).is_some())
        {
            return false;
        }
    }
    false
}

fn bad_comparison_sequence(expr: &BinaryExpression) -> Option<(Span, Span)> {
    let Expression::BinaryExpression(left_expr) = &expr.left else {
        return None;
    };

    let same_comparison_kind = (expr.operator.is_equality() && left_expr.operator.is_equality())
        || (expr.operator.is_compare() && left_expr.operator.is_compare());

    same_comparison_kind.then_some((left_expr.span, expr.right.span()))
}

#[test]
fn test() {
    use crate::tester::Tester;

    let pass = vec![
        "if (a == b > c) { console.log('foo') }",
        "if (a == b < c) { console.log('foo') }",
        "if (a == b >= c) { console.log('foo') }",
        "if (a == b <= c) { console.log('foo') }",
        "if (a === b > c) { console.log('foo') }",
        "if (a === b < c) { console.log('foo') }",
        "if (a === b >= c) { console.log('foo') }",
        "if (a === b <= c) { console.log('foo') }",
        "if (a != b > c) { console.log('foo') }",
        "if (a != b < c) { console.log('foo') }",
        "if (a != b >= c) { console.log('foo') }",
        "if (a != b <= c) { console.log('foo') }",
        "if (a !== b > c) { console.log('foo') }",
        "if (a !== b < c) { console.log('foo') }",
        "if (a !== b >= c) { console.log('foo') }",
        "if (a !== b <= c) { console.log('foo') }",
        "if (a > b == c) { console.log('foo') }",
        "if (a > b === c) { console.log('foo') }",
        "if (a > b != c) { console.log('foo') }",
        "if (a > b !== c) { console.log('foo') }",
        "if (a < b == c) { console.log('foo') }",
        "if (a < b === c) { console.log('foo') }",
        "if (a < b != c) { console.log('foo') }",
        "if (a < b !== c) { console.log('foo') }",
        "if (a >= b == c) { console.log('foo') }",
        "if (a >= b === c) { console.log('foo') }",
        "if (a >= b != c) { console.log('foo') }",
        "if (a >= b !== c) { console.log('foo') }",
        "if (a <= b == c) { console.log('foo') }",
        "if (a <= b === c) { console.log('foo') }",
        "if (a <= b != c) { console.log('foo') }",
        "if (a <= b !== c) { console.log('foo') }",
        "if ((a == b) && (b == c)) { console.log('foo') }",
        "if ((a > b) && (b > c)) { console.log('foo') }",
    ];

    let fail = vec![
        "if (a == b == c) { console.log('foo') }",
        "if (a == b == c == d) { console.log('foo') }",
        "if ((a == b == c) == d) { console.log('foo') }",
        "if ((a == b == c) == d == e == f) { console.log('foo') }",
        "if (a == b === c) { console.log('foo') }",
        "if (a == b != c) { console.log('foo') }",
        "if (a == b !== c) { console.log('foo') }",
        "if (a === b == c) { console.log('foo') }",
        "if (a === b === c) { console.log('foo') }",
        "if (a === b != c) { console.log('foo') }",
        "if (a === b !== c) { console.log('foo') }",
        "if (a != b == c) { console.log('foo') }",
        "if (a != b === c) { console.log('foo') }",
        "if (a != b != c) { console.log('foo') }",
        "if (a != b !== c) { console.log('foo') }",
        "if (a !== b == c) { console.log('foo') }",
        "if (a !== b === c) { console.log('foo') }",
        "if (a !== b != c) { console.log('foo') }",
        "if (a !== b !== c) { console.log('foo') }",
        "if (a > b > c) { console.log('foo') }",
        "if (a > b < c) { console.log('foo') }",
        "if (a > b >= c) { console.log('foo') }",
        "if (a > b <= c) { console.log('foo') }",
        "if (a < b > c) { console.log('foo') }",
        "if (a < b < c) { console.log('foo') }",
        "if (a < b >= c) { console.log('foo') }",
        "if (a < b <= c) { console.log('foo') }",
        "if (a >= b > c) { console.log('foo') }",
        "if (a >= b < c) { console.log('foo') }",
        "if (a >= b >= c) { console.log('foo') }",
        "if (a >= b <= c) { console.log('foo') }",
        "if (a <= b > c) { console.log('foo') }",
        "if (a <= b < c) { console.log('foo') }",
        "if (a <= b >= c) { console.log('foo') }",
        "if (a <= b <= c) { console.log('foo') }",
    ];

    Tester::new(BadComparisonSequence::NAME, BadComparisonSequence::PLUGIN, pass, fail)
        .test_and_snapshot();
}
