"""Tests for the monotonic marker."""

import textwrap


def test_monotonic_desc_all_implied(pytester):
    """When the highest count passes, all lower counts are implied."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("count")
        @pytest.mark.parametrize("count", [10, 50, 100])
        def test_count(count):
            assert True
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=3)
    lines = result.stdout.str()
    assert "100" in lines
    assert "IMPLIED" in lines


def test_monotonic_bisect_default_true(pytester):
    """monotonic() defaults to bisect=True."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n")
        @pytest.mark.parametrize("n", range(1, 17))
        def test_load(n):
            assert True
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=16)
    result.stdout.fnmatch_lines([
        "*IMPLIED PASS monotonic bisect (n, desc)*",
    ])
    assert "IMPLIED FAIL" not in result.stdout.str()


def test_monotonic_desc_failure_bisects(pytester):
    """When the highest fails, the next runs; implied stops at the pass."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("count")
        @pytest.mark.parametrize("count", [10, 50, 100])
        def test_count(count):
            assert count < 100
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=2, failed=1)
    lines = result.stdout.str()
    # 100 runs first (desc) and fails
    assert "FAILED" in lines
    # 50 runs (passes) → 10 implied
    assert "IMPLIED" in lines


def test_monotonic_asc(pytester):
    """direction='asc' runs smallest first."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("count", direction="asc")
        @pytest.mark.parametrize("count", [10, 50, 100])
        def test_count(count):
            assert True
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=3)
    lines = result.stdout.str()
    assert "IMPLIED" in lines


def test_monotonic_no_imply_flag(pytester):
    """--no-imply disables implication; all tests run."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        results = []

        @pytest.mark.monotonic("count")
        @pytest.mark.parametrize("count", [10, 50, 100])
        def test_count(count):
            results.append(count)
            assert True
    """))
    result = pytester.runpytest("-v", "--no-imply")
    result.assert_outcomes(passed=3)
    assert "IMPLIED" not in result.stdout.str()


def test_monotonic_ordering(pytester):
    """Monotonic groups are reordered: highest first for desc."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        order = []

        @pytest.mark.monotonic("n")
        @pytest.mark.parametrize("n", [1, 2, 3])
        def test_order(n):
            order.append(n)

        def test_check():
            # Only n3 actually runs (n2, n1 are implied)
            assert order == [3]
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=4)


def test_monotonic_independent_groups(pytester):
    """Different test functions form independent monotonic groups."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n")
        @pytest.mark.parametrize("n", [1, 5])
        def test_alpha(n):
            assert True

        @pytest.mark.monotonic("n")
        @pytest.mark.parametrize("n", [1, 5])
        def test_beta(n):
            assert n < 5   # 5 fails for beta

    """))
    result = pytester.runpytest("-v")
    # alpha: n5 passes → n1 implied (2 passed)
    # beta: n5 fails → n1 runs and passes (1 failed, 1 passed)
    result.assert_outcomes(passed=3, failed=1)


def test_bisect_all_pass(pytester):
    """bisect=True with extreme passing → all others IMPLIED PASS."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", [1, 2, 3, 4, 5, 6, 7, 8])
        def test_load(n):
            assert True
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=8)
    lines = result.stdout.str()
    assert "IMPLIED PASS" in lines
    assert "IMPLIED FAIL" not in lines


def test_bisect_all_fail(pytester):
    """bisect=True with all failing → extreme fails, all others probed."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", [1, 2, 3, 4, 5])
        def test_load(n):
            assert False
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.str()
    # All items fail — binary search probes each level
    # Depending on boundary logic, some may be IMPLIED FAIL
    assert "FAILED" in lines
    assert "IMPLIED FAIL" in lines


def test_bisect_finds_boundary(pytester):
    """bisect=True finds the pass/fail boundary with O(log n) probes."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", range(1, 9))
        def test_load(n):
            # desc order: 8,7,...,1 — threshold at n=4
            assert n <= 4
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.str()
    assert "IMPLIED PASS" in lines
    assert "IMPLIED FAIL" in lines
    # n=8 fails (extreme), n=4 passes, n=6 fails, n=5 fails
    # boundary at index 4 (n=4) → n=3,2,1 IMPLIED PASS, n=5,6,7 IMPLIED FAIL
    # Total: n=8 FAILED, n=4 PASSED, n=6 FAILED, n=5 FAILED,
    #        plus implied pass/fail for the rest


def test_bisect_boundary_at_extreme(pytester):
    """bisect=True when only the extreme fails; boundary is index 1."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", [1, 2, 3, 4])
        def test_load(n):
            # desc: 4,3,2,1 — only n=4 fails
            assert n < 4
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.str()
    assert "FAILED" in lines
    assert "IMPLIED PASS" in lines


def test_bisect_asc(pytester):
    """bisect=True with direction='asc' probes from the low end."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", direction="asc", bisect=True)
        @pytest.mark.parametrize("n", [1, 2, 3, 4, 5])
        def test_load(n):
            assert True
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=5)
    lines = result.stdout.str()
    assert "IMPLIED PASS" in lines
    assert "IMPLIED FAIL" not in lines


def test_bisect_two_items(pytester):
    """bisect=True with only two parametrize values."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", [1, 10])
        def test_load(n):
            assert n < 10
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.str()
    # n=10 (extreme, desc) fails → n=1 probed, passes
    assert "FAILED" in lines
    assert "PASSED" in lines


def test_bisect_propagates_implies_tokens(pytester):
    """bisect implied-pass items propagate their implies tokens."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.implies("load_ok")
        @pytest.mark.parametrize("n", [1, 2, 3, 4])
        def test_load(n):
            assert True

        @pytest.mark.implied_by("load_ok")
        def test_consumer():
            raise RuntimeError("should not run")
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.str()
    assert "IMPLIED PASS" in lines
    # test_consumer should be implied via propagated token
    assert "test_consumer" in lines
    assert "should not run" not in lines


def test_bisect_terminal_summary_counts(pytester):
    """Terminal summary shows both implied pass and implied fail counts."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", range(1, 9))
        def test_load(n):
            assert n <= 4
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.str()
    assert "implied pass" in lines
    assert "implied fail" in lines


def test_bisect_single_item_no_bisect(pytester):
    """bisect=True with a single parametrize value runs normally."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", [42])
        def test_load(n):
            assert True
    """))
    result = pytester.runpytest("-v")
    result.assert_outcomes(passed=1)
    assert "IMPLIED" not in result.stdout.str()


def test_bisect_probe_count(pytester):
    """bisect=True uses O(log n) probes, not linear."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", range(1, 17))
        def test_load(n):
            # desc: 16..1, threshold at n=8
            assert n <= 8
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.lines
    # Count actually-executed tests (verbose lines with percentage),
    # excluding IMPLIED verdicts.
    executed = sum(
        1 for line in lines
        if ("PASSED" in line or "FAILED" in line)
        and "IMPLIED" not in line
        and "test_load" in line
        and "%" in line
    )
    # 16 items; binary search should need far fewer than 16 probes
    assert executed <= 8, f"expected ≤8 probes, got {executed}"
    assert executed < 16, "bisect should not probe every item"


def test_bisect_reason_in_output(pytester):
    """Bisect implied items show the monotonic bisect reason."""
    pytester.makepyfile(textwrap.dedent("""\
        import pytest

        @pytest.mark.monotonic("n", bisect=True)
        @pytest.mark.parametrize("n", [1, 2, 3, 4])
        def test_load(n):
            assert True
    """))
    result = pytester.runpytest("-v")
    lines = result.stdout.str()
    assert "monotonic bisect" in lines
