use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use oxc_ast::{
    AstKind,
    ast::{ArrowFunctionExpression, Expression, Function, YieldExpression},
};
use oxc_ast_visit::Visit;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use oxc_syntax::{node::NodeId, scope::ScopeFlags};

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

fn no_constant_condition_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Unexpected constant condition")
        .with_help("Update the condition to not be constant, or remove the condition entirely")
        .with_label(span.label("this expression will always evaluate to the same value"))
}

#[derive(Debug, Default, Clone, PartialEq, JsonSchema, Serialize)]
#[serde(rename_all = "camelCase")]
enum CheckLoops {
    All,
    #[default]
    AllExceptWhileTrue,
    None,
}

impl<'de> Deserialize<'de> for CheckLoops {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum CheckLoopsConfig {
            Bool(bool),
            String(CheckLoopsString),
        }

        #[derive(Deserialize)]
        #[serde(rename_all = "camelCase")]
        enum CheckLoopsString {
            All,
            AllExceptWhileTrue,
            None,
        }

        Ok(match CheckLoopsConfig::deserialize(deserializer)? {
            CheckLoopsConfig::Bool(true) | CheckLoopsConfig::String(CheckLoopsString::All) => {
                Self::All
            }
            CheckLoopsConfig::Bool(false) | CheckLoopsConfig::String(CheckLoopsString::None) => {
                Self::None
            }
            CheckLoopsConfig::String(CheckLoopsString::AllExceptWhileTrue) => {
                Self::AllExceptWhileTrue
            }
        })
    }
}

#[derive(Deserialize, JsonSchema, Serialize)]
#[serde(untagged)]
enum CheckLoopsConfig {
    Bool(bool),
    String(CheckLoops),
}

#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct NoConstantCondition {
    /// Configuration option to specify whether to check for constant conditions in loops.
    ///
    /// - `"all"` or `true` disallows constant expressions in loops
    /// - `"allExceptWhileTrue"` disallows constant expressions in loops except while loops with expression `true`
    /// - `"none"` or `false` allows constant expressions in loops
    #[schemars(with = "CheckLoopsConfig")]
    check_loops: CheckLoops,
}

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Disallow constant expressions in conditions.
    ///
    /// ### Why is this bad?
    ///
    /// A constant expression (for example, a literal) as a test condition might
    /// be a typo or development trigger for a specific behavior.
    ///
    /// This rule disallows constant expressions in the test condition of:
    ///
    /// - `if`, `for`, `while`, or `do...while` statement
    /// - `?`: ternary expression
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```js
    /// if (false) {
    ///   doSomethingUnfinished();
    /// }
    ///
    /// if (new Boolean(x)) {
    ///   doSomethingAlways();
    /// }
    /// if (x ||= true) {
    ///   doSomethingAlways();
    /// }
    ///
    /// do {
    ///   doSomethingForever();
    /// } while (x = -1);
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```js
    /// if (x === 0) {
    ///   doSomething();
    /// }
    ///
    /// while (typeof x === "undefined") {
    ///   doSomething();
    /// }
    /// ```
    NoConstantCondition,
    eslint,
    correctness,
    config = NoConstantCondition,
    version = "0.0.3",
    short_description = "Disallow constant expressions in conditions.",
);

impl Rule for NoConstantCondition {
    fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
    }

    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::IfStatement(if_stmt) => check(ctx, &if_stmt.test),
            AstKind::ConditionalExpression(condition_expr) => check(ctx, &condition_expr.test),
            AstKind::WhileStatement(while_stmt) => {
                self.check_loop(ctx, &while_stmt.test, true, || {
                    has_yield_before_loop_exit_in_same_generator(ctx, node.id(), None, |finder| {
                        finder.visit_while_statement(while_stmt);
                    })
                });
            }
            AstKind::DoWhileStatement(do_while_stmt) => {
                self.check_loop(ctx, &do_while_stmt.test, false, || {
                    has_yield_before_loop_exit_in_same_generator(ctx, node.id(), None, |finder| {
                        finder.visit_do_while_statement(do_while_stmt);
                    })
                });
            }
            AstKind::ForStatement(for_stmt) => {
                let Some(test) = &for_stmt.test else {
                    return;
                };

                self.check_loop(ctx, test, false, || {
                    has_yield_before_loop_exit_in_same_generator(
                        ctx,
                        node.id(),
                        Some(test.span()),
                        |finder| {
                            finder.visit_for_statement(for_stmt);
                        },
                    )
                });
            }
            _ => {}
        }
    }
}

