# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

import sys
from collections import Counter

import pytest

from hypothesis import given, strategies as st
from hypothesis.internal.conjecture.shrinking import (
    Bytes,
    Collection,
    Float,
    Integer,
    Ordering,
    String,
)
from hypothesis.internal.conjecture.shrinking.floats import (
    _float_to_position,
    _position_to_float,
)
from hypothesis.internal.floats import MAX_PRECISE_INTEGER, next_down
from hypothesis.internal.intervalsets import IntervalSet


def test_shrink_to_zero():
    assert Integer.shrink(2**16, lambda n: True) == 0


def test_shrink_to_smallest():
    assert Integer.shrink(2**16, lambda n: n > 10) == 11


def test_can_sort_bytes_by_reordering():
    start = bytes([5, 4, 3, 2, 1, 0])
    finish = Ordering.shrink(start, lambda x: set(x) == set(start))
    assert bytes(finish) == bytes([0, 1, 2, 3, 4, 5])


def test_can_sort_bytes_by_reordering_partially():
    start = bytes([5, 4, 3, 2, 1, 0])
    finish = Ordering.shrink(start, lambda x: set(x) == set(start) and x[0] > x[-1])
    assert bytes(finish) == bytes([1, 2, 3, 4, 5, 0])


def test_can_sort_bytes_by_reordering_partially2():
    start = bytes([5, 4, 3, 2, 1, 0])
    finish = Ordering.shrink(
        start,
        lambda x: Counter(x) == Counter(start) and x[0] > x[2],
        full=True,
    )
    assert bytes(finish) == bytes([1, 2, 0, 3, 4, 5])


def test_can_sort_bytes_by_reordering_partially_not_cross_stationary_element():
    start = bytes([5, 3, 0, 2, 1, 4])
    finish = Ordering.shrink(start, lambda x: set(x) == set(start) and x[3] == 2)
    assert bytes(finish) == bytes([0, 1, 3, 2, 4, 5])


@pytest.mark.parametrize(
    "initial, predicate, intervals, expected",
    [
        ("f" * 10, lambda s: True, IntervalSet.from_string("abcdefg"), ""),
        ("f" * 10, lambda s: len(s) >= 3, IntervalSet.from_string("abcdefg"), "aaa"),
        (
            "f" * 10,
            lambda s: len(s) >= 3 and "a" not in s,
            IntervalSet.from_string("abcdefg"),
            "bbb",
        ),
    ],
)
def test_shrink_strings(initial, predicate, intervals, expected):
    assert String.shrink(
        initial, predicate, intervals=intervals, min_size=len(expected)
    ) == tuple(expected)


@pytest.mark.parametrize(
    "initial, predicate, expected",
    [
        (b"\x18\x12", lambda v: len(v) == 2, b"\x00\x00"),
        (b"\x18\x12", lambda v: True, b""),
        (b"\x01\x10", lambda v: len(v) > 0 and v[0] == 1, b"\x01"),
        (b"\x01\x10\x01\x92", lambda v: sum(v) >= 9, b"\x09"),
    ],
)
def test_shrink_bytes(initial, predicate, expected):
    assert bytes(Bytes.shrink(initial, predicate, min_size=len(expected))) == expected


def test_collection_left_is_better():
    shrinker = Collection(
        [1, 2, 3], lambda v: True, ElementShrinker=Integer, min_size=3
    )
    assert not shrinker.left_is_better([1, 2, 3], [1, 2, 3])


def test_collection_deletes_adaptively():
    # A long run of deletable elements should be removed in O(log(n)) calls
    # rather than O(n), thanks to adaptive chunk deletion.
    n = 1000
    calls = 0

    def predicate(value):
        nonlocal calls
        calls += 1
        return sum(value) >= 3

    result = Collection.shrink(
        tuple([1] * n), predicate, ElementShrinker=Integer, min_size=0, full=True
    )
    assert result == (1, 1, 1)
    assert calls < 100


_MAX_FINITE_POSITION = _float_to_position(sys.float_info.max)


@given(st.integers(min_value=0, max_value=_MAX_FINITE_POSITION))
def test_position_to_float_round_trips(n):
    # _position_to_float always lands on an integer-valued non-negative float,
    # so the inverse is exact for every position in the finite-float range.
    assert _float_to_position(_position_to_float(n)) == n


@given(st.floats(min_value=0, allow_nan=False, allow_infinity=False))
def test_float_to_position_round_trips(f):
    # Round-trip lands on float(int(f)): exact for integer-valued f, and a
    # deliberate truncation for fractional f below the boundary.
    assert _position_to_float(_float_to_position(f)) == int(f)


@given(
    st.floats(min_value=0, allow_nan=False, allow_infinity=False),
    st.floats(min_value=0, allow_nan=False, allow_infinity=False),
)
def test_float_to_position_is_monotonic(f, g):
    lo, hi = sorted([f, g])
    assert _float_to_position(lo) <= _float_to_position(hi)


@given(
    st.floats(
        min_value=float(MAX_PRECISE_INTEGER),
        exclude_min=True,
        allow_infinity=False,
        allow_nan=False,
    )
)
def test_decrement_position_steps_to_next_down(f):
    # The defining property: stepping the position by 1 corresponds to stepping
    # to the next representable float, so Integer.shrink's n - 1 makes real
    # progress past the boundary instead of rounding back to n.
    assert _position_to_float(_float_to_position(f) - 1) == next_down(f)


@pytest.mark.parametrize(
    "target",
    [
        2.0**54,
        2.0**60,
        1.5 * 2**58,  # not a power of two: the float grid is coarser than 1 here
        2.0**53 + 2,  # just above the precise-integer boundary
        1e300,
    ],
)
def test_shrink_floats_above_max_precise_integer(target):
    # Floats larger than MAX_PRECISE_INTEGER have a gap between adjacent values
    # of more than 1, so shrinking must step on the float grid rather than the
    # integers. We should still reach the minimal satisfying value.
    assert target > MAX_PRECISE_INTEGER
    assert Float.shrink(target * 4, lambda f: f >= target) == target
