# Multiparadigm Programming with Python 3
# Rational Number; Black-box unit test with Pytest for RatRep
# Copyright (C) 2018, H. Conrad Cunningham
#
# 34567890123456789012345678901234567890123456789012345678901234567890
#
# 2018-11-01: Created from RatCore test script without fixture
#             Added fixture rat, more type hints for mypy
# 2018-11-02: Corrected test_divRat0 (name, fixture parameter)

from typing import List, Tuple
import pytest

from rational_arith import Rat
from rational_rep import RatRep
from rational_core import RatCore
from rational_defer import RatDefer


# Session-scope fixture to step thru RatRep implementations
@pytest.fixture(scope="session", params=[Rat(RatCore), Rat(RatDefer)])
def rat(request) -> Rat:
    """Generate Rat objects with the different RatRep objects"""
    return request.param


# Test illegal 0 denominator
@pytest.mark.xfail()
def test_denom0(rat: Rat) -> None:
    """Test zero denominator for makeRat; raises exception"""
    rr: RatRep = rat.makeRat(1, 0)
    assert rr.numer() == 1
    assert rr.denom() == 0


# Test 0/1, 0/-1, 0/9, 0/-9; all become 0/1
y_axis_pairs: List[Tuple[int, int]] = [
    (0, 1),
    (0, -1),
    (0, 9),
    (0, -9)
]
y_axis_ids: List[str] = [f"{t}" for t in y_axis_pairs]


@pytest.mark.parametrize("x,y", y_axis_pairs, ids=y_axis_ids)
def test_y_axis(rat: Rat, x: int, y: int) -> None:
    """Test RatRep non-error inputs on y-axis boundary"""
    rr: RatRep = rat.makeRat(x, y)
    assert rr.numer() == 0
    assert rr.denom() == 1


# Test 1/1, -1/-1,  9/9, -9/-9
quad_1_3_pairs: List[Tuple[int, int]] = [
    (1, 1),
    (-1, -1),
    (9, 9),
    (-9, -9)
]
quad_1_3_ids: List[str] = [f"{t}" for t in quad_1_3_pairs]


@pytest.mark.parametrize("x,y", quad_1_3_pairs, ids=quad_1_3_ids)
def test_quad_1_3(rat: Rat, x: int, y: int) -> None:
    """Test RatRep 1st & 3rd quadrants, same integer output"""
    rr: RatRep = rat.makeRat(x, y)
    assert rr.numer() == 1
    assert rr.denom() == 1


# Test -1/1, -9/9, 1/-1, 9/-9
quad_2_4_pairs: List[Tuple[int, int]] = [
    (-1, 1),
    (1, -1),
    (-9, 9),
    (9, -9)
]
quad_2_4_ids: List[str] = [f"{t}" for t in quad_2_4_pairs]


@pytest.mark.parametrize("x,y", quad_2_4_pairs, ids=quad_2_4_ids)
def test_quad_2_4(rat: Rat, x: int, y: int) -> None:
    """Test RatRep 2nd & 4th quadrants, same integer output"""
    rr = rat.makeRat(x, y)
    assert rr.numer() == -1
    assert rr.denom() == 1


# Test 3/2, -3/-2, 12/8, -12/-8; all become 3/2
quad_1_3_pairs_nonint: List[Tuple[int, int]] = [
    (3, 2),
    (-3, -2),
    (12, 8),
    (-12, -8)
]
quad_1_3_ids_nonint: List[str] = [
    f"{t}" for t in quad_1_3_pairs_nonint
]


@pytest.mark.parametrize(
    "x,y", quad_1_3_pairs_nonint, ids=quad_1_3_ids_nonint
)
def test_quad_1_3_notint(rat: Rat, x: int, y: int) -> None:
    """Test RatRep 1st and 3rd quadrants, non-integer"""
    rr: RatRep = rat.makeRat(x, y)
    assert rr.numer() == 3
    assert rr.denom() == 2


