use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::Decorator;
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::UnqualifiedName;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, Parameters, Stmt};
use ruff_python_semantic::analyze::visibility::is_abstract;
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
use ruff_text_size::{TextLen, TextRange};
use rustc_hash::FxHashSet;

use crate::checkers::ast::Checker;
use crate::fix::edits;
use crate::registry::Rule;
use crate::{AlwaysFixableViolation, Violation};
use crate::{Edit, Fix};

use crate::rules::flake8_pytest_style::helpers::{
    Parentheses, get_mark_decorators, is_pytest_fixture, is_pytest_yield_fixture,
    keyword_is_literal,
};

/// ## What it does
/// Checks for argument-free `@pytest.fixture()` decorators with or without
/// parentheses, depending on the [`lint.flake8-pytest-style.fixture-parentheses`]
/// setting.
///
/// ## Why is this bad?
/// If a `@pytest.fixture()` doesn't take any arguments, the parentheses are
/// optional.
///
/// Either removing those unnecessary parentheses _or_ requiring them for all
/// fixtures is fine, but it's best to be consistent. The rule defaults to
/// removing unnecessary parentheses, to match the documentation of the
/// official pytest projects.
///
/// ## Example
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture(): ...
/// ```
///
/// Use instead:
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture
/// def my_fixture(): ...
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe if there's comments in the
/// `pytest.fixture` decorator.
///
/// For example, the fix would be marked as unsafe in the following case:
/// ```python
/// import pytest
///
///
/// @pytest.fixture(
///     # comment
///     # scope = "module"
/// )
/// def my_fixture(): ...
/// ```
///
/// ## Options
/// - `lint.flake8-pytest-style.fixture-parentheses`
///
/// ## References
/// - [`pytest` documentation: API Reference: Fixtures](https://docs.pytest.org/en/latest/reference/reference.html#fixtures-api)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestFixtureIncorrectParenthesesStyle {
    expected: Parentheses,
    actual: Parentheses,
}

impl AlwaysFixableViolation for PytestFixtureIncorrectParenthesesStyle {
    #[derive_message_formats]
    fn message(&self) -> String {
        let PytestFixtureIncorrectParenthesesStyle { expected, actual } = self;
        format!("Use `@pytest.fixture{expected}` over `@pytest.fixture{actual}`")
    }

    fn fix_title(&self) -> String {
        let PytestFixtureIncorrectParenthesesStyle { expected, .. } = self;
        match expected {
            Parentheses::None => "Remove parentheses".to_string(),
            Parentheses::Empty => "Add parentheses".to_string(),
        }
    }
}

/// ## What it does
/// Checks for `pytest.fixture` calls with positional arguments.
///
/// ## Why is this bad?
/// For clarity and consistency, prefer using keyword arguments to specify
/// fixture configuration.
///
/// ## Example
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture("module")
/// def my_fixture(): ...
/// ```
///
/// Use instead:
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture(scope="module")
/// def my_fixture(): ...
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestFixturePositionalArgs {
    function: String,
}

impl Violation for PytestFixturePositionalArgs {
    #[derive_message_formats]
    fn message(&self) -> String {
        let PytestFixturePositionalArgs { function } = self;
        format!("Configuration for fixture `{function}` specified via positional args, use kwargs")
    }
}

/// ## What it does
/// Checks for `pytest.fixture` calls with `scope="function"`.
///
/// ## Why is this bad?
/// `scope="function"` can be omitted, as it is the default.
///
/// ## Example
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture(scope="function")
/// def my_fixture(): ...
/// ```
///
/// Use instead:
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture(): ...
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestExtraneousScopeFunction;

impl AlwaysFixableViolation for PytestExtraneousScopeFunction {
    #[derive_message_formats]
    fn message(&self) -> String {
        "`scope='function'` is implied in `@pytest.fixture()`".to_string()
    }

    fn fix_title(&self) -> String {
        "Remove implied `scope` argument".to_string()
    }
}

