"""Boundary controls and independent enumerations for the teaching arithmetic."""
from fractions import Fraction
from itertools import product, combinations
from pathlib import Path
import json
import unittest
from model import crossing, example, sample_probability, score


class Arithmetic(unittest.TestCase):
    def test_crossing(self):
        self.assertEqual(crossing(4, 0, 1, 9), 12)
        self.assertEqual(score(4, 0, 10), Fraction(2, 5))
        self.assertEqual(score(1, 9, 10), 1)
        self.assertEqual(score(4, 0, 12), score(1, 9, 12))
        self.assertGreater(score(4, 0, 20), score(1, 9, 20))

    def test_cross_product_oracle(self):
        for a, b in product(range(1, 9), repeat=2):
            for sa, sb in product(range(5), repeat=2):
                root = crossing(a, sa, b, sb)
                if root is not None:
                    self.assertEqual(a * (root - sb), b * (root - sa))
                    self.assertGreater(root, max(sa, sb))
                for now in range(max(sa, sb) + 1, 12):
                    self.assertEqual(score(a, sa, now) < score(b, sb, now),
                                     a * (now - sb) < b * (now - sa))

    def test_no_future_crossing(self):
        self.assertIsNone(crossing(2, 0, 2, 4))
        self.assertIsNone(crossing(2, 4, 2, 4))
        self.assertIsNone(crossing(1, 0, 4, 9))

    def test_clock_scaling(self):
        for now in (10, 12, 20):
            for n, s in ((4, 0), (1, 9)):
                self.assertEqual(score(n, s + 100, now + 100), score(n, s, now))
                self.assertEqual(score(n, 10 * s, 10 * now), score(n, s, now) / 10)

    def test_sampling_by_enumeration(self):
        for n in range(1, 6):
            for k in range(n + 1):
                for draws in range(4):
                    samples = list(product(range(n), repeat=draws))
                    observed = Fraction(sum(any(v < k for v in row) for row in samples), len(samples))
                    self.assertEqual(observed, sample_probability(n, k, draws, replacement=True))
                    if draws <= n:
                        samples = list(combinations(range(n), draws))
                        observed = Fraction(sum(any(v < k for v in row) for row in samples), len(samples))
                        self.assertEqual(observed, sample_probability(n, k, draws, replacement=False))

    def test_sampling_boundary_and_exact_result(self):
        self.assertEqual(sample_probability(100, 10, 8, replacement=True), Fraction(56953279, 100000000))
        for replacement in (True, False):
            self.assertEqual(sample_probability(5, 0, 3, replacement=replacement), 0)
            self.assertEqual(sample_probability(5, 5, 3, replacement=replacement), 1)
            self.assertEqual(sample_probability(5, 5, 0, replacement=replacement), 0)
        self.assertEqual(sample_probability(5, 1, 5, replacement=False), 1)

    def test_invalid_inputs(self):
        for args in ((0, 0, 2), (True, 0, 2), (1, -1, 2), (1, 2, 2), (1, 3, 2), (1, 0, 0.5)):
            with self.assertRaises(ValueError):
                score(*args)
        for args in ((0, 0, 1, 2), (1, -1, 2, 0), (1, 0, True, 1)):
            with self.assertRaises(ValueError):
                crossing(*args)
        for args, mode in (((0, 0, 0), True), ((2, 3, 1), True), ((2, 1, -1), True),
                           ((2, 1, 3), False), ((True, 0, 0), True), ((2, 1, 1), 1)):
            with self.assertRaises(ValueError):
                sample_probability(*args, replacement=mode)

    def test_generated_result(self):
        self.assertEqual(example(), json.loads(Path(__file__).with_name("results.json").read_text()))


if __name__ == "__main__":
    unittest.main()