# Test -3/2, 3/-2, -12/8, 12/-8; all becomes -3/2
quad_2_4_pairs_nonint: List[Tuple[int, int]] = [
    (-3, 2),
    (3, -2),
    (-12, 8),
    (12, -8)
]
quad_2_4_ids_nonint: List[str] = [
    f"{t}" for t in quad_2_4_pairs_nonint
]


@pytest.mark.parametrize(
    "x,y", quad_2_4_pairs_nonint, ids=quad_2_4_ids_nonint
)
def test_quad_2_4_notint(rat: Rat, x: int, y: int) -> None:
    """Test RatRep 2nd and 4th quadrants, non-integer"""
    rr: RatRep = rat.makeRat(x, y)
    assert rr.numer() == -3
    assert rr.denom() == 2


# Named constants used in testing of Rat
zero: Tuple[int, int] = (0, 1)
one: Tuple[int, int] = (1, 1)
negone: Tuple[int, int] = (-1, 1)
two: Tuple[int, int] = (2, 1)
negtwo: Tuple[int, int] = (-2, 1)
half: Tuple[int, int] = (1, 2)
quart1: Tuple[int, int] = (1, 4)
quart3: Tuple[int, int] = (3, 4)
r_32_27: Tuple[int, int] = (32, 27)
nr_32_27: Tuple[int, int] = (-32, 27)


# Test eqRat
# Boundary values: zero, one, negone
# Representative values: quart1, quart3
# Equality properties: refexivity, symmetry, transitivity
# Interface invariant

eqRat_tests: List[Tuple[Tuple[int, int], Tuple[int, int], bool]] = [
    (zero, zero, True),
    (zero, one, False),
    (one, zero, False),
    (negone, negone, True),
    (one, negone, False),
    (negone, one, False),
    (quart1, quart3, False),
    (quart3, quart1, False)
]
eqRat_ids = [f"{t}" for t in eqRat_tests]


@pytest.mark.parametrize("l,r,b", eqRat_tests, ids=eqRat_ids)
def test_eqRat(rat, l, r, b) -> None:
    """Test Rat eqRat"""
    rl: RatRep = rat.makeRat(l[0], l[1])
    rr: RatRep = rat.makeRat(r[0], r[1])
    assert rat.eqRat(rl, rr) == b


# Test negRat
# Boundary values: zero, one, negone
# Representative values: r_32_27, nr_32_27
# Properties: idempotence (double negation)
# Interface invariant

negRat_tests: List[Tuple[Tuple[int, int], Tuple[int, int]]] = [
    (zero, zero),
    (one, negone),
    (negone, one),
    (two, negtwo),
    (negtwo, two),
    (r_32_27, nr_32_27),
    (nr_32_27, r_32_27)
]
negRat_ids = [f"{t}" for t in negRat_tests]


@pytest.mark.parametrize("l, r", negRat_tests, ids=negRat_ids)
def test_negRat(rat, l, r) -> None:
    """Test Rat negRat"""
    rl: RatRep = rat.makeRat(l[0], l[1])
    rr: RatRep = rat.makeRat(r[0], r[1])
    assert rat.eqRat(rat.negRat(rl), rr) is True


# Test addRat
# Boundary values: zero, one, negone
# Representative values: quart1, half, quart3, two
# Properties: associativity, symmetry/commutativity,
#             identity, inverse

addRat_tests: List[
    Tuple[Tuple[int, int], Tuple[int, int], Tuple[int, int]]] = [
    (zero, zero, zero),
    (zero, one, one),
    (one, zero, one),
    (one, one, two),
    (one, negone, zero),
    (negone, one, zero),
    (quart1, quart1, half),
    (half, quart1, quart3),
    (quart1, half, quart3)
]
addRat_ids = [f"{t}" for t in addRat_tests]


