from pathlib import Path
import re
import sys
import warnings

from _pytest import deprecated
from _pytest.compat import legacy_path
from _pytest.pytester import Pytester
import pytest
from pytest import PytestDeprecationWarning


@pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
@pytest.mark.filterwarnings("default")
def test_external_plugins_integrated(pytester: Pytester, plugin) -> None:
    pytester.syspathinsert()
    pytester.makepyfile(**{plugin: ""})

    with pytest.warns(pytest.PytestConfigWarning):
        pytester.parseconfig("-p", plugin)


def test_hookspec_via_function_attributes_are_deprecated():
    from _pytest.config import PytestPluginManager

    pm = PytestPluginManager()

    class DeprecatedHookMarkerSpec:
        def pytest_bad_hook(self):
            pass

        pytest_bad_hook.historic = False  # type: ignore[attr-defined]

    with pytest.warns(
        PytestDeprecationWarning,
        match=r"Please use the pytest\.hookspec\(historic=False\) decorator",
    ) as recorder:
        pm.add_hookspecs(DeprecatedHookMarkerSpec)
    (record,) = recorder
    assert (
        record.lineno
        == DeprecatedHookMarkerSpec.pytest_bad_hook.__code__.co_firstlineno
    )
    assert record.filename == __file__


def test_hookimpl_via_function_attributes_are_deprecated():
    from _pytest.config import PytestPluginManager

    pm = PytestPluginManager()

    class DeprecatedMarkImplPlugin:
        def pytest_runtest_call(self):
            pass

        pytest_runtest_call.tryfirst = True  # type: ignore[attr-defined]

    with pytest.warns(
        PytestDeprecationWarning,
        match=r"Please use the pytest.hookimpl\(tryfirst=True\)",
    ) as recorder:
        pm.register(DeprecatedMarkImplPlugin())
    (record,) = recorder
    assert (
        record.lineno
        == DeprecatedMarkImplPlugin.pytest_runtest_call.__code__.co_firstlineno
    )
    assert record.filename == __file__


def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None:
    module = pytester.getmodulecol(
        """
        def test_foo(): pass
        """,
        withinit=True,
    )
    assert isinstance(module, pytest.Module)
    package = module.parent
    assert isinstance(package, pytest.Package)

    with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"):
        package.gethookproxy(pytester.path)

    with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"):
        package.isinitpath(pytester.path)

    # The methods on Session are *not* deprecated.
    session = module.session
    with warnings.catch_warnings(record=True) as rec:
        session.gethookproxy(pytester.path)
        session.isinitpath(pytester.path)
    assert len(rec) == 0


def test_strict_option_is_deprecated(pytester: Pytester) -> None:
    """--strict is a deprecated alias to --strict-markers (#7530)."""
    pytester.makepyfile(
        """
        import pytest

        @pytest.mark.unknown
        def test_foo(): pass
        """
    )
    result = pytester.runpytest("--strict", "-Wdefault::pytest.PytestRemovedIn8Warning")
    result.stdout.fnmatch_lines(
        [
            "'unknown' not found in `markers` configuration option",
            "*PytestRemovedIn8Warning: The --strict option is deprecated, use --strict-markers instead.",
        ]
    )


def test_yield_fixture_is_deprecated() -> None:
    with pytest.warns(DeprecationWarning, match=r"yield_fixture is deprecated"):

        @pytest.yield_fixture
        def fix():
            assert False


def test_private_is_deprecated() -> None:
    class PrivateInit:
        def __init__(self, foo: int, *, _ispytest: bool = False) -> None:
            deprecated.check_ispytest(_ispytest)

    with pytest.warns(
        pytest.PytestDeprecationWarning, match="private pytest class or function"
    ):
        PrivateInit(10)

    # Doesn't warn.
    PrivateInit(10, _ispytest=True)


@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
    path = legacy_path(tmp_path)

    PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*"
    if hooktype == "ihook":
        hooks = request.node.ihook
    else:
        hooks = request.config.hook

    with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
        l1 = sys._getframe().f_lineno
        hooks.pytest_ignore_collect(
            config=request.config, path=path, collection_path=tmp_path
        )
        l2 = sys._getframe().f_lineno

    (record,) = r
    assert record.filename == __file__
    assert l1 < record.lineno < l2

    hooks.pytest_ignore_collect(config=request.config, collection_path=tmp_path)

    # Passing entirely *different* paths is an outright error.
    with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"):
        with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
            hooks.pytest_ignore_collect(
                config=request.config, path=path, collection_path=Path("/bla/bla")
            )


def test_warns_none_is_deprecated():
    with pytest.warns(
        PytestDeprecationWarning,
        match=re.escape(
            "Passing None has been deprecated.\n"
            "See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
            "#additional-use-cases-of-warnings-in-tests"
            " for alternatives in common use cases."
        ),
    ):
        with pytest.warns(None):  # type: ignore[call-overload]
            pass


