use schemars::JsonSchema;
use serde::Deserialize;

use oxc_ast::{
    AstKind,
    ast::{Class, ClassElement},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{
    AstNode,
    context::{ContextHost, LintContext},
    rule::{DefaultRuleConfig, Rule},
    utils::{expression_contains_jsx, function_contains_jsx, is_es6_component},
};

fn prefer_function_component_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Class component should be written as a function component.")
        .with_help("Convert the class component to a function component.")
        .with_note("See https://react.dev/reference/react/Component#migrating-a-simple-component-from-a-class-to-a-function")
        .with_label(span)
}

#[derive(Debug, Clone, JsonSchema, Deserialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct PreferFunctionComponent {
    /// If `true`, error boundary classes (those implementing `componentDidCatch`
    /// or `static getDerivedStateFromError`) are allowed as class components.
    ///
    /// This is because these classes are not easily converted to function components,
    /// and so they are exempted from this rule by default.
    allow_error_boundary: bool,
    /// If `true`, classes that contain JSX but do not extend `Component` or
    /// `PureComponent` are allowed.
    allow_jsx_utility_class: bool,
}

impl Default for PreferFunctionComponent {
    fn default() -> Self {
        Self { allow_error_boundary: true, allow_jsx_utility_class: false }
    }
}

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Enforces that React components are written as function components
    /// instead of class components.
    ///
    /// ### Why is this bad?
    ///
    /// Function components are simpler, easier to read, and support React
    /// hooks. Class components are a legacy pattern that is discouraged in
    /// modern React.
    ///
    /// This rule is based on the rule from
    /// [eslint-plugin-react-prefer-function-component](https://www.npmjs.com/package/eslint-plugin-react-prefer-function-component).
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```jsx
    /// class Foo extends React.Component {
    ///   render() {
    ///     return <div>{this.props.foo}</div>;
    ///   }
    /// }
    ///
    /// class Bar extends React.PureComponent {
    ///   render() {
    ///     return <div>{this.props.bar}</div>;
    ///   }
    /// }
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```jsx
    /// const Foo = function(props) {
    ///   return <div>{props.foo}</div>;
    /// };
    ///
    /// const Bar = ({ bar }) => <div>{bar}</div>;
    /// ```
    PreferFunctionComponent,
    react,
    restriction,
    config = PreferFunctionComponent,
    version = "1.59.0",
    short_description = "Enforces that React components are written as function components instead of class components.",
);

impl Rule for PreferFunctionComponent {
    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>) {
        let AstKind::Class(class) = node.kind() else {
            return;
        };

        if !is_es6_component(node) {
            // Not extending Component/PureComponent. Only flag if it contains
            // JSX and allowJsxUtilityClass is false.
            if self.allow_jsx_utility_class {
                return;
            }
            if !class_body_contains_jsx(class) {
                return;
            }
        }

        // Check error boundary exemption
        if self.allow_error_boundary && is_error_boundary(class) {
            return;
        }

        let span = class.id.as_ref().map_or(class.span, |id| id.span);
        ctx.diagnostic(prefer_function_component_diagnostic(span));
    }

    fn should_run(&self, ctx: &ContextHost) -> bool {
        ctx.source_type().is_jsx()
    }
}