/// ## Removal
/// This rule has been removed because marking fixtures that do not return a value with an underscore
/// isn't a practice recommended by the pytest community.
///
/// ## What it does
/// Checks for `pytest` fixtures that do not return a value, but are not named
/// with a leading underscore.
///
/// ## Why is this bad?
/// By convention, fixtures that don't return a value should be named with a
/// leading underscore, while fixtures that do return a value should not.
///
/// This rule ignores abstract fixtures and generators.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def patch_something(mocker):
///     mocker.patch("module.object")
///
///
/// @pytest.fixture()
/// def use_context():
///     with create_context():
///         yield
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def _patch_something(mocker):
///     mocker.patch("module.object")
///
///
/// @pytest.fixture()
/// def _use_context():
///     with create_context():
///         yield
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[derive(ViolationMetadata)]
#[deprecated(note = "PT004 has been removed")]
#[violation_metadata(removed_since = "0.8.0")]
pub(crate) struct PytestMissingFixtureNameUnderscore;

#[expect(deprecated)]
impl Violation for PytestMissingFixtureNameUnderscore {
    fn message(&self) -> String {
        unreachable!("PT004 has been removed");
    }

    fn message_formats() -> &'static [&'static str] {
        &["Fixture `{function}` does not return anything, add leading underscore"]
    }
}

/// ## Removal
/// This rule has been removed because marking fixtures that do not return a value with an underscore
/// isn't a practice recommended by the pytest community.
///
/// ## What it does
/// Checks for `pytest` fixtures that return a value, but are named with a
/// leading underscore.
///
/// ## Why is this bad?
/// By convention, fixtures that don't return a value should be named with a
/// leading underscore, while fixtures that do return a value should not.
///
/// This rule ignores abstract fixtures.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def _some_object():
///     return SomeClass()
///
///
/// @pytest.fixture()
/// def _some_object_with_cleanup():
///     obj = SomeClass()
///     yield obj
///     obj.cleanup()
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def some_object():
///     return SomeClass()
///
///
/// @pytest.fixture()
/// def some_object_with_cleanup():
///     obj = SomeClass()
///     yield obj
///     obj.cleanup()
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[derive(ViolationMetadata)]
#[deprecated(note = "PT005 has been removed")]
#[violation_metadata(removed_since = "0.8.0")]
pub(crate) struct PytestIncorrectFixtureNameUnderscore;

#[expect(deprecated)]
impl Violation for PytestIncorrectFixtureNameUnderscore {
    fn message(&self) -> String {
        unreachable!("PT005 has been removed");
    }

    fn message_formats() -> &'static [&'static str] {
        &["Fixture `{function}` returns a value, remove leading underscore"]
    }
}

/// ## What it does
/// Checks for `pytest` test functions that should be decorated with
/// `@pytest.mark.usefixtures`.
///
/// ## Why is this bad?
/// In `pytest`, fixture injection is used to activate fixtures in a test
/// function.
///
/// Fixtures can be injected either by passing them as parameters to the test
/// function, or by using the `@pytest.mark.usefixtures` decorator.
///
/// If the test function depends on the fixture being activated, but does not
/// use it in the test body or otherwise rely on its return value, prefer
/// the `@pytest.mark.usefixtures` decorator, to make the dependency explicit
/// and avoid the confusion caused by unused arguments.
///
/// ## Example
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture
/// def _patch_something(): ...
///
///
/// def test_foo(_patch_something): ...
/// ```
///
/// Use instead:
///
/// ```python
/// import pytest
///
///
/// @pytest.fixture
/// def _patch_something(): ...
///
///
/// @pytest.mark.usefixtures("_patch_something")
/// def test_foo(): ...
/// ```
///
/// ## References
/// - [`pytest` documentation: `pytest.mark.usefixtures`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-mark-usefixtures)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestFixtureParamWithoutValue {
    name: String,
}

impl Violation for PytestFixtureParamWithoutValue {
    #[derive_message_formats]
    fn message(&self) -> String {
        let PytestFixtureParamWithoutValue { name } = self;
        format!(
            "Fixture `{name}` without value is injected as parameter, use \
             `@pytest.mark.usefixtures` instead"
        )
    }
}

