use oxc_ast::{AstKind, ast::Argument};
use oxc_diagnostics::OxcDiagnostic;
use oxc_span::Span;
use oxc_str::CompactStr;
use schemars::JsonSchema;
use serde::Deserialize;

use crate::{
    context::LintContext,
    rule::DefaultRuleConfig,
    utils::{
        JestFnKind, JestGeneralFnKind, ParsedJestFnCallNew, PossibleJestNode, parse_jest_fn_call,
    },
};

fn prefer_lowercase_title_diagnostic(title: &str, span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Enforce lowercase test names")
        .with_help(format!("`{title:?}`s should begin with lowercase"))
        .with_label(span)
}

pub const DOCUMENTATION: &str = r"### What it does

Enforce `it`, `test`, `describe`, and `bench` to have descriptions that begin with a
lowercase letter. This provides more readable test failures.

### Why is this bad?

Lowercase messages for test failures generally result in more grammatically correct
failure messages when you have a test failure.

### Examples

Examples of **incorrect** code for this rule:
```javascript
it('Adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
});
```

Examples of **correct** code for this rule:
```javascript
it('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
});
```
";

#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct PreferLowercaseTitleConfig {
    /// This array option allows specifying prefixes, which contain capitals that titles
    /// can start with. This can be useful when writing tests for API endpoints, where
    /// you'd like to prefix with the HTTP method.
    /// By default, nothing is allowed (the equivalent of `{ "allowedPrefixes": [] }`).
    ///
    /// Example of **correct** code for the `{ "allowedPrefixes": ["GET"] }` option:
    /// ```js
    /// /* jest/prefer-lowercase-title: ["error", { "allowedPrefixes": ["GET"] }] */
    /// describe('GET /live');
    /// ```
    allowed_prefixes: Vec<CompactStr>,
    /// This array option controls which Jest or Vitest functions are checked by this rule. There
    /// are four possible values:
    /// - `"describe"`
    /// - `"test"`
    /// - `"it"`
    /// - `"bench"`
    ///
    /// By default, none of these options are enabled (the equivalent of
    /// `{ "ignore": [] }`).
    ///
    /// Example of **correct** code for the `{ "ignore": ["describe"] }` option:
    /// ```js
    /// /* jest/prefer-lowercase-title: ["error", { "ignore": ["describe"] }] */
    /// describe('Uppercase description');
    /// ```
    ///
    /// Example of **correct** code for the `{ "ignore": ["test"] }` option:
    /// ```js
    /// /* jest/prefer-lowercase-title: ["error", { "ignore": ["test"] }] */
    /// test('Uppercase description');
    /// ```
    ///
    /// Example of **correct** code for the `{ "ignore": ["it"] }` option:
    /// ```js
    /// /* jest/prefer-lowercase-title: ["error", { "ignore": ["it"] }] */
    /// it('Uppercase description');
    /// ```
    ignore: Vec<CompactStr>,
    /// This option can be set to allow only the top-level `describe` blocks to have a
    /// title starting with an upper-case letter.
    ///
    /// Example of **correct** code for the `{ "ignoreTopLevelDescribe": true }` option:
    /// ```js
    /// /* jest/prefer-lowercase-title: ["error", { "ignoreTopLevelDescribe": true }] */
    /// describe('MyClass', () => {
    ///     describe('#myMethod', () => {
    ///         it('does things', () => {
    ///             //
    ///         });
    ///     });
    /// });
    /// ```
    ignore_top_level_describe: bool,
    /// This option can be set to only validate that the first character of a test name is lowercased.
    ///
    /// Example of **correct** code for the `{ "lowercaseFirstCharacterOnly": true }` option:
    /// ```js
    /// /* vitest/prefer-lowercase-title: ["error", { "lowercaseFirstCharacterOnly": true }] */
    /// describe('myClass', () => {
    ///     describe('myMethod', () => {
    ///         it('does things', () => {
    ///             //
    ///         });
    ///     });
    /// });
    /// ```
    ///
    /// Example of **incorrect** code for the `{ "lowercaseFirstCharacterOnly": true }` option:
    /// ```js
    /// /* vitest/prefer-lowercase-title: ["error", { "lowercaseFirstCharacterOnly": true }] */
    /// describe('MyClass', () => {
    ///     describe('MyMethod', () => {
    ///         it('does things', () => {
    ///             //
    ///         });
    ///     });
    /// });
    /// ```
    lowercase_first_character_only: bool,
}

