# Copyright (C) 2015 - 2019 Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; If not, see <http://www.gnu.org/licenses/>.
#
# Red Hat Author(s): Anne Mulhern <amulhern@redhat.com>
# Other Author(s): Anne Mulhern <mulhern@cs.wisc.edu>

""" Test for rational conversions. """

from __future__ import absolute_import

from fractions import Fraction
from os import environ
from os import sys
import unittest

from hypothesis import given
from hypothesis import settings
from hypothesis import strategies

from justbases import BasesError
from justbases import Radices
from justbases import Rationals
from justbases import RoundingMethods

if sys.gettrace() is not None or environ.get('TRAVIS') is not None:
    settings.load_profile("tracing")

class RationalsTestCase(unittest.TestCase):
    """ Tests for rationals. """

    @given(
       strategies.fractions().map(lambda x: x.limit_denominator(100)),
       strategies.integers(min_value=2)
    )
    @settings(max_examples=50)
    def testInverses(self, value, to_base):
        """
        Test that functions are inverses of each other.
        """
        (result, relation) = Radices.from_rational(value, to_base)
        assert result.sign in (0, 1) or value < 0
        assert relation == 0
        assert result.as_rational() == value

    @given(
       strategies.fractions().map(lambda x: x.limit_denominator(100)),
       strategies.integers(min_value=2, max_value=64),
       strategies.integers(min_value=0, max_value=64),
       strategies.sampled_from(RoundingMethods.METHODS())
    )
    @settings(max_examples=500)
    def testRoundingConversion(self, value, base, precision, method):
        """
        Test that converting and then rounding is the same as converting
        with rounding.
        """
        (rounded, rel) = \
           Radices.from_rational(value, base, precision, method)
        (unrounded, urel) = Radices.from_rational(value, base)

        assert urel == 0

        (frounded, frel) = unrounded.rounded(precision, method)

        assert frounded == rounded
        assert rel == frel

        rounded_value = rounded.as_rational()

        if rounded_value > value:
            assert rel == 1
        elif rounded_value < value:
            assert rel == -1
        else:
            assert rel == 0

    def testExceptions(self):
        """
        Test exceptions.
        """
        with self.assertRaises(BasesError):
            Radices.from_rational(Fraction(1, 2), 0)
        with self.assertRaises(BasesError):
            Radices.from_rational(Fraction(1, 2), 2, -1)

    @given(
       strategies.fractions(),
       strategies.sampled_from(RoundingMethods.METHODS())
    )
    @settings(max_examples=50)
    def testRounding(self, value, method):
        """
        Test rounding to int.
        """
        (result, _) = Rationals.round_to_int(value, method)
        self.assertIsInstance(result, int)

        (lower, upper) = (result - 1, result + 1)
        self.assertTrue(
           (lower <= value <= result) or (result <= value <= upper)
        )

    @given(strategies.integers(min_value=1, max_value=9))
    @settings(max_examples=20)
    def testRoundingPrecise(self, numerator):
        """
        Test with predicted value.
        """
        # pylint: disable=too-many-statements
        value = Fraction(numerator, 10)
        (result, rel) = \
           Rationals.round_to_int(value, RoundingMethods.ROUND_DOWN)
        self.assertEqual(result, 0)
        self.assertEqual(rel, -1)

        (result, rel) = \
           Rationals.round_to_int(-value, RoundingMethods.ROUND_DOWN)
        self.assertEqual(result, -1)
        self.assertEqual(rel, -1)

        (result, rel) = \
           Rationals.round_to_int(value, RoundingMethods.ROUND_UP)
        self.assertEqual(result, 1)
        self.assertEqual(rel, 1)

        (result, rel) = \
           Rationals.round_to_int(-value, RoundingMethods.ROUND_UP)
        self.assertEqual(result, 0)
        self.assertEqual(rel, 1)

        (result, rel) = \
           Rationals.round_to_int(value, RoundingMethods.ROUND_TO_ZERO)
        self.assertEqual(result, 0)
        self.assertEqual(rel, -1)

        (result, rel) = \
           Rationals.round_to_int(-value, RoundingMethods.ROUND_TO_ZERO)
        self.assertEqual(result, 0)
        self.assertEqual(rel, 1)

        (result, rel) = \
           Rationals.round_to_int(value, RoundingMethods.ROUND_HALF_UP)
        if numerator < 5:
            self.assertEqual(result, 0)
            self.assertEqual(rel, -1)
        else:
            self.assertEqual(result, 1)
            self.assertEqual(rel, 1)

        (result, rel) = \
           Rationals.round_to_int(-value, RoundingMethods.ROUND_HALF_UP)
        if numerator <= 5:
            self.assertEqual(result, 0)
            self.assertEqual(rel, 1)
        else:
            self.assertEqual(result, -1)
            self.assertEqual(rel, -1)

        (result, rel) = \
           Rationals.round_to_int(value, RoundingMethods.ROUND_HALF_DOWN)
        if numerator > 5:
            self.assertEqual(result, 1)
            self.assertEqual(rel, 1)
        else:
            self.assertEqual(result, 0)
            self.assertEqual(rel, -1)

        (result, rel) = \
           Rationals.round_to_int(-value, RoundingMethods.ROUND_HALF_DOWN)
        if numerator >= 5:
            self.assertEqual(result, -1)
            self.assertEqual(rel, -1)
        else:
            self.assertEqual(result, 0)
            self.assertEqual(rel, 1)

        (result, rel) = \
           Rationals.round_to_int(value, RoundingMethods.ROUND_HALF_ZERO)
        if numerator > 5:
            self.assertEqual(result, 1)
            self.assertEqual(rel, 1)
        else:
            self.assertEqual(result, 0)
            self.assertEqual(rel, -1)

        (result, rel) = \
           Rationals.round_to_int(-value, RoundingMethods.ROUND_HALF_ZERO)
        if numerator > 5:
            self.assertEqual(result, -1)
            self.assertEqual(rel, -1)
        else:
            self.assertEqual(result, 0)
            self.assertEqual(rel, 1)


    def testRoundingExceptions(self):
        """
        Test exceptions.
        """
        # pylint: disable=pointless-statement
        with self.assertRaises(BasesError):
            Rationals.round_to_int(Fraction(1, 2), None)