impl NoConstantCondition {
    fn check_loop<'a>(
        &self,
        ctx: &LintContext<'a>,
        test: &'a Expression<'_>,
        is_while: bool,
        has_yield_before_loop_exit: impl FnOnce() -> bool,
    ) {
        match self.check_loops {
            CheckLoops::None => return,
            CheckLoops::AllExceptWhileTrue if is_while => match test {
                Expression::BooleanLiteral(bool) if bool.value => return,
                _ => {}
            },
            _ => {}
        }

        if !test.is_constant(true, ctx) {
            return;
        }

        if self.check_loops == CheckLoops::AllExceptWhileTrue && has_yield_before_loop_exit() {
            return;
        }

        ctx.diagnostic(no_constant_condition_diagnostic(test.span()));
    }
}

fn check<'a>(ctx: &LintContext<'a>, test: &'a Expression<'_>) {
    if test.is_constant(true, ctx) {
        ctx.diagnostic(no_constant_condition_diagnostic(test.span()));
    }
}

fn has_yield_before_loop_exit_in_same_generator(
    ctx: &LintContext<'_>,
    loop_node_id: NodeId,
    after_span: Option<Span>,
    visit_loop: impl FnOnce(&mut YieldBeforeLoopExitFinder),
) -> bool {
    if !ctx
        .nodes()
        .ancestors_enumerated(loop_node_id)
        .find_map(|(_id, node)| match node.kind() {
            AstKind::Function(function) => Some(function.generator),
            _ => None,
        })
        .unwrap_or(false)
    {
        return false;
    }

    let mut finder = YieldBeforeLoopExitFinder { found: false, after_span };
    visit_loop(&mut finder);
    finder.found
}

struct YieldBeforeLoopExitFinder {
    found: bool,
    after_span: Option<Span>,
}

impl<'a> Visit<'a> for YieldBeforeLoopExitFinder {
    fn visit_yield_expression(&mut self, expr: &YieldExpression<'a>) {
        if self.after_span.is_none_or(|after_span| expr.span.start > after_span.start) {
            self.found = true;
        }
    }

