from __future__ import annotations

import asyncio
import sys
import unittest
from types import SimpleNamespace
from unittest import IsolatedAsyncioTestCase, mock

import pytest

from unittest_parametrize import ParametrizedTestCase, param, parametrize


def run_tests(test_case: type[ParametrizedTestCase]) -> unittest.TestResult:
    loader = unittest.TestLoader()
    suite = loader.loadTestsFromTestCase(test_case)
    runner = unittest.TextTestRunner()
    return runner.run(suite)


def test_wrong_length_argnames():
    with pytest.raises(ValueError) as excinfo:
        parametrize((), [])

    assert excinfo.value.args[0] == "argnames must contain at least one element"


def test_wrong_length_ids():
    with pytest.raises(ValueError) as excinfo:
        parametrize(
            "x",
            [(1,)],
            ids=[],
        )

    assert excinfo.value.args[0] == "ids must have the same length as argvalues"


def test_wrong_length_tuple():
    with pytest.raises(ValueError) as excinfo:
        parametrize(
            "x",
            [(2, 3)],
        )

    assert (
        excinfo.value.args[0]
        == "tuple at index 0 has wrong number of arguments (2 != 1)"
    )


def test_wrong_length_param():
    with pytest.raises(ValueError) as excinfo:
        parametrize(
            "x",
            [param(id="one")],
        )

    assert (
        excinfo.value.args[0]
        == "param at index 0 has wrong number of arguments (0 != 1)"
    )


def test_wrong_type_argvalues():
    with pytest.raises(TypeError) as excinfo:
        parametrize(
            "x,y",
            [{"x": 1}, {"y": 1}],
        )

    assert (
        excinfo.value.args[0]
        == "argvalue at index 0 is not a tuple, param instance, or single value: {'x': 1}"
    )


def test_wrong_argname():
    with pytest.raises(TypeError) as excinfo:

        @parametrize(
            "x",
            [(1,)],
        )
        def test_something(self, y):  # pragma: no cover
            pass

    assert excinfo.value.args[0] == "got an unexpected keyword argument 'x'"


def test_param_invalid_id():
    with pytest.raises(ValueError) as excinfo:
        param(id="!")

    assert excinfo.value.args[0] == "id must be a valid Python identifier suffix: '!'"


def test_duplicate_param_ids():
    with pytest.raises(ValueError) as excinfo:
        parametrize(
            "x",
            [
                param(1, id="a"),
                param(1, id="a"),
            ],
        )

    assert excinfo.value.args[0] == "Duplicate param id 'a'"


def test_duplicate_param_ids_mixed():
    with pytest.raises(ValueError) as excinfo:
        parametrize(
            "x",
            [
                param(1),
                param(1, id="a"),
            ],
            ids=["a", None],
        )

    assert excinfo.value.args[0] == "Duplicate param id 'a'"


def test_duplicate_tuple_param_ids_mixed():
    with pytest.raises(ValueError) as excinfo:
        parametrize(
            "x",
            [
                (1,),
                param(1, id="a"),
            ],
            ids=["a", None],
        )

    assert excinfo.value.args[0] == "Duplicate param id 'a'"


def test_duplicate_test_name():
    with pytest.raises(ValueError) as excinfo:

        class VanillaTest(ParametrizedTestCase):
            @parametrize(
                "x",
                [(1,)],
            )
            def test_something(self, x):  # pragma: no cover
                pass

            def test_something_0(self):  # pragma: no cover
                pass

    assert (
        excinfo.value.args[0] == "Duplicate test name test_something_0 in VanillaTest"
    )


def test_no_decorators_above_parametrize():
    obj = SimpleNamespace(x=1)

    with pytest.raises(TypeError) as excinfo:

        class SomethingTests(ParametrizedTestCase):
            @mock.patch.object(obj, "x", new=2)
            @parametrize(
                "y",
                [(1,)],
            )
            def test_something(self, y):  # pragma: no cover
                pass

    assert (
        excinfo.value.args[0]
        == "@parametrize must be the top-most decorator on"
        + " test_no_decorators_above_parametrize.<locals>.SomethingTests.test_something"
    )


