use biome_analyze::{
    Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_syntax::JsForInStatement;
use biome_rowan::AstNode;
use biome_rule_options::no_for_in::NoForInOptions;

declare_lint_rule! {
    /// Disallow iterating using a for-in loop.
    ///
    /// A for-in loop (`for (const i in o)`) iterates over the properties of an Object. While it is legal to use for-in loops with array values, it is not common. There are several potential bugs with this:
    ///
    /// 1. It iterates over all enumerable properties, including non-index ones and the entire prototype chain. For example, [`RegExp.prototype.exec`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec) returns an array with additional properties, and `for-in` will iterate over them. Some libraries or even your own code may add additional methods to `Array.prototype` (either as polyfill or as custom methods), and if not done properly, they may be iterated over as well.
    /// 2. It skips holes in the array. While sparse arrays are rare and advised against, they are still possible and your code should be able to handle them.
    /// 3. The "index" is returned as a string, not a number. This can be caught by TypeScript, but can still lead to subtle bugs.
    ///
    /// You may have confused for-in with for-of, which iterates over the elements of the array. If you actually need the index, use a regular `for` loop or the `forEach` method.
    ///
    /// ## Examples
    ///
    /// ### Invalid
    ///
    /// ```js,expect_diagnostic
    /// for (const i in array) {
    ///   console.log(i, array[i]);
    /// }
    /// ```
    ///
    /// ### Valid
    ///
    /// ```js
    /// for (const value of array) {
    ///   console.log(value);
    /// }
    /// ```
    /// ```js
    /// for (let i = 0; i < array.length; i += 1) {
    ///   console.log(i, array[i]);
    /// }
    /// ```
    /// ```js
    /// array.forEach((value, i) => {
    ///   console.log(i, value);
    /// });
    /// ```
    /// ```js
    /// for (const [i, value] of array.entries()) {
    ///   console.log(i, value);
    /// }
    /// ```
    ///
    pub NoForIn {
        version: "2.3.6",
        name: "noForIn",
        language: "js",
        recommended: false,
        sources: &[RuleSource::EslintTypeScript("no-for-in-array").inspired()],
    }
}

impl Rule for NoForIn {
    type Query = Ast<JsForInStatement>;
    type State = ();
    type Signals = Option<Self::State>;
    type Options = NoForInOptions;

    fn run(_ctx: &RuleContext<Self>) -> Self::Signals {
        Some(())
    }

    fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
        let node = ctx.query();
        Some(
            RuleDiagnostic::new(
                rule_category!(),
                node.range(),
                markup! {
                    "Unexpected for-in loop."
                },
            )
            .note(markup! {
                "For-in loops are confusing and easy to misuse. You likely want to use a regular loop, for-of loop or forEach instead."
            }),
        )
    }
}