class TestSkipMsgArgumentDeprecated:
    def test_skip_with_msg_is_deprecated(self, pytester: Pytester) -> None:
        p = pytester.makepyfile(
            """
            import pytest

            def test_skipping_msg():
                pytest.skip(msg="skippedmsg")
            """
        )
        result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
        result.stdout.fnmatch_lines(
            [
                "*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, "
                "use pytest.skip(reason=...) instead",
                '*pytest.skip(msg="skippedmsg")*',
            ]
        )
        result.assert_outcomes(skipped=1, warnings=1)

    def test_fail_with_msg_is_deprecated(self, pytester: Pytester) -> None:
        p = pytester.makepyfile(
            """
            import pytest

            def test_failing_msg():
                pytest.fail(msg="failedmsg")
            """
        )
        result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
        result.stdout.fnmatch_lines(
            [
                "*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, "
                "use pytest.fail(reason=...) instead",
                '*pytest.fail(msg="failedmsg")',
            ]
        )
        result.assert_outcomes(failed=1, warnings=1)

    def test_exit_with_msg_is_deprecated(self, pytester: Pytester) -> None:
        p = pytester.makepyfile(
            """
            import pytest

            def test_exit_msg():
                pytest.exit(msg="exitmsg")
            """
        )
        result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
        result.stdout.fnmatch_lines(
            [
                "*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, "
                "use pytest.exit(reason=...) instead",
            ]
        )
        result.assert_outcomes(warnings=1)


def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
    pytester.makeconftest(
        """
        def pytest_cmdline_preparse(config, args):
            ...

        """
    )
    result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
    result.stdout.fnmatch_lines(
        [
            "*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*",
            "*Please use pytest_load_initial_conftests hook instead.*",
        ]
    )


def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
    mod = pytester.getmodulecol("")

    class MyFile(pytest.File):
        def collect(self):
            raise NotImplementedError()

    with pytest.warns(
        pytest.PytestDeprecationWarning,
        match=re.escape(
            "The (fspath: py.path.local) argument to MyFile is deprecated."
        ),
    ):
        MyFile.from_parent(
            parent=mod.parent,
            fspath=legacy_path("bla"),
        )


def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
    with pytest.warns(
        pytest.PytestDeprecationWarning,
        match=re.escape("The pytest.Instance collector type is deprecated"),
    ):
        pytest.Instance  # type:ignore[attr-defined]

    with pytest.warns(
        pytest.PytestDeprecationWarning,
        match=re.escape("The pytest.Instance collector type is deprecated"),
    ):
        from _pytest.python import Instance  # noqa: F401


def test_fixture_disallow_on_marked_functions():
    """Test that applying @pytest.fixture to a marked function warns (#3364)."""
    with pytest.warns(
        pytest.PytestRemovedIn9Warning,
        match=r"Marks applied to fixtures have no effect",
    ) as record:

        @pytest.fixture
        @pytest.mark.parametrize("example", ["hello"])
        @pytest.mark.usefixtures("tmp_path")
        def foo():
            raise NotImplementedError()

    # it's only possible to get one warning here because you're already prevented
    # from applying @fixture twice
    # ValueError("fixture is being applied more than once to the same function")
    assert len(record) == 1


def test_fixture_disallow_marks_on_fixtures():
    """Test that applying a mark to a fixture warns (#3364)."""
    with pytest.warns(
        pytest.PytestRemovedIn9Warning,
        match=r"Marks applied to fixtures have no effect",
    ) as record:

        @pytest.mark.parametrize("example", ["hello"])
        @pytest.mark.usefixtures("tmp_path")
        @pytest.fixture
        def foo():
            raise NotImplementedError()

    assert len(record) == 2  # one for each mark decorator


def test_fixture_disallowed_between_marks():
    """Test that applying a mark to a fixture warns (#3364)."""
    with pytest.warns(
        pytest.PytestRemovedIn9Warning,
        match=r"Marks applied to fixtures have no effect",
    ) as record:

        @pytest.mark.parametrize("example", ["hello"])
        @pytest.fixture
        @pytest.mark.usefixtures("tmp_path")
        def foo():
            raise NotImplementedError()

    assert len(record) == 2  # one for each mark decorator


@pytest.mark.filterwarnings("default")
def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
    pytest.importorskip("nose")
    pytester.makepyfile(
        """
        from nose.tools import with_setup

        def setup_fn_no_op():
            ...

        def teardown_fn_no_op():
            ...

        @with_setup(setup_fn_no_op, teardown_fn_no_op)
        def test_omits_warnings():
            ...
        """
    )
    output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
    message = [
        "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
        "*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)",
        "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
        "*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `teardown_fn_no_op` (teardown)",
    ]
    output.stdout.fnmatch_lines(message)
    output.assert_outcomes(passed=1)


@pytest.mark.filterwarnings("default")
def test_nose_deprecated_setup_teardown(pytester: Pytester) -> None:
    pytest.importorskip("nose")
    pytester.makepyfile(
        """
        class Test:

            def setup(self):
                ...

            def teardown(self):
                ...

            def test(self):
                ...
        """
    )
    output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
    message = [
        "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
        "*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`",
        "*To remove this warning, rename it to `setup_method(self)`",
        "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
        "*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `teardown(self)`",
        "*To remove this warning, rename it to `teardown_method(self)`",
    ]
    output.stdout.fnmatch_lines(message)
    output.assert_outcomes(passed=1)
