import pytest
from argparse import Namespace
import entropy_from_dice as ee


# Notes:
# - "work directory" = directory that contains this file.
# - Running the command {pytest} in the work directory should load and run the tests in this file.
# -- You can also use: { pytest test_entropy_from_dice.py }
# - Run a specific test:
# -- pytest test_entropy_from_dice.py::test_f1_one
# - Run quietly:
# -- pytest -q
# - Print log data during a single test:
# -- pytest -o log_cli=true --log-cli-level=DEBUG --log-format="%(levelname)s [%(lineno)s: %(funcName)s] %(message)s" test_entropy_from_dice.py::test_f1_one
# -- This is very useful when you want to manually check the operation of the functions during the test.




# Rename functions for convenience. 
f1 = ee.nextLowestIntegerPowerOf2
f1_keys = 'value exponent'.split()
f2 = ee.base10IntToBase6Str
f3 = ee.convertDiceRollsToBits
f4 = ee.convertDiceRollsToBitsUsingLargestPossibleGroups
f5 = ee.estimateRequiredDiceRolls
f5_keys = 'rolls efficiency'.split()
f6 = ee.processDiceRolls


# Notes:
# - Tests are grouped by functions.
# - Test groups are in reverse order. So: Tests for function f1 are at the bottom.








# SECTION: Tests for processDiceRolls.
# Args: Namespace containing: diceRollsString diceGroupSizes outputType entropyAmount


def test_f6_bad_args_type():
	args = 'foo'
	with pytest.raises(TypeError):
		result = f6(args)


def test_f6_empty_args():
	args = Namespace()
	with pytest.raises(ValueError):
		result = f6(args)


def test_f6_non_permitted_chars():
	args = Namespace(
		diceRollsString = "122###^%$",
		diceGroupSizes = [2,1],
		outputType = 'bits',
		entropyAmount = 3
	)
	with pytest.raises(ValueError):
		result = f6(args)


def test_f6_bad_outputType_type():
	args = Namespace(
		diceRollsString = "122###^%$",
		diceGroupSizes = [2,1],
		outputType = 123.123,
		entropyAmount = 3
	)
	with pytest.raises(TypeError):
		result = f6(args)


def test_f6_bad_outputType_value():
	args = Namespace(
		diceRollsString = "122###^%$",
		diceGroupSizes = [2,1],
		outputType = 'foo',
		entropyAmount = 3
	)
	with pytest.raises(ValueError):
		result = f6(args)


def test_f6_122():
	args = Namespace(
		diceRollsString = "122",
		diceGroupSizes = [2,1],
		outputType = 'bits',
		entropyAmount = 3
	)
	assert f6(args) == '0000101'


def test_f6_122_to_bytes():
	args = Namespace(
		diceRollsString = "122",
		diceGroupSizes = [2,1],
		outputType = 'bytes',
		entropyAmount = 0
	)
	assert f6(args) == ''


def test_f6_1234():
	args = Namespace(
		diceRollsString = "1234",
		diceGroupSizes = [2,1],
		outputType = 'bits',
		entropyAmount = 10
	)
	assert f6(args) == '0000101111'


def test_f6_1234_to_bytes():
	args = Namespace(
		diceRollsString = "1234",
		diceGroupSizes = [2,1],
		outputType = 'bytes',
		entropyAmount = 1
	)
	assert f6(args) == '0b'


def test_f6_1111_to_bytes():
	args = Namespace(
		diceRollsString = "1111",
		diceGroupSizes = [2,1],
		outputType = 'bytes',
		entropyAmount = 1
	)
	assert f6(args) == '00'


def test_f6_6262_to_bytes():
	args = Namespace(
		diceRollsString = "6262",
		diceGroupSizes = [2,1],
		outputType = 'bytes',
		entropyAmount = 1
	)
	assert f6(args) == 'ff'


def test_f6_6262_group_1():
	args = Namespace(
		diceRollsString = "6262",
		diceGroupSizes = [1],
		outputType = 'bits',
		entropyAmount = 1
	)
	assert f6(args) == '0101'