@pytest.mark.parametrize("l, r, a", addRat_tests, ids=addRat_ids)
def test_addRat(rat, l, r, a) -> None:
    """Test Rat addRat"""
    rl: RatRep = rat.makeRat(l[0], l[1])
    rr: RatRep = rat.makeRat(r[0], r[1])
    ra: RatRep = rat.makeRat(a[0], a[1])
    assert rat.eqRat(rat.addRat(rl, rr), ra) is True


# Test subRat
# Boundary values: zero, one, negone
# Representative values: quart1, half, quart3, two
# Properties: right identity, right inverse
# Interface invariant

subRat_tests: List[
    Tuple[Tuple[int, int], Tuple[int, int], Tuple[int, int]]] = [
    (one, zero, one),
    (one, one, zero),
    (one, negone, two),
    (one, two, negone),
    (quart3, half, quart1),
    (quart3, quart1, half)
]
subRat_ids = [f"{t}" for t in subRat_tests]


@pytest.mark.parametrize("l,r,a", subRat_tests, ids=subRat_ids)
def test_subRat(rat, l, r, a) -> None:
    """Test Rat subRat"""
    rl: RatRep = rat.makeRat(l[0], l[1])
    rr: RatRep = rat.makeRat(r[0], r[1])
    ra: RatRep = rat.makeRat(a[0], a[1])
    assert rat.eqRat(rat.subRat(rl, rr), ra) is True


# Test mulRat
# Boundary values: zero, one, negone
# Representative values: quart1, half, quart3, two
# Properties: associativity, symmetry/commutativity,
#             identity, inverse, zero element
# Interface invariant

mulRat_tests: List[
    Tuple[Tuple[int, int], Tuple[int, int], Tuple[int, int]]] = [
    (zero, zero, zero),
    (zero, one, zero),
    (one, zero, zero),
    (one, one, one),
    (one, negone, negone),
    (negone, one, negone),
    (one, two, two),
    (two, one, two),
    (half, two, one),
    (two, half, one),
    (two, quart1, half),
    (quart1, two, half)
]
mulRat_ids = [f"{t}" for t in mulRat_tests]


@pytest.mark.parametrize("l, r, a", mulRat_tests, ids=mulRat_ids)
def test_mulRat(rat, l, r, a) -> None:
    """Test Rat mulRat"""
    rl: RatRep = rat.makeRat(l[0], l[1])
    rr: RatRep = rat.makeRat(r[0], r[1])
    ra: RatRep = rat.makeRat(a[0], a[1])
    assert rat.eqRat(rat.mulRat(rl, rr), ra) is True


# Test divRat
# Division by zero is an error!
# Boundary values: zero, one, negone
# Representative values: half, two, negtwo
# Properties: left zero, right identity, inverse
# Interface invariant

divRat_tests: List[
    Tuple[Tuple[int, int], Tuple[int, int], Tuple[int, int]]] = [
    (zero, one, zero),
    (one, one, one),
    (one, negone, negone),
    (negone, one, negone),
    (one, two, half),
    (one, half, two),
    (half, half, one)
]
divRat_ids = [f"{t}" for t in divRat_tests]


@pytest.mark.parametrize("l,r,a", divRat_tests, ids=divRat_ids)
def test_divRat(rat, l, r, a) -> None:
    """Test Rat divRat"""
    rl: RatRep = rat.makeRat(l[0], l[1])
    rr: RatRep = rat.makeRat(r[0], r[1])
    ra: RatRep = rat.makeRat(a[0], a[1])
    assert rat.eqRat(rat.divRat(rl, rr), ra) is True


# Test division by zero
@pytest.mark.xfail()
def test_div0(rat: Rat) -> None:
    """Test divison by zero; raises exception"""
    rl: RatRep = rat.makeRat(1, 1)
    rr: RatRep = rat.makeRat(0, 1)
    assert rat.divRat(rl, rr).denom() == 0