/// Returns `true` if the class has a `componentDidCatch` method or a
/// `static getDerivedStateFromError` method/property.
fn is_error_boundary(class: &Class) -> bool {
    class.body.body.iter().any(|element| {
        let (is_static, key) = match element {
            ClassElement::MethodDefinition(m) => (m.r#static, &m.key),
            ClassElement::PropertyDefinition(p) => (p.r#static, &p.key),
            _ => return false,
        };
        match key.static_name().as_deref() {
            Some("componentDidCatch") => !is_static,
            Some("getDerivedStateFromError") => is_static,
            _ => false,
        }
    })
}

/// Returns `true` if any class method or property initializer contains JSX.
/// Reuses the existing `function_contains_jsx` and `expression_contains_jsx`
/// utilities from `crate::utils`, which already handle nested function
/// boundaries correctly.
fn class_body_contains_jsx(class: &Class) -> bool {
    class.body.body.iter().any(|element| match element {
        ClassElement::MethodDefinition(method) => function_contains_jsx(&method.value),
        ClassElement::PropertyDefinition(prop) => {
            prop.value.as_ref().is_some_and(|v| expression_contains_jsx(v))
        }
        _ => false,
    })
}

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

    let pass = vec![
        // Already a stateless function
        (
            "const Foo = function(props) {
               return <div>{props.foo}</div>;
             };",
            None,
        ),
        // Already a stateless (arrow) function
        ("const Foo = ({foo}) => <div>{foo}</div>;", None),
        // class without JSX
        (
            "class Foo {
               render() {
                 return 'hello'
               }
             };",
            None,
        ),
        // object with JSX
        ("const foo = { foo: <h>hello</h> };", None),
        // Error boundary with componentDidCatch (allowed by default)
        (
            "class Foo extends React.Component {
               componentDidCatch(error, errorInfo) {
                 logErrorToMyService(error, errorInfo);
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            None,
        ),
        // Error boundary with componentDidCatch extending PureComponent
        (
            "class Foo extends React.PureComponent {
               componentDidCatch(error, errorInfo) {
                 logErrorToMyService(error, errorInfo);
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            None,
        ),
        // Error boundary in expression context
        (
            "const Foo = class extends React.Component {
               componentDidCatch(error, errorInfo) {
                 logErrorToMyService(error, errorInfo);
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            None,
        ),
        // Error boundary with static getDerivedStateFromError
        (
            "class Foo extends React.Component {
               constructor(props) {
                 super(props);
                 this.state = { hasError: false };
               }
               static getDerivedStateFromError(error) {
                 return { hasError: true };
               }
               render() {
                 return <div>{this.state.hasError ? 'Error' : this.props.foo}</div>;
               }
             }",
            None,
        ),
        // getDerivedStateFromError extending PureComponent
        (
            "class Foo extends React.PureComponent {
               constructor(props) {
                 super(props);
                 this.state = { hasError: false };
               }
               static getDerivedStateFromError(error) {
                 return { hasError: true };
               }
               render() {
                 return <div>{this.state.hasError ? 'Error' : this.props.foo}</div>;
               }
             }",
            None,
        ),
        // getDerivedStateFromError in expression context
        (
            "const Foo = class extends React.Component {
               constructor(props) {
                 super(props);
                 this.state = { hasError: false };
               }
               static getDerivedStateFromError(error) {
                 return { hasError: true };
               }
               render() {
                 return <div>{this.state.hasError ? 'Error' : this.props.foo}</div>;
               }
             };",
            None,
        ),
        // Error boundary with allowJsxUtilityClass: true (still valid)
        (
            "class Foo extends React.Component {
               componentDidCatch(error, errorInfo) {
                 logErrorToMyService(error, errorInfo);
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        // JSX utility class with allowJsxUtilityClass: true
        (
            "class Foo {
               getBar() {
                 return <Bar />;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        // validForAllOptions with allowJsxUtilityClass: true
        (
            "const Foo = function(props) {
               return <div>{props.foo}</div>;
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "const Foo = ({foo}) => <div>{foo}</div>;",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "class Foo {
               render() {
                 return 'hello'
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "const foo = { foo: <h>hello</h> };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        // validForAllOptions with allowErrorBoundary: false
        (
            "const Foo = function(props) {
               return <div>{props.foo}</div>;
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "const Foo = ({foo}) => <div>{foo}</div>;",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "class Foo {
               render() {
                 return 'hello'
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "const foo = { foo: <h>hello</h> };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        // validForAllOptions with both options
        (
            "const Foo = function(props) {
               return <div>{props.foo}</div>;
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "const Foo = ({foo}) => <div>{foo}</div>;",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "class Foo {
               render() {
                 return 'hello'
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "const foo = { foo: <h>hello</h> };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
    ];

    // Note: Many of these test cases are duped for the purpose of testing different config options.

    let fail = vec![
        // Extending from react
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            None,
        ),
        // For now, we are not going to check for other libraries like preact and inferno.
        // This rule only supports React.
        //
        // Extending from preact
        // (
        //     "import { Component } from 'preact';
        //
        //      class Foo extends Component {
        //        render() {
        //          return <div>{this.props.foo}</div>;
        //        }
        //      };",
        //     None,
        // ),
        // Extending from inferno
        // (
        //     "import { Component } from 'inferno';
        //
        //      class Foo extends Component {
        //        render() {
        //          return <div>{this.props.foo}</div>;
        //        }
        //      };",
        //     None,
        // ),
        // Extending from another class (not Component)
        // (
        //     "import Document from 'next/document';
        //
        //      class Foo extends Document {
        //        render() {
        //          return <div>{this.props.foo}</div>;
        //        }
        //      };",
        //     None,
        // ),
        (
            "class Foo extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            None,
        ),
        (
            "class Foo extends React.PureComponent {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            None,
        ),
        (
            "const Foo = class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            None,
        ),
        (
            "export default class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            None,
        ),
        // Does not contain JSX and extends React.Component.
        (
            "class Foo extends React.Component {
               render() {
                 return null;
               }
             };",
            None,
        ),
        // Does not contain JSX and extends Component.
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return null;
               }
             }",
            None,
        ),
        // Does not contain JSX and extends React.Component in an expression context.
        (
            "const Foo = class extends React.Component {
               render() {
                 return null;
               }
             };",
            None,
        ),
        // Does not contain JSX and extends Component in an expression context.
        (
            "import { Component } from 'react';

             const Foo = class extends Component {
               render() {
                 return null;
               }
             };",
            None,
        ),
        // Does not contain JSX and extends Component in an expression context.
        (
            "import { Component } from 'react';

             const Foo = class Bar extends Component {
               render() {
                 return null;
               }
             };",
            None,
        ),
        // Does not contain JSX and extends Component in a default export expression context.
        (
            "import { Component } from 'react';

             export default class extends Component {
               render() {
                 return null;
               }
             };",
            None,
        ),
        // Does not contain JSX and extends Component in a default export expression context.
        (
            "import { Component } from 'react';

             export default class Foo extends Component {
               render() {
                 return null;
               }
             };",
            None,
        ),
        // Error boundaries with allowErrorBoundary: false
        (
            "class Foo extends React.Component {
               componentDidCatch(error, errorInfo) {
                 logErrorToMyService(error, errorInfo);
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "class Foo extends React.PureComponent {
               componentDidCatch(error, errorInfo) {
                 logErrorToMyService(error, errorInfo);
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "const Foo = class extends React.Component {
               componentDidCatch(error, errorInfo) {
                 logErrorToMyService(error, errorInfo);
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "class Foo extends React.Component {
               constructor(props) {
                 super(props);
                 this.state = { hasError: false };
               }
               static getDerivedStateFromError(error) {
                 return { hasError: true };
               }
               render() {
                 return <div>{this.state.hasError ? 'Error' : this.props.foo}</div>;
               }
             }",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "class Foo extends React.PureComponent {
               constructor(props) {
                 super(props);
                 this.state = { hasError: false };
               }
               static getDerivedStateFromError(error) {
                 return { hasError: true };
               }
               render() {
                 return <div>{this.state.hasError ? 'Error' : this.props.foo}</div>;
               }
             }",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "const Foo = class extends React.Component {
               constructor(props) {
                 super(props);
                 this.state = { hasError: false };
               }
               static getDerivedStateFromError(error) {
                 return { hasError: true };
               }
               render() {
                 return <div>{this.state.hasError ? 'Error' : this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        // JSX utility class (fails by default)
        (
            "class Foo {
               getBar() {
                 return <Bar />;
               }
             };",
            None,
        ),
        // JSX utility class with allowErrorBoundary: false (still fails)
        (
            "class Foo {
               getBar() {
                 return <Bar />;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        // invalidForAllOptions with allowJsxUtilityClass: true
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "class Foo extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "class Foo extends React.PureComponent {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "const Foo = class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "export default class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "class Foo extends React.Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return null;
               }
             }",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "const Foo = class extends React.Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "import { Component } from 'react';

             const Foo = class extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "import { Component } from 'react';

             const Foo = class Bar extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "import { Component } from 'react';

             export default class extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        (
            "import { Component } from 'react';

             export default class Foo extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowJsxUtilityClass": true }])),
        ),
        // invalidForAllOptions with allowErrorBoundary: false
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "class Foo extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "class Foo extends React.PureComponent {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "const Foo = class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "export default class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "class Foo extends React.Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return null;
               }
             }",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "const Foo = class extends React.Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "import { Component } from 'react';

             const Foo = class extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "import { Component } from 'react';

             const Foo = class Bar extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "import { Component } from 'react';

             export default class extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        (
            "import { Component } from 'react';

             export default class Foo extends Component {
               render() {
                 return null;
               }
             };",
            Some(serde_json::json!([{ "allowErrorBoundary": false }])),
        ),
        // invalidForAllOptions with both options
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "class Foo extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "class Foo extends React.PureComponent {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "const Foo = class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "export default class extends React.Component {
               render() {
                 return <div>{this.props.foo}</div>;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "class Foo extends React.Component {
               render() {
                 return null;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "import { Component } from 'react';

             class Foo extends Component {
               render() {
                 return null;
               }
             }",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "const Foo = class extends React.Component {
               render() {
                 return null;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "import { Component } from 'react';

             const Foo = class extends Component {
               render() {
                 return null;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "import { Component } from 'react';

             const Foo = class Bar extends Component {
               render() {
                 return null;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "import { Component } from 'react';

             export default class extends Component {
               render() {
                 return null;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        (
            "import { Component } from 'react';

             export default class Foo extends Component {
               render() {
                 return null;
               }
             };",
            Some(
                serde_json::json!([{ "allowJsxUtilityClass": true, "allowErrorBoundary": false }]),
            ),
        ),
        // Non-static getDerivedStateFromError is NOT a valid error boundary,
        // so allowErrorBoundary should not exempt it.
        (
            "class Foo extends React.Component {
               getDerivedStateFromError(error) {
                 return { hasError: true };
               }
               render() {
                 return <div>{this.props.foo}</div>;
               }
             }",
            None,
        ),
    ];

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