    fn visit_function(&mut self, _function: &Function<'a>, _flags: ScopeFlags) {}

    fn visit_arrow_function_expression(&mut self, _expr: &ArrowFunctionExpression<'a>) {}
}

#[test]
fn test() {
    use serde_json::json;

    use crate::tester::Tester;

    let pass = vec![
        ("if(a);", None),
        ("if(a == 0);", None),
        ("if(a = f());", None),
        ("if(a += 1);", None),
        ("if(a |= 1);", None),
        ("if(a |= true);", None),
        ("if(a |= false);", None),
        ("if(a &= 1);", None),
        ("if(a &= true);", None),
        ("if(a &= false);", None),
        ("if(a >>= 1);", None),
        ("if(a >>= true);", None),
        ("if(a >>= false);", None),
        ("if(a >>>= 1);", None),
        ("if(a ??= 1);", None),
        ("if(a ??= true);", None),
        ("if(a ??= false);", None),
        ("if(a ||= b);", None),
        ("if(a ||= false);", None),
        ("if(a ||= 0);", None),
        ("if(a ||= void 0);", None),
        ("if(+(a ||= 1));", None),
        ("if(f(a ||= true));", None),
        ("if((a ||= 1) + 2);", None),
        ("if(1 + (a ||= true));", None),
        ("if(a ||= '' || false);", None),
        ("if(a ||= void 0 || null);", None),
        ("if((a ||= false) || b);", None),
        ("if(a || (b ||= false));", None),
        ("if((a ||= true) && b);", None),
        ("if(a && (b ||= true));", None),
        ("if(a &&= b);", None),
        ("if(a &&= true);", None),
        ("if(a &&= 1);", None),
        ("if(a &&= 'foo');", None),
        ("if((a &&= '') + false);", None),
        ("if('' + (a &&= null));", None),
        ("if(a &&= 1 && 2);", None),
        ("if((a &&= true) && b);", None),
        ("if(a && (b &&= true));", None),
        ("if((a &&= false) || b);", None),
        ("if(a || (b &&= false));", None),
        ("if(a ||= b ||= false);", None),
        ("if(a &&= b &&= true);", None),
        ("if(a ||= b &&= false);", None),
        ("if(a ||= b &&= true);", None),
        ("if(a &&= b ||= false);", None),
        ("if(a &&= b ||= true);", None),
        ("if(1, a);", None),
        ("if ('every' in []);", None),
        ("if (`\\\n${a}`) {}", None),
        ("if (`${a}`);", None),
        ("if (`${foo()}`);", None),
        ("if (`${a === 'b' && b==='a'}`);", None),
        ("if (`foo${a}` === 'fooa');", None),
        ("if (tag`a`);", None),
        ("if (tag`${a}`);", None),
        ("if (+(a || true));", None),
        ("if (-(a || true));", None),
        ("if (~(a || 1));", None),
        ("if (+(a && 0) === +(b && 0));", None),
        ("if(typeof x === 'undefined'){}", None),
        ("if(`${typeof x}` === 'undefined'){}", None),
        ("if(a === 'str' && typeof b){}", None),
        ("typeof a == typeof b", None),
        ("typeof 'a' === 'string'|| typeof b === 'string'", None),
        ("`${typeof 'a'}` === 'string'|| `${typeof b}` === 'string'", None),
        ("if (void a || a);", None),
        ("if (a || void a);", None),
        ("if(xyz === 'str1' && abc==='str2'){}", None),
        ("if(xyz === 'str1' || abc==='str2'){}", None),
        ("if(xyz === 'str1' || abc==='str2' && pqr === 5){}", None),
        ("if(typeof abc === 'string' && abc==='str2'){}", None),
        ("if(false || abc==='str'){}", None),
        ("if(true && abc==='str'){}", None),
        ("if(typeof 'str' && abc==='str'){}", None),
        ("if(abc==='str' || false || def ==='str'){}", None),
        ("if(true && abc==='str' || def ==='str'){}", None),
        ("if(true && typeof abc==='string'){}", None),
        ("if('str1' && a){}", None),
        ("if(a && 'str'){}", None),
        ("if ((foo || true) === 'baz') {}", None),
        ("if ((foo || 'bar') === 'baz') {}", None),
        ("if ((foo || 'bar') !== 'baz') {}", None),
        ("if ((foo || 'bar') == 'baz') {}", None),
        ("if ((foo || 'bar') != 'baz') {}", None),
        ("if ((foo || 233) > 666) {}", None),
        ("if ((foo || 233) < 666) {}", None),
        ("if ((foo || 233) >= 666) {}", None),
        ("if ((foo || 233) <= 666) {}", None),
        ("if ((key || 'k') in obj) {}", None),
        ("if ((foo || {}) instanceof obj) {}", None),
        ("if ((foo || 'bar' || 'bar') === 'bar');", None),
        ("if ((foo || 1n) === 'baz') {}", None),
        ("if (a && 0n || b);", None),
        ("if(1n && a){};", None),
        ("if ('' + [y] === '' + [ty]) {}", None),
        ("if ('a' === '' + [ty]) {}", None),
        ("if ('' + [y, m, d] === 'a') {}", None),
        ("if ('' + [y, 'm'] === '' + [ty, 'tm']) {}", None),
        ("if ('' + [y, 'm'] === '' + ['ty']) {}", None),
        ("if ([,] in\n\n($2))\n ;\nelse\n ;", None),
        ("if ([...x]+'' === 'y'){}", None),
        ("if (new Number(x) + 1 === 2) {}", None),
        ("if([a]==[b]) {}", None),
        ("if (+[...a]) {}", None),
        ("if (+[...[...a]]) {}", None),
        ("if (`${[...a]}`) {}", None),
        ("if (`${[a]}`) {}", None),
        ("if (+[a]) {}", None),
        ("if (0 - [a]) {}", None),
        ("if (1 * [a]) {}", None),
        ("if (Boolean(a)) {}", None),
        ("if (Boolean(...args)) {}", None),
        ("if (foo.Boolean(1)) {}", None),
        ("const undefined = 'lol'; if (undefined) {}", None),
        ("function foo(Boolean) { if (Boolean(1)) {} }", None),
        ("const Boolean = () => {}; if (Boolean(1)) {}", None),
        ("q > 0 ? 1 : 2;", None),
        ("`${a}` === a ? 1 : 2", None),
        ("`foo${a}` === a ? 1 : 2", None),
        ("tag`a` === a ? 1 : 2", None),
        ("tag`${a}` === a ? 1 : 2", None),
        ("while(~!a);", None),
        ("while(a = b);", None),
        ("while(`${a}`);", None),
        ("for(;x < 10;);", None),
        ("for(;;);", None),
        ("for(;`${a}`;);", None),
        ("do{ }while(x)", None),
        ("while(x += 3) {}", None),
        ("while(tag`a`) {}", None),
        ("while(tag`${a}`) {}", None),
        ("while(`\\\n${a}`) {}", None),
        ("while(true);", Some(json!([{ "checkLoops": false }]))),
        ("for(;true;);", Some(json!([{ "checkLoops": false }]))),
        ("do{}while(true)", Some(json!([{ "checkLoops": "none" }]))),
        ("while(true);", Some(json!([{ "checkLoops": "none" }]))),
        ("for(;true;);", Some(json!([{ "checkLoops": "none" }]))),
        ("do{ }while(x);", Some(json!([{ "checkLoops": "all" }]))),
        ("while(true);", Some(json!([{ "checkLoops": "allExceptWhileTrue" }]))),
        ("while(true);", None),
        ("while(a == b);", Some(json!([{ "checkLoops": "all" }]))),
        ("for (let x = 0; x <= 10; x++) {};", Some(json!([{ "checkLoops": "all" }]))),
        ("do{}while(true)", Some(json!([{ "checkLoops": false }]))),
        ("function* foo(){while(true){yield 'foo';}}", None),
        ("function* foo(){for(;true;){yield 'foo';}}", None),
        ("function* foo(){do{yield 'foo';}while(true)}", None),
        ("function* foo(){while (true) { while(true) {yield;}}}", None),
        ("function* foo() {for (; yield; ) {}}", None),
        ("function* foo() {for (; ; yield) {}}", None),
        ("function* foo() {for (; true; yield) {}}", None),
        ("function* foo() {while (true) {function* foo() {yield;}yield;}}", None),
        ("function* foo() { for (let x = yield; x < 10; x++) {yield;}yield;}", None),
        ("function* foo() { for (let x = yield; ; x++) { yield; }}", None),
    ];

    let fail = vec![
        ("if(-2);", None),
        ("if(true);", None),
        ("if(1);", None),
        ("if({});", None),
        ("if(0 < 1);", None),
        ("if(0 || 1);", None),
        ("if(a, 1);", None),
        ("if(`foo`);", None),
        ("if(``);", None),
        ("if(`\\\n`);", None),
        ("if(`${'bar'}`);", None),
        ("if(`${'bar' + `foo`}`);", None),
        ("if(`foo${false || true}`);", None),
        ("if(`foo${0 || 1}`);", None),
        ("if(`foo${bar}`);", None),
        ("if(`${bar}foo`);", None),
        ("if(!(true || a));", None),
        ("if(!(a && void b && c));", None),
        ("if(0 || !(a && null));", None),
        ("if(1 + !(a || true));", None),
        ("if(!(null && a) > 1);", None),
        ("if(+(!(a && 0)));", None),
        ("if(!typeof a === 'string');", None),
        ("if(-('foo' || a));", None),
        ("if(+(void a && b) === ~(1 || c));", None),
        ("if(a ||= true);", None),
        ("if(a ||= 5);", None),
        ("if(a ||= 'foo' || b);", None),
        ("if(a ||= b || /regex/);", None),
        ("if(a ||= b ||= true);", None),
        ("if(a ||= b ||= c || 1);", None),
        ("if(!(a ||= true));", None),
        ("if(!(a ||= 'foo') === true);", None),
        ("if(!(a ||= 'foo') === false);", None),
        ("if(a || (b ||= true));", None),
        ("if((a ||= 1) || b);", None),
        ("if((a ||= true) && true);", None),
        ("if(true && (a ||= true));", None),
        ("if(a &&= false);", None),
        ("if(a &&= null);", None),
        ("if(a &&= void b);", None),
        ("if(a &&= 0 && b);", None),
        ("if(a &&= b && '');", None),
        ("if(a &&= b &&= false);", None),
        ("if(a &&= b &&= c && false);", None),
        ("if(!(a &&= false));", None),
        ("if(!(a &&= 0) + 1);", None),
        ("if(a && (b &&= false));", None),
        ("if((a &&= null) && b);", None),
        ("if(false || (a &&= false));", None),
        ("if((a &&= false) || false);", None),
        ("while(x = 1);", None),
        ("if(typeof x){}", None),
        ("if(typeof 'abc' === 'string'){}", None),
        ("if(a = typeof b){}", None),
        ("if(a, typeof b){}", None),
        ("if(typeof 'a' == 'string' || typeof 'b' == 'string'){}", None),
        ("while(typeof x){}", None),
        ("if(1 || void x);", None),
        ("if(void x);", None),
        ("if(y = void x);", None),
        ("if(x, void x);", None),
        ("if(void x === void y);", None),
        ("if(void x && a);", None),
        ("if(a && void x);", None),
        ("if(false && abc==='str'){}", None),
        ("if(true || abc==='str'){}", None),
        ("if(1 || abc==='str'){}", None),
        ("if(abc==='str' || true){}", None),
        ("if(abc==='str' || true || def ==='str'){}", None),
        ("if(false || true){}", None),
        ("if(typeof abc==='str' || true){}", None),
        ("if('str' || a){}", None),
        ("if('str' || abc==='str'){}", None),
        ("if('str1' || 'str2'){}", None),
        ("if('str1' && 'str2'){}", None),
        ("if(abc==='str' || 'str'){}", None),
        ("if(a || 'str'){}", None),
        ("if([a]) {}", None),
        ("if([]) {}", None),
        ("if(''+['a']) {}", None),
        ("if(''+[]) {}", None),
        ("if(+1) {}", None),
        ("if ([,] + ''){}", None),
        ("if(/foo/ui);", None),
        ("if(0n);", None),
        ("if(0b0n);", None),
        ("if(0o0n);", None),
        ("if(0x0n);", None),
        ("if(0b1n);", None),
        ("if(0o1n);", None),
        ("if(0x1n);", None),
        ("if(0x1n || foo);", None),
        // Classes and instances are always truthy
        ("if(class {}) {}", None),
        ("if(new Foo()) {}", None),
        // Boxed primitives are always truthy
        ("if(new Boolean(foo)) {}", None),
        ("if(new String(foo)) {}", None),
        ("if(new Number(foo)) {}", None),
        // Spreading a constant array
        ("if(`${[...['a']]}`) {}", None),
        // undefined is always falsy (except in old browsers that let you
        // re-assign, but that's an obscure enough edge case to not worry about)
        ("if (undefined) {}", None),
        // Coercion to boolean via Boolean function
        ("if (Boolean(1)) {}", None),
        ("if (Boolean()) {}", None),
        ("if (Boolean([a])) {}", None),
        ("if (Boolean(1)) { function Boolean() {}}", None),
        ("true ? 1 : 2;", None),
        ("1 ? 1 : 2;", None),
        ("q = 0 ? 1 : 2;", None),
        ("(q = 0) ? 1 : 2;", None),
        ("`` ? 1 : 2;", None),
        ("`foo` ? 1 : 2;", None),
        ("`foo${bar}` ? 1 : 2;", None),
        ("for(;true;);", None),
        ("for(;``;);", None),
        ("for(;`foo`;);", None),
        ("for(;`foo${bar}`;);", None),
        ("do{}while(true)", None),
        ("do{}while('1')", None),
        ("do{}while(0)", None),
        ("do{}while(t = -2)", None),
        ("do{}while(``)", None),
        ("do{}while(`foo`)", None),
        ("do{}while(`foo${bar}`)", None),
        ("while([]);", None),
        ("while(~!0);", None),
        ("while(x = 1);", Some(json!([{ "checkLoops": "all" }]))),
        ("while(function(){});", None),
        ("while(true);", Some(json!([{ "checkLoops": "all" }]))),
        ("while(1);", None),
        ("while(() => {});", None),
        ("while(`foo`);", None),
        ("while(``);", None),
        ("while(`${'foo'}`);", None),
        ("while(`${'foo' + 'bar'}`);", None),
        ("do{ }while(x = 1)", Some(json!([{ "checkLoops": "all" }]))),
        ("for (;true;) {};", Some(json!([{ "checkLoops": "all" }]))),
        ("function* foo(){while(true){} yield 'foo';}", Some(json!([{ "checkLoops": "all" }]))),
        ("function* foo(){while(true){} yield 'foo';}", Some(json!([{ "checkLoops": true }]))),
        (
            "function* foo(){while(true){if (true) {yield 'foo';}}}",
            Some(json!([{ "checkLoops": "all" }])),
        ),
        (
            "function* foo(){while(true){if (true) {yield 'foo';}}}",
            Some(json!([{ "checkLoops": true }])),
        ),
        (
            "function* foo(){while(true){yield 'foo';} while(true) {}}",
            Some(json!([{ "checkLoops": "all" }])),
        ),
        (
            "function* foo(){while(true){yield 'foo';} while(true) {}}",
            Some(json!([{ "checkLoops": true }])),
        ),
        (
            "var a = function* foo(){while(true){} yield 'foo';}",
            Some(json!([{ "checkLoops": "all" }])),
        ),
        (
            "var a = function* foo(){while(true){} yield 'foo';}",
            Some(json!([{ "checkLoops": true }])),
        ),
        ("while (true) { function* foo() {yield;}}", Some(json!([{ "checkLoops": "all" }]))),
        ("while (true) { function* foo() {yield;}}", Some(json!([{ "checkLoops": true }]))),
        ("function* foo(){if (true) {yield 'foo';}}", None),
        ("function* foo() {for (let foo = yield; true;) {}}", None),
        ("function* foo() {for (foo = yield; true;) {}}", None),
        (
            "function foo() {while (true) {function* bar() {while (true) {yield;}}}}",
            Some(json!([{ "checkLoops": "all" }])),
        ),
        (
            "function foo() {while (true) {const bar = function*() {while (true) {yield;}}}}",
            Some(json!([{ "checkLoops": "all" }])),
        ),
        ("function* foo() { for (let foo = 1 + 2 + 3 + (yield); true; baz) {}}", None),
    ];

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