def test_f6_group_1_large_input():
	diceRollsString = """
12224
32455
43512
63552
22164

14133
41133
33313
32336
61214

24161
64534
43126
65454
13563

25466
26462
61651
25156
45236

33631
13365
22211
42146
15162

61523
56355
65313
43624
32555

33364
26155
52361
11545
66244

15162
64235
63343
21244
12313
"""
	args = Namespace(
		diceRollsString = diceRollsString,
		diceGroupSizes = [1],
		outputType = 'bytes',
		entropyAmount = 32
	)
	assert f6(args) == '15e7e195332b0aa8a684dc3be1f29dd04daa0a5434c11a8b9e6ad180df076ae47c62'


def test_f6_group_1_large_input_large_group():
	diceRollsString = """
12224
32455
43512
63552
22164

14133
41133
33313
32336
61214

24161
64534
43126
65454
13563

25466
26462
61651
25156
45236
"""
	args = Namespace(
		diceRollsString = diceRollsString,
		diceGroupSizes = [53],
		outputType = 'bytes',
		entropyAmount = 0
	)
	result = f6(args)
	assert result == '089f7ffa2076aeb36d09a0d095296116cf'
	# 2 hex characters per byte.
	assert len(result) == 17*2


def test_f6_group_1_large_input_large_groups():
	diceRollsString = """
12224
32455
43512
63552
22164

14133
41133
33313
32336
61214

24161
64534
43126
65454
13563

25466
26462
61651
25156
45236
"""
	args = Namespace(
		diceRollsString = diceRollsString,
		diceGroupSizes = [53,12,7,2,1],
		outputType = 'bytes',
		entropyAmount = 32
	)
	result = f6(args)
	assert result == '089f7ffa2076aeb36d09a0d095296116cf6f7fe78bf9958815585d6a283351cc'
	# 2 hex characters per byte.
	assert len(result) == 32*2








# SECTION: Tests for estimateRequiredDiceRolls.
# Args: nBits, diceGroupSizes


def test_f5_string():
	with pytest.raises(TypeError):
		result = f5("foo", [2,1])


def test_f5_0_bits():
	result = f5(0, [2,1])
	rolls, efficiency = (result[k] for k in f5_keys)
	assert rolls == 0
	assert efficiency == '1.0000'


def test_f5_1_bits():
	result = f5(1, [2,1])
	rolls, efficiency = (result[k] for k in f5_keys)
	assert rolls == 2
	assert efficiency == '0.5158'


def test_f5_2_bits():
	result = f5(2, [2,1])
	rolls, efficiency = (result[k] for k in f5_keys)
	assert rolls == 2
	assert efficiency == '0.5158'


def test_f5_5_bits():
	result = f5(5, [2,1])
	rolls, efficiency = (result[k] for k in f5_keys)
	assert rolls == 3
	assert efficiency == '0.8597'


def test_f5_7_bits():
	result = f5(7, [2,1])
	rolls, efficiency = (result[k] for k in f5_keys)
	assert rolls == 4


def test_f5_18_bits():
	result = f5(18, [7,2,1])
	rolls, efficiency = (result[k] for k in f5_keys)
	assert rolls == 8
	assert efficiency == '0.9315'


def test_f5_24_bits():
	# 24 bits will result in 1 group of each of [7,2,1] i.e. 7+2+1 = 10 dice rolls.
	# efficiency is normalised by nDiceRolls in each group.
	result = f5(24, [7,2,1])
	rolls, efficiency = (result[k] for k in f5_keys)
	assert rolls == 12








# SECTION: Tests for convertDiceRollsToBitsUsingLargestPossibleGroups.
# Args: diceRollsString, groupSizeList


def test_f4_badly_ordered_sizeList():
	g = [7,1,12]
	with pytest.raises(ValueError):
		result = f4("52123", g)


def test_f4_0():
	assert f4("1", [2,1]) == '00'


def test_f4_11():
	assert f4("11", [2,1]) == '00000'