/// ## What it does
/// Checks for `pytest.yield_fixture` usage.
///
/// ## Why is this bad?
/// `pytest.yield_fixture` is deprecated. `pytest.fixture` should be used instead.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.yield_fixture()
/// def my_fixture():
///     obj = SomeClass()
///     yield obj
///     obj.cleanup()
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture():
///     obj = SomeClass()
///     yield obj
///     obj.cleanup()
/// ```
///
/// ## References
/// - [`pytest` documentation: `yield_fixture` functions](https://docs.pytest.org/en/latest/yieldfixture.html)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestDeprecatedYieldFixture;

impl Violation for PytestDeprecatedYieldFixture {
    #[derive_message_formats]
    fn message(&self) -> String {
        "`@pytest.yield_fixture` is deprecated, use `@pytest.fixture`".to_string()
    }
}

/// ## What it does
/// Checks for unnecessary `request.addfinalizer` usages in `pytest` fixtures.
///
/// ## Why is this bad?
/// `pytest` offers two ways to perform cleanup in fixture code. The first is
/// sequential (via the `yield` statement), the second callback-based (via
/// `request.addfinalizer`).
///
/// The sequential approach is more readable and should be preferred, unless
/// the fixture uses the "factory as fixture" pattern.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture(request):
///     resource = acquire_resource()
///     request.addfinalizer(resource.release)
///     return resource
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture():
///     resource = acquire_resource()
///     yield resource
///     resource.release()
///
///
/// # "factory-as-fixture" pattern
/// @pytest.fixture()
/// def my_factory(request):
///     def create_resource(arg):
///         resource = acquire_resource(arg)
///         request.addfinalizer(resource.release)
///         return resource
///
///     return create_resource
/// ```
///
/// ## References
/// - [`pytest` documentation: Adding finalizers directly](https://docs.pytest.org/en/latest/how-to/fixtures.html#adding-finalizers-directly)
/// - [`pytest` documentation: Factories as fixtures](https://docs.pytest.org/en/latest/how-to/fixtures.html#factories-as-fixtures)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestFixtureFinalizerCallback;

impl Violation for PytestFixtureFinalizerCallback {
    #[derive_message_formats]
    fn message(&self) -> String {
        "Use `yield` instead of `request.addfinalizer`".to_string()
    }
}
/// ## What it does
/// Checks for unnecessary `yield` expressions in `pytest` fixtures.
///
/// ## Why is this bad?
/// In `pytest` fixtures, the `yield` expression should only be used for fixtures
/// that include teardown code, to clean up the fixture after the test function
/// has finished executing.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture():
///     resource = acquire_resource()
///     yield resource
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture_with_teardown():
///     resource = acquire_resource()
///     yield resource
///     resource.release()
///
///
/// @pytest.fixture()
/// def my_fixture_without_teardown():
///     resource = acquire_resource()
///     return resource
/// ```
///
/// ## Fix safety
///
/// This rule's fix is always marked unsafe because removing the `yield` can change the behavior of
/// code that relies on implicit cleanup, such as when a value is garbage-collected.
///
/// ## References
/// - [`pytest` documentation: Teardown/Cleanup](https://docs.pytest.org/en/latest/how-to/fixtures.html#teardown-cleanup-aka-fixture-finalization)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestUselessYieldFixture {
    name: String,
}

impl AlwaysFixableViolation for PytestUselessYieldFixture {
    #[derive_message_formats]
    fn message(&self) -> String {
        let PytestUselessYieldFixture { name } = self;
        format!("No teardown in fixture `{name}`, use `return` instead of `yield`")
    }

    fn fix_title(&self) -> String {
        "Replace `yield` with `return`".to_string()
    }
}

/// ## What it does
/// Checks for `pytest.mark.usefixtures` decorators applied to `pytest`
/// fixtures.
///
/// ## Why is this bad?
/// The `pytest.mark.usefixtures` decorator has no effect on `pytest` fixtures.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def a():
///     pass
///
///
/// @pytest.mark.usefixtures("a")
/// @pytest.fixture()
/// def b(a):
///     pass
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def a():
///     pass
///
///
/// @pytest.fixture()
/// def b(a):
///     pass
/// ```
///
/// ## References
/// - [`pytest` documentation: `pytest.mark.usefixtures`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-mark-usefixtures)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestErroneousUseFixturesOnFixture;

impl AlwaysFixableViolation for PytestErroneousUseFixturesOnFixture {
    #[derive_message_formats]
    fn message(&self) -> String {
        "`pytest.mark.usefixtures` has no effect on fixtures".to_string()
    }

    fn fix_title(&self) -> String {
        "Remove `pytest.mark.usefixtures`".to_string()
    }
}

/// ## What it does
/// Checks for unnecessary `@pytest.mark.asyncio` decorators applied to fixtures.
///
/// ## Why is this bad?
/// `pytest.mark.asyncio` is unnecessary for fixtures.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.mark.asyncio()
/// @pytest.fixture()
/// async def my_fixture():
///     return 0
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// async def my_fixture():
///     return 0
/// ```
///
/// ## References
/// - [PyPI: `pytest-asyncio`](https://pypi.org/project/pytest-asyncio/)
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.208")]
pub(crate) struct PytestUnnecessaryAsyncioMarkOnFixture;

impl AlwaysFixableViolation for PytestUnnecessaryAsyncioMarkOnFixture {
    #[derive_message_formats]
    fn message(&self) -> String {
        "`pytest.mark.asyncio` is unnecessary for fixtures".to_string()
    }

    fn fix_title(&self) -> String {
        "Remove `pytest.mark.asyncio`".to_string()
    }
}

/// Visitor that skips functions
#[derive(Debug, Default)]
struct SkipFunctionsVisitor<'a> {
    has_return_with_value: bool,
    has_yield_from: bool,
    yield_statements: Vec<&'a Expr>,
    addfinalizer_call: Option<&'a Expr>,
}

impl<'a> Visitor<'a> for SkipFunctionsVisitor<'a> {
    fn visit_stmt(&mut self, stmt: &'a Stmt) {
        match stmt {
            Stmt::Return(ast::StmtReturn {
                value,
                range: _,
                node_index: _,
            }) => {
                if value.is_some() {
                    self.has_return_with_value = true;
                }
            }
            Stmt::FunctionDef(_) => {}
            _ => visitor::walk_stmt(self, stmt),
        }
    }

    fn visit_expr(&mut self, expr: &'a Expr) {
        match expr {
            Expr::YieldFrom(_) => {
                self.has_yield_from = true;
            }
            Expr::Yield(ast::ExprYield {
                value,
                range: _,
                node_index: _,
            }) => {
                self.yield_statements.push(expr);
                if value.is_some() {
                    self.has_return_with_value = true;
                }
            }
            Expr::Call(ast::ExprCall { func, .. }) => {
                if UnqualifiedName::from_expr(func)
                    .is_some_and(|name| matches!(name.segments(), ["request", "addfinalizer"]))
                {
                    self.addfinalizer_call = Some(expr);
                }
                visitor::walk_expr(self, expr);
            }
            _ => {}
        }
    }
}

fn fixture_decorator<'a>(decorators: &'a [Decorator], checker: &Checker) -> Option<&'a Decorator> {
    decorators.iter().find(|decorator| {
        is_pytest_fixture(decorator, checker)
            || is_pytest_yield_fixture(decorator, checker.semantic())
    })
}

fn pytest_fixture_parentheses(
    checker: &Checker,
    decorator: &Decorator,
    fix: Fix,
    expected: Parentheses,
    actual: Parentheses,
) {
    let mut diagnostic = checker.report_diagnostic(
        PytestFixtureIncorrectParenthesesStyle { expected, actual },
        decorator.range(),
    );
    diagnostic.set_fix(fix);
}