def test_only_one_parametrize():
    with pytest.raises(TypeError) as excinfo:

        class SomethingTests(ParametrizedTestCase):
            @parametrize("x", [(1,)])
            @parametrize("y", [(2,)])
            def test_something(self, x, y):  # pragma: no cover
                pass

    assert (
        excinfo.value.args[0]
        == "@parametrize cannot be stacked on"
        + " test_only_one_parametrize.<locals>.SomethingTests.test_something"
    )


def test_vanilla():
    # Non-parametrized tests work as usual
    ran = False

    class VanillaTest(ParametrizedTestCase):
        def test_vanilla(self):
            nonlocal ran
            ran = True
            self.assertEqual(1, 1)

    run_tests(VanillaTest)

    assert ran


def test_parametrized_failure_has_note():
    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [(1, 2)],
        )
        def test_square(self, x, expected):
            self.assertEqual(x**2, expected)

    result = run_tests(SquareTests)

    assert len(result.failures) == 1
    if sys.version_info >= (3, 11):
        failure = result.failures[0]
        *_, message = failure
        assert message.endswith("\nTest parameters: x=1, expected=2\n")


def test_parametrized_async_failure_has_note():
    class SquareTests(ParametrizedTestCase, IsolatedAsyncioTestCase):
        @parametrize(
            "x,expected",
            [(1, 2)],
        )
        async def test_square(self, x, expected):
            await asyncio.sleep(0.001)
            self.assertEqual(x**2, expected)

    result = run_tests(SquareTests)

    assert len(result.failures) == 1
    if sys.version_info >= (3, 11):
        failure = result.failures[0]
        *_, message = failure
        assert message.endswith("\nTest parameters: x=1, expected=2\n")


def test_single_parametrized():
    ran = 0

    class EqualTests(ParametrizedTestCase):
        @parametrize("x", [1, 2])
        def test_equal(self, x: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x, x)

    run_tests(EqualTests)

    assert ran == 2
    assert not hasattr(EqualTests, "test_equal")
    assert hasattr(EqualTests, "test_equal_0")
    assert hasattr(EqualTests, "test_equal_1")


def test_double_parametrized():
    ran = 0

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                (1, 1),
                (2, 4),
            ],
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_0")
    assert hasattr(SquareTests, "test_square_1")


def test_double_parametrized_async():
    ran = 0

    class SquareTests(ParametrizedTestCase, IsolatedAsyncioTestCase):
        @parametrize(
            "x,expected",
            [
                (1, 1),
                (2, 4),
            ],
        )
        async def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            await asyncio.sleep(0.001)
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_0")
    assert hasattr(SquareTests, "test_square_1")


def test_argnames_whitespace():
    ran = 0

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x, expected",
            [
                (1, 1),
                (2, 4),
            ],
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_0")
    assert hasattr(SquareTests, "test_square_1")


def test_full_argnames():
    ran = 0

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            ("x", "expected"),
            [
                (1, 1),
                (2, 4),
            ],
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_0")
    assert hasattr(SquareTests, "test_square_1")


def test_custom_ids():
    ran = 0

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                (1, 1),
                (2, 4),
            ],
            ids=["one", "two"],
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_one")
    assert hasattr(SquareTests, "test_square_two")


def test_param_instances():
    ran = 0

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                param(1, 1, id="one"),
                param(2, 4, id="two"),
            ],
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_one")
    assert hasattr(SquareTests, "test_square_two")


def test_param_instance_with_ids_arg():
    ran = 0

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                param(1, 1),
            ],
            ids=["one"],
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 1
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_one")