impl Default for PreferLowercaseTitleConfig {
    fn default() -> Self {
        Self {
            allowed_prefixes: Vec::new(),
            ignore: Vec::new(),
            ignore_top_level_describe: false,
            lowercase_first_character_only: true,
        }
    }
}

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

    pub fn run_on_jest_node<'a, 'c>(
        &self,
        possible_jest_node: &PossibleJestNode<'a, 'c>,
        ctx: &'c LintContext<'a>,
    ) {
        let node = possible_jest_node.node;
        let AstKind::CallExpression(call_expr) = node.kind() else {
            return;
        };
        let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
            parse_jest_fn_call(call_expr, possible_jest_node, ctx)
        else {
            return;
        };

        let scopes = ctx.scoping();

        let ignores = Self::populate_ignores(&self.ignore);

        if ignores.contains(&jest_fn_call.name.as_ref()) {
            return;
        }

        if matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe)) {
            if self.ignore_top_level_describe && scopes.scope_flags(node.scope_id()).is_top() {
                return;
            }
        } else if !matches!(
            jest_fn_call.kind,
            JestFnKind::General(JestGeneralFnKind::Test | JestGeneralFnKind::Bench)
        ) {
            return;
        }

        let Some(arg) = call_expr.arguments.first() else {
            return;
        };

        if let Argument::StringLiteral(string_expr) = arg {
            self.lint_string(ctx, string_expr.value.as_str(), string_expr.span);
        } else if let Argument::TemplateLiteral(template_expr) = arg {
            let Some(template_string) = template_expr.single_quasi() else {
                return;
            };
            self.lint_string(ctx, template_string.as_str(), template_expr.span);
        }
    }

    fn lint_string<'a>(&self, ctx: &LintContext<'a>, literal: &'a str, span: Span) {
        if literal.is_empty()
            || self.allowed_prefixes.iter().any(|name| literal.starts_with(name.as_str()))
        {
            return;
        }

        if self.lowercase_first_character_only {
            let Some(first_char) = literal.chars().next() else {
                return;
            };

            let lower = first_char.to_ascii_lowercase();
            if first_char == lower {
                return;
            }
        } else if !literal.bytes().any(|b| b.is_ascii_uppercase()) {
            return;
        }

        let replacement = if self.lowercase_first_character_only {
            cow_utils::CowUtils::cow_to_ascii_lowercase(&literal.chars().as_str()[0..1])
        } else {
            cow_utils::CowUtils::cow_to_ascii_lowercase(literal)
        };

        #[expect(clippy::cast_possible_truncation)]
        let replacement_len = replacement.len() as u32;

        ctx.diagnostic_with_fix(prefer_lowercase_title_diagnostic(literal, span), |fixer| {
            fixer.replace(Span::sized(span.start + 1, replacement_len), replacement.into_owned())
        });
    }

    fn populate_ignores(ignore: &[CompactStr]) -> Vec<&str> {
        let mut ignores: Vec<&str> = vec![];
        let test_case_name = ["fit", "it", "xit", "test", "xtest"];
        let describe_alias = ["describe", "fdescribe", "xdescribe"];
        let test_name = "test";
        let it_name = "it";
        let bench_name = "bench";

        if ignore.iter().any(|alias| alias == "describe") {
            ignores.extend(describe_alias.iter());
        }

        if ignore.iter().any(|alias| alias == bench_name) {
            ignores.push(bench_name);
        }

        if ignore.iter().any(|alias| alias == test_name) {
            ignores.extend(test_case_name.iter().filter(|alias| alias.ends_with(test_name)));
        }

        if ignore.iter().any(|alias| alias == it_name) {
            ignores.extend(test_case_name.iter().filter(|alias| alias.ends_with(it_name)));
        }

        ignores
    }
}