/// PT001, PT002, PT003
fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decorator) {
    match &decorator.expression {
        Expr::Call(ast::ExprCall {
            func: _,
            arguments,
            range: _,
            node_index: _,
        }) => {
            if checker.is_rule_enabled(Rule::PytestFixtureIncorrectParenthesesStyle) {
                if !checker.settings().flake8_pytest_style.fixture_parentheses
                    && arguments.args.is_empty()
                    && arguments.keywords.is_empty()
                {
                    let fix = Fix::applicable_edit(
                        Edit::range_deletion(arguments.range()),
                        if checker.comment_ranges().intersects(arguments.range()) {
                            Applicability::Unsafe
                        } else {
                            Applicability::Safe
                        },
                    );
                    pytest_fixture_parentheses(
                        checker,
                        decorator,
                        fix,
                        Parentheses::None,
                        Parentheses::Empty,
                    );
                }
            }

            if checker.is_rule_enabled(Rule::PytestFixturePositionalArgs) {
                if !arguments.args.is_empty() {
                    checker.report_diagnostic(
                        PytestFixturePositionalArgs {
                            function: func_name.to_string(),
                        },
                        decorator.range(),
                    );
                }
            }

            if checker.is_rule_enabled(Rule::PytestExtraneousScopeFunction) {
                if let Some(keyword) = arguments.find_keyword("scope") {
                    if keyword_is_literal(keyword, "function") {
                        let mut diagnostic = checker
                            .report_diagnostic(PytestExtraneousScopeFunction, keyword.range());
                        diagnostic.try_set_fix(|| {
                            edits::remove_argument(
                                keyword,
                                arguments,
                                edits::Parentheses::Preserve,
                                checker.source(),
                                checker.tokens(),
                            )
                            .map(Fix::unsafe_edit)
                        });
                    }
                }
            }
        }
        _ => {
            if checker.is_rule_enabled(Rule::PytestFixtureIncorrectParenthesesStyle) {
                if checker.settings().flake8_pytest_style.fixture_parentheses {
                    let fix = Fix::safe_edit(Edit::insertion(
                        Parentheses::Empty.to_string(),
                        decorator.end(),
                    ));
                    pytest_fixture_parentheses(
                        checker,
                        decorator,
                        fix,
                        Parentheses::Empty,
                        Parentheses::None,
                    );
                }
            }
        }
    }
}

/// PT022
fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns: Option<&Expr>) {
    let mut visitor = SkipFunctionsVisitor::default();

    for stmt in body {
        visitor.visit_stmt(stmt);
    }

    if checker.is_rule_enabled(Rule::PytestUselessYieldFixture) {
        let Some(stmt) = body.last() else {
            return;
        };
        let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
            return;
        };
        if !value.is_yield_expr() {
            return;
        }
        if visitor.yield_statements.len() != 1 {
            return;
        }
        let mut diagnostic = checker.report_diagnostic(
            PytestUselessYieldFixture {
                name: name.to_string(),
            },
            stmt.range(),
        );
        let yield_edit = Edit::range_replacement(
            "return".to_string(),
            TextRange::at(stmt.start(), "yield".text_len()),
        );
        let return_type_edit = returns.and_then(|returns| {
            let ast::ExprSubscript { value, slice, .. } = returns.as_subscript_expr()?;
            let ast::ExprTuple { elts, .. } = slice.as_tuple_expr()?;
            let [first, ..] = elts.as_slice() else {
                return None;
            };
            if !checker.semantic().match_typing_expr(value, "Generator") {
                return None;
            }
            Some(Edit::range_replacement(
                checker.generator().expr(first),
                returns.range(),
            ))
        });
        if let Some(return_type_edit) = return_type_edit {
            diagnostic.set_fix(Fix::unsafe_edits(yield_edit, [return_type_edit]));
        } else {
            diagnostic.set_fix(Fix::unsafe_edit(yield_edit));
        }
    }
}