def test_f4_12():
	assert f4("12", [2,1]) == '00001'


def test_f4_121():
	assert f4("121", [2,1]) == '0000100'


# Tests for handling of values that are greater than the maxUsableValue.
# In these cases, the function should recurse and try to use smaller groupSizes to extract entropy from the value. 
# Example: Using groupSize=2 (max usable dice roll value='62'), the dice roll value '64' will not be usable (i.e. 0 bits extracted). However, re-processing it with groupSize=1 (max usable dice roll value='4') will result in '' for '6' and '11' for '4'. We get some entropy from the value rather than nothing.
# - Note: In this case, little entropy would be lost (only 2 bits) if we didn't process the non-usable value. But: This will matter rather more for groupSize=53, i.e. we would waste 53 dice roll digits if we didn't try to re-process the value.


def test_f4_recurse_64():
	assert f4("64", [2,1]) == '11'


def test_f4_recurse_66():
	# Empty result because for both groupSizes the values are above the maxUsableValue.
	assert f4("66", [2,1]) == ''


def test_f4_recurse_62():
	# This is the maxUsableValue for groupSize=2. In this case, there shouldn't be any recursion (i.e. use of groupSize=1 to re-process the value). This test checks that no recursion occurred.
	assert f4("62", [2,1]) == '11111'


# maxDiceRollValue for groupSize=7: 6452454
# Note: The processing of the first two digits '64', using groupSizes [2,1] is the same as in test_f4_recurse_64.
# Note: Without recursion, the result would be 0 bits extracted.
def test_f4_recurse_group_7():
	assert f4("6452463", [7,2,1]) == '11110011011110'


# maxDiceRollValue for groupSize=12: 664143116642
# Note: The first 7 digits 6641431 are greater than the maxDiceRollValue for groupSize=7, so their re-processing is similar to that seen in test_f4_recurse_group_7.
def test_f4_recurse_group_12():
	assert f4("664143116646", [12,7,2,1]) == '1001010100000010111'


# maxDiceRollValue for groupSize=53: 66642551333153451122363633251644331125461552315552362
# Note: The first 12 digits 666425513331 are greater than the maxDiceRollValue for groupSize=12, so their re-processing is similar to that seen in test_f4_recurse_group_12.
def test_f4_recurse_group_53():
	assert f4("66642551333153451122363633251644331125461552315552364", [53,12,7,2,1]) == '1101010000100111000101111111101000101000111110010101100110111100100111101100010000100110010110110011100001011100110011000111'








# SECTION: Tests for convertDiceRollsToBits.
# Args: convertDiceRollsToBits(diceRollsString, groupSize)


def test_f3_int_dice():
	with pytest.raises(TypeError):
		result = f3(52, 1)


def test_f3_empty_dice():
	assert f3('', 1) == ''


def test_f3_string_group():
	with pytest.raises(TypeError):
		result = f3("4521236", 'foo')


def test_f3_negative_group():
	with pytest.raises(ValueError):
		result = f3("4521236", -1)


def test_f3_zero_group():
	with pytest.raises(ValueError):
		result = f3("4521236", 0)


def test_f3_group_larger_than_number_of_dice_rolls():
	with pytest.raises(ValueError):
		result = f3("1111", 5)

def test_f3_number_of_rolls_not_exactly_divisible_by_group():
	with pytest.raises(ValueError):
		result = f3("1111", 3)


def test_f3_one():
	assert f3("1", 1) == '00'


def test_f3_two():
	assert f3("2", 1) == '01'


def test_f3_three():
	assert f3("3", 1) == '10'


def test_f3_four():
	# maximum permitted value if groupSize==1.
	assert f3("4", 1) == '11'


def test_f3_five():
	assert f3("5", 1) == ''


def test_f3_six():
	assert f3("6", 1) == ''

def test_f3_11():
	assert f3("11", 1) == '0000'


def test_f3_14():
	assert f3("14", 1) == '0011'


def test_f3_42():
	assert f3("42", 1) == '1101'


