import unittest
from itertools import product
from fractions import Fraction
from example import SetCover,CoverReduction,gap_instance,compare_deletion_orders


class ReductionTests(unittest.TestCase):
    def test_paper_worked_example_all_subsets(self):
        r=CoverReduction(SetCover(3,({0,1},{1,2},{2})),2)
        self.assertEqual(r.audit(),dict(reaction_subsets_checked=512,raf_count=3,minimum_cover_size=2,minimum_raf_size=7))
        self.assertEqual(sorted(map(len,r.network.all_rafs())),[7,7,9])
        self.assertEqual(r.network.maximal_raf(),r.network.identifiers)
        self.assertTrue(all(len(x.reactants)<=2 for x in r.network.reactions.values()))
        self.assertEqual(len({x.product for x in r.network.reactions.values()}),9)
        self.assertTrue(all('f' not in x.catalysts for x in r.network.reactions.values()))

    def test_all_two_element_three_set_instances(self):
        # Includes empty sets and repeated indexed sets. The literal reaction
        # subset checker does not use the cover formula being tested.
        options=(set(),{0},{1},{0,1});count=0
        for sets in product(options,repeat=3):
            if set().union(*sets)!={0,1}:continue
            for M in (1,2):
                r=CoverReduction(SetCover(2,sets),M);r.audit();count+=1
        self.assertEqual(count,98)

    def test_closure_and_invalid_partial_blocks(self):
        r=CoverReduction(SetCover(3,({0,1},{1,2},{2})),2)
        chosen=r.canonical({0,1});stages=r.network.closure_stages(chosen)
        self.assertEqual(len(stages)-1,5)
        self.assertIn('y0',stages[1]);self.assertIn('z0:1',stages[5])
        for reaction in chosen:
            self.assertFalse(r.network.is_raf(chosen-{reaction}))
        with self.assertRaises(ValueError):r.decode(chosen-{'b0:1'})

    def test_exact_approximation_arithmetic(self):
        instance=gap_instance(4)
        for M in (1,2,4,8):
            r=CoverReduction(instance,M)
            for cover in instance.all_covers():
                result=r.approximation(cover)
                self.assertEqual(result['bound_applicable'],M>=4)
                if M>=4:self.assertTrue(result['bound_holds'])
        self.assertIsNone(CoverReduction(instance,1).approximation({1,2,3,4})['transferred_bound'])

    def test_deletion_order_returns_different_irreducible_sizes(self):
        row=compare_deletion_orders(8)
        self.assertEqual(row['small_irraf'],16);self.assertEqual(row['large_irraf'],72)
        self.assertEqual(Fraction(row['ratio']),Fraction(9,2))

    def test_reject_invalid_inputs_and_budget(self):
        for build in (lambda:SetCover(0,()),lambda:SetCover(2,({0},)),lambda:SetCover(1,({2},)),
                      lambda:CoverReduction(SetCover(1,({0},)),0)):
            with self.assertRaises(ValueError):build()
        with self.assertRaises(ValueError):CoverReduction(gap_instance(5),5).network.all_rafs()


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