def test_param_instances_without_id():
    ran = 0

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                param(1, 1),
                param(20, 400, id="large"),
            ],
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_0")
    assert hasattr(SquareTests, "test_square_large")


def test_param_instances_reused():
    ran = 0
    cases = [param(1, 1)]

    class SquareTests(ParametrizedTestCase):
        @parametrize("x,expected", cases)
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

        @parametrize("x,expected", cases, ids=["minusone"])
        def test_negative_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual((-x) ** 2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_0")
    assert hasattr(SquareTests, "test_negative_square_minusone")


def test_zero_parametrized():
    # Zero parametrized tests allowed to make generating tests easier

    ran = False

    class NoTests(ParametrizedTestCase):
        @parametrize("x", [])
        def test_never_runs(self, x: int) -> None:  # pragma: no cover
            nonlocal ran
            ran = True

    run_tests(NoTests)

    assert ran is False
    assert not hasattr(NoTests, "test_never_runs")
    assert not hasattr(NoTests, "test_never_runs_0")


def test_async_tests() -> None:
    ran = 0

    class AsyncTests(ParametrizedTestCase, IsolatedAsyncioTestCase):
        @parametrize("x", [(0,), (1,)])
        async def test_x(self, x: int) -> None:
            nonlocal ran
            ran += 1
            await asyncio.sleep(0.001)

    run_tests(AsyncTests)

    assert ran == 2
    assert hasattr(AsyncTests, "test_x_0")
    assert hasattr(AsyncTests, "test_x_1")


def test_callable_ids():
    ran = 0

    def make_id(value):
        return f"num{value}"

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                (1, 1),
                (2, 4),
                (3, 9),
            ],
            ids=make_id,
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 3
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_num1_num1")
    assert hasattr(SquareTests, "test_square_num2_num4")
    assert hasattr(SquareTests, "test_square_num3_num9")


def test_callable_ids_with_param_instances():
    ran = 0

    def make_id(value):
        return f"num{value}"

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                param(1, 1),
                param(2, 4),
            ],
            ids=make_id,
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_num1_num1")
    assert hasattr(SquareTests, "test_square_num2_num4")


def test_callable_ids_invalid_identifier():
    def bad_id(value):
        return "!"

    with pytest.raises(ValueError) as excinfo:

        class BadTests(ParametrizedTestCase):
            @parametrize(
                "x,expected",
                [(1, 1)],
                ids=bad_id,
            )
            def test_square(self, x: int, expected: int) -> None:  # pragma: no cover
                pass

    assert "callable ids returned invalid Python identifier suffix: '!_!'" in str(
        excinfo.value
    )


def test_callable_ids_with_mixed_param_instances():
    ran = 0

    def make_id(value):
        return f"num{value}"

    class SquareTests(ParametrizedTestCase):
        @parametrize(
            "x,expected",
            [
                param(1, 1),
                param(2, 4, id="explicit"),
            ],
            ids=make_id,
        )
        def test_square(self, x: int, expected: int) -> None:
            nonlocal ran
            ran += 1
            self.assertEqual(x**2, expected)

    run_tests(SquareTests)

    assert ran == 2
    assert not hasattr(SquareTests, "test_square")
    assert hasattr(SquareTests, "test_square_num1_num1")
    assert hasattr(SquareTests, "test_square_explicit")


def test_callable_ids_returning_none():
    ran = 0

    def even_numbers_only(value):
        if isinstance(value, int) and value % 2 == 0:
            return f"even{value}"
        return None

    class Tests(ParametrizedTestCase):
        @parametrize(
            "x,y",
            [
                (1, 2),
                (3, 4),
            ],
            ids=even_numbers_only,
        )
        def test_values(self, x: int, y: int) -> None:
            nonlocal ran
            ran += 1

    run_tests(Tests)

    assert ran == 2
    assert not hasattr(Tests, "test_values")
    assert hasattr(Tests, "test_values_1_even2")
    assert hasattr(Tests, "test_values_3_even4")