def test_f3_group_2_maximum_permitted_value():
	assert f3("62", 2) == '11111'


def test_f3_group_2_greater_than_maximum_permitted_value():
	assert f3("63", 2) == ''


def test_f3_5426():
	assert f3("5426", 1) == '1101'


def test_f3_11_group_2():
	assert f3("11", 2) == '00000'


def test_f3_14_group_2():
	assert f3("14", 2) == '00011'


def test_f3_42_group_2():
	assert f3("42", 2) == '10011'


def test_f3_group_5():
	assert f3("31234", 5) == '101001010011'


def test_f3_group_5_greater_than_maximum_permitted_value():
	assert f3("51234", 5) == ''


def test_f3_group_12():
	input = '5'*12 + '4'*12 + '3'*12 + '2'*12 +'1'*12
	assert f3(input, 12) == '11001111100110000001100110011001001101110110010000100110011001011001111100110000001100110011000110011111001100000011001100110000000000000000000000000000000'


def test_f3_group_53_minimum_permitted_value():
	assert f3("1"*53, 53) == '0'*137


def test_f3_group_53_maximum_permitted_value():
	expected_output = '1'*137
	v = int(expected_output, 2)
	v = ee.base10IntToBase6Str(v)
	v = ee.base6StrToDiceRollsStr(v)
	assert f3(v, 53) == expected_output


def test_f3_group_53_greater_than_maximum_permitted_value():
	maxPossibleOutput = '1'*137
	v = int(maxPossibleOutput, 2) + 1 # note the increment.
	v = ee.base10IntToBase6Str(v)
	v = ee.base6StrToDiceRollsStr(v)
	assert f3(v, 53) == ''








# SECTION: Tests for base10IntToBase6Str.


def test_f2_string():
	with pytest.raises(TypeError):
		result = f2('foo')


def test_f2_negative():
	with pytest.raises(ValueError):
		result = f2(-1)


def test_f2_0():
	assert f2(0) == '0'


def test_f2_1():
	assert f2(1) == '1'


def test_f2_5():
	assert f2(5) == '5'


def test_f2_6():
	assert f2(6) == '10'


def test_f2_7():
	assert f2(7) == '11'


def test_f2_11():
	assert f2(11) == '15'


def test_f2_12():
	assert f2(12) == '20'


def test_f2_137():
	assert f2(137) == '345'








# SECTION: Tests for nextLowestIntegerPowerOf2.

	
def test_f1_string():
	with pytest.raises(TypeError):
		result = f1("foo")


def test_f1_float():
	with pytest.raises(TypeError):
		result = f1(1.5)


def test_f1_negative():
	with pytest.raises(ValueError):
		result = f1(-1)


def test_f1_one(caplog):
	result = f1(1)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 1
	assert exponent == 0


def test_f1_two():
	result = f1(2)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 2
	assert exponent == 1


def test_f1_three():
	result = f1(3)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 2
	assert exponent == 1


def test_f1_four():
	result = f1(4)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 4
	assert exponent == 2


def test_f1_five():
	result = f1(4)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 4
	assert exponent == 2


def test_f1_seven():
	result = f1(7)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 4
	assert exponent == 2


def test_f1_eight():
	result = f1(8)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 8
	assert exponent == 3


def test_f1_nine():
	result = f1(8)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 8
	assert exponent == 3


def test_f1_sixteen():
	result = f1(16)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 16
	assert exponent == 4


def test_f1_seventeen():
	result = f1(17)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 16
	assert exponent == 4


def test_f1_thirty_one():
	result = f1(31)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 16
	assert exponent == 4


def test_f1_thirty_three():
	result = f1(33)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 32
	assert exponent == 5


def test_f1_one_hundred():
	result = f1(100)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 64
	assert exponent == 6


def test_f1_two_hundred():
	result = f1(200)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 128
	assert exponent == 7


def test_f1_two_thousand():
	result = f1(2000)
	value, exponent = (result[k] for k in f1_keys)
	assert value == 1024
	assert exponent == 10