import string

import pytest
from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st
from papass import PasswordGenerator

from tests.utils.cycle_rng import CycleRng


@pytest.fixture
def alphabet():
    alpha = "abcdefgh"
    assert len(alpha) == 2**3
    return alpha


@st.composite
def st_ordered_pair(draw, min: int, max: int) -> tuple[int, int]:
    """Strategy returning (i, j) with min <= i < j <= max."""
    j = draw(st.integers(min + 1, max))
    i = draw(st.integers(min, j - 1))
    return (i, j)


@given(ij=st_ordered_pair(0, 26**11 - 1))
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_randbelow_to_password_1(ij):
    """Makes sure that the mapping preserves strict order and hence is bijective.

    This ensures that the passwords adhere to a *uniform* probability distribution if the
    numbers generated by randbelow are uniform already. This is important for security.

    More precisely the test checks (indirectly) that the integers returned from randbelow
    are mapped in the following way to passwords:

    0          -> aaaaaaaaaaa
    1          -> aaaaaaaaaab
    2          -> aaaaaaaaaac
    ...
    26**11 - 1 -> zzzzzzzzzzz

    NOTE: If the test succeeds for all values of (i, j) the above mapping is the only way
    how this can happen.
    """
    length = 11
    alphabet = string.ascii_lowercase

    # Compare with @given:
    assert length == 11, "Assumption of test violated"
    assert len(alphabet) == 26, "Assumption of test violated"

    # Each call to generate calls randbelow exactly once:
    i, j = ij
    pwg = PasswordGenerator(alphabet=alphabet, rng=CycleRng([i, j]))
    password_1 = pwg.generate(length).password
    password_2 = pwg.generate(length).password

    assert i < j, "This shouldn't fail"
    assert password_1 < password_2


@pytest.mark.parametrize(
    "cycle, password",
    [
        # NOTE: choice and hence randbelow should only be called once.
        ([0], "aaaa"),
        ([1], "aaab"),
        ([2], "aaac"),
        # Checks that the second value in cycle is irrelevant:
        ([0, 1], "aaaa"),
        # Each coefficient corresponds to a letter in order:
        ([4 * 8**3 + 0 * 8**2 + 2 * 8 + 7], "each"),
        ([2 * 8**3 + 0 * 8**2 + 5 * 8 + 4], "cafe"),
        ([8**4 - 1], "hhhh"),
    ],
)
def test_randbelow_to_password_2(alphabet, cycle, password):
    """Similar purpose as the other test of the same name but from a different angle."""
    pwg = PasswordGenerator(alphabet=alphabet, rng=CycleRng(cycle))
    result = pwg.generate(4)

    assert result.password == password
    assert result.entropy == pytest.approx(12.0)


@pytest.mark.parametrize("length", range(4))
def test_entropy(alphabet, length):
    pwg = PasswordGenerator(alphabet=alphabet, rng=CycleRng([42]))

    # alphabet has 8 characters, so 3 bits of entropy per character:
    assert pwg.generate(length).entropy == pytest.approx(3 * length)


@pytest.mark.parametrize("alphabet", ["ab", "a", "aa", "*3#a=+"])
def test_valid_alphabets(alphabet):
    """Should not raise an error."""
    pwg = PasswordGenerator(alphabet=alphabet, rng=CycleRng([42]))
    pwg.generate(5)


@pytest.mark.parametrize("alphabet", ["", [], ["a", "b", "bb"]])
def test_invalid_alphabets(alphabet):
    with pytest.raises(AssertionError):
        PasswordGenerator(alphabet=alphabet, rng=CycleRng(range(4)))