/// PT019
fn check_test_function_args(checker: &Checker, parameters: &Parameters, decorators: &[Decorator]) {
    let mut named_parametrize = FxHashSet::default();
    for decorator in decorators.iter().filter(|decorator| {
        UnqualifiedName::from_expr(map_callable(&decorator.expression))
            .is_some_and(|name| matches!(name.segments(), ["pytest", "mark", "parametrize"]))
    }) {
        let Some(call_expr) = decorator.expression.as_call_expr() else {
            continue;
        };
        let Some(first_arg) = call_expr.arguments.find_argument_value("argnames", 0) else {
            continue;
        };

        match first_arg {
            Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
                named_parametrize.extend(
                    value
                        .to_str()
                        .split(',')
                        .map(str::trim)
                        .filter(|param| !param.is_empty() && param.starts_with('_')),
                );
            }

            Expr::Name(_) => return,
            Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. })
                if elts.iter().any(Expr::is_name_expr) =>
            {
                return;
            }
            Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
                named_parametrize.extend(
                    elts.iter()
                        .filter_map(Expr::as_string_literal_expr)
                        .map(|param| param.value.to_str().trim())
                        .filter(|param| !param.is_empty() && param.starts_with('_')),
                );
            }
            _ => {}
        }
    }

    for parameter in parameters.iter_non_variadic_params() {
        let name = parameter.name();
        if name.starts_with('_') && !named_parametrize.contains(name.as_str()) {
            checker.report_diagnostic(
                PytestFixtureParamWithoutValue {
                    name: name.to_string(),
                },
                parameter.range(),
            );
        }
    }
}

/// PT020
fn check_fixture_decorator_name(checker: &Checker, decorator: &Decorator) {
    if is_pytest_yield_fixture(decorator, checker.semantic()) {
        let mut diagnostic =
            checker.report_diagnostic(PytestDeprecatedYieldFixture, decorator.range());
        diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
    }
}

/// PT021
fn check_fixture_addfinalizer(checker: &Checker, parameters: &Parameters, body: &[Stmt]) {
    if !parameters.includes("request") {
        return;
    }

    let mut visitor = SkipFunctionsVisitor::default();

    for stmt in body {
        visitor.visit_stmt(stmt);
    }

    if let Some(addfinalizer) = visitor.addfinalizer_call {
        checker.report_diagnostic(PytestFixtureFinalizerCallback, addfinalizer.range());
    }
}

/// PT024, PT025
fn check_fixture_marks(checker: &Checker, decorators: &[Decorator]) {
    for (expr, marker) in get_mark_decorators(decorators, checker.semantic()) {
        if checker.is_rule_enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) {
            if marker == "asyncio" {
                let mut diagnostic =
                    checker.report_diagnostic(PytestUnnecessaryAsyncioMarkOnFixture, expr.range());
                let range = checker.locator().full_lines_range(expr.range());
                diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range)));
            }
        }

        if checker.is_rule_enabled(Rule::PytestErroneousUseFixturesOnFixture) {
            if marker == "usefixtures" {
                let mut diagnostic =
                    checker.report_diagnostic(PytestErroneousUseFixturesOnFixture, expr.range());
                let line_range = checker.locator().full_lines_range(expr.range());
                diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(line_range)));
            }
        }
    }
}

pub(crate) fn fixture(
    checker: &Checker,
    name: &str,
    parameters: &Parameters,
    returns: Option<&Expr>,
    decorators: &[Decorator],
    body: &[Stmt],
) {
    let decorator = fixture_decorator(decorators, checker);
    if let Some(decorator) = decorator {
        if checker.is_rule_enabled(Rule::PytestFixtureIncorrectParenthesesStyle)
            || checker.is_rule_enabled(Rule::PytestFixturePositionalArgs)
            || checker.is_rule_enabled(Rule::PytestExtraneousScopeFunction)
        {
            check_fixture_decorator(checker, name, decorator);
        }

        if checker.is_rule_enabled(Rule::PytestDeprecatedYieldFixture) {
            check_fixture_decorator_name(checker, decorator);
        }

        if checker.is_rule_enabled(Rule::PytestUselessYieldFixture)
            && !is_abstract(decorators, checker.semantic())
        {
            check_fixture_returns(checker, name, body, returns);
        }

        if checker.is_rule_enabled(Rule::PytestFixtureFinalizerCallback) {
            check_fixture_addfinalizer(checker, parameters, body);
        }

        if checker.is_rule_enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture)
            || checker.is_rule_enabled(Rule::PytestErroneousUseFixturesOnFixture)
        {
            check_fixture_marks(checker, decorators);
        }
    }

    if checker.is_rule_enabled(Rule::PytestFixtureParamWithoutValue) && name.starts_with("test_") {
        check_test_function_args(checker, parameters, decorators);
    }
}
