"""Independent exhaustive oracle for small systems; never used by enumeration."""
import unittest
from itertools import combinations
from fractions import Fraction
from random import Random
from example import (Reaction,ReactionSystem,SupplierOptions,SupplierEnumerator,
                     coupled_gates,independent_modules,deterministic_chain,
                     minimal_cuts,greedy_cut,exact_availability,zero_excess_reliability,
                     DELETION_COSTS)


def oracle_raf(system,chosen):
    # Deliberately separate closure implementation, with in-place asynchronous
    # growth instead of the production synchronous stages.
    known=set(system.food);changed=True
    while changed:
        changed=False
        for r in chosen:
            reaction=system.reactions[r]
            if all(x in known for x in reaction.reactants):
                for x in reaction.products:
                    if x not in known: known.add(x);changed=True
    return bool(chosen) and all(all(x in known for x in system.reactions[r].reactants)
                               and any(x in known for x in system.reactions[r].catalysts) for r in chosen)


def oracle_catalogue(system):
    minimal=set()
    for size in range(1,len(system.identifiers)+1):
        for ids in combinations(sorted(system.identifiers),size):
            candidate=frozenset(ids)
            if not any(c<candidate for c in minimal) and oracle_raf(system,candidate): minimal.add(candidate)
    return minimal


class CatalogueTests(unittest.TestCase):
    def test_worked_catalogue_queries_costs_and_availability(self):
        system=coupled_gates(2);cat=SupplierEnumerator().catalogue(system)
        expected={frozenset(c) for c in [(0,2,4,5),(0,3,4,5),(1,2,4,5),(1,3,4,5)]}
        self.assertEqual(set(cat.members),expected)
        self.assertEqual(cat.beta,2);self.assertEqual(cat.global_resolution_count,4)
        self.assertEqual(cat.frequencies(),{0:2,1:2,2:2,3:2,4:4,5:4})
        self.assertEqual(cat.filter([0],[2]),(frozenset((0,3,4,5)),))
        cuts=minimal_cuts(system,cat)
        self.assertEqual(set(cuts),{frozenset((4,)),frozenset((5,)),frozenset((0,1)),frozenset((2,3))})
        self.assertEqual(min(sum(DELETION_COSTS[r] for r in c) for c in cuts),5)
        self.assertEqual(greedy_cut(cat,DELETION_COSTS),([3,0,1],Fraction(6)))
        self.assertEqual(exact_availability(system,cat),Fraction(9,64))

    def test_all_restrictions(self):
        system=coupled_gates(2);engine=SupplierEnumerator();cat=engine.catalogue(system)
        for size in range(7):
            for ids in combinations(range(6),size):
                restricted=system.restrict(ids);result=engine.catalogue(restricted)
                self.assertEqual(set(result.members),{c for c in cat.members if c<=set(ids)})
                self.assertEqual(set(result.members),oracle_catalogue(restricted))
                self.assertLessEqual(result.beta,cat.beta)

    def test_small_random_systems_against_independent_oracle(self):
        rng=Random(14092026);tested=0
        for _ in range(100):
            species=['f','x','y','z'];reactions=[]
            for r in range(5):
                fields=[frozenset(s for s in species if rng.random()<p) for p in (.3,.35,.3)]
                reactions.append(Reaction(r,*fields))
            system=ReactionSystem({'f'},reactions)
            if SupplierOptions.from_system(system).count>256: continue
            expected=oracle_catalogue(system);engine=SupplierEnumerator(256)
            self.assertEqual(set(engine.global_catalogue(system).members),expected)
            self.assertEqual(set(engine.catalogue(system).members),expected)
            tested+=1
        self.assertGreaterEqual(tested,80)

    def test_validation_and_startup_counterexamples(self):
        # Alternative catalyst creates a nonminimal sink candidate.
        system=ReactionSystem({'f'},[Reaction(0,{'f'},{'x'},{'x','y'}),Reaction(1,{'x'},{'y'},{'x'})])
        cat=SupplierEnumerator().global_catalogue(system)
        self.assertEqual(cat.members,(frozenset((0,)),));self.assertEqual(cat.rejected,1)
        # A self-loop cannot manufacture its own reactant from food.
        system=ReactionSystem({'f'},[Reaction(0,{'f'},{'x'},{'x'}),Reaction(1,{'x'},{'x'},{'x'})])
        self.assertEqual(SupplierEnumerator().catalogue(system).members,(frozenset((0,)),))
        self.assertFalse(system.is_raf({1}))
        # Immediate deletion fails, but a smaller RAF remains after pruning.
        system=ReactionSystem({'f'},[Reaction(0,{'f'},{'x'},{'f'}),Reaction(1,{'x'},{'y'},{'z'}),Reaction(2,{'x'},{'z'},{'y'})])
        self.assertTrue(system.is_raf({0,1,2}))
        self.assertTrue(all(not system.is_raf(ids) for ids in ({0,1},{0,2},{1,2})))
        self.assertEqual(system.maximal_raf({0,1}),frozenset((0,)))

    def test_components_depth_and_zero_excess(self):
        engine=SupplierEnumerator()
        modules=independent_modules(6)
        global_=engine.global_catalogue(modules);local=engine.catalogue(modules)
        self.assertEqual(global_.members,local.members)
        self.assertEqual(global_.resolutions_inspected,64);self.assertEqual(local.resolutions_inspected,12)
        chain=deterministic_chain(64);cat=engine.global_catalogue(chain)
        self.assertEqual(cat.beta,0);self.assertEqual(cat.members,(chain.identifiers,))
        self.assertEqual(len(chain.closure_stages())-1,64)
        pairs=independent_modules(4,choices=False);cat=engine.catalogue(pairs)
        expected=1-Fraction(3,4)**4
        self.assertEqual(zero_excess_reliability(cat),expected)
        # Independent availability states evaluated with the independent RAF oracle.
        count=sum(oracle_raf(pairs,ids) or bool(oracle_catalogue(pairs.restrict(ids)))
                  for size in range(9) for ids in combinations(range(8),size))
        self.assertEqual(Fraction(count,256),expected)

    def test_food_catalyst_empty_products_ids_and_budget(self):
        # Food catalyst makes nonfood catalyst alternatives irrelevant.
        system=ReactionSystem({'f'},[Reaction(20,{'f'},{'x','y'},{'f','x','y'}),Reaction(99,{'f'},set(),{'f'})])
        cat=SupplierEnumerator().catalogue(system)
        self.assertEqual(cat.beta,0);self.assertEqual(set(cat.members),{frozenset((20,)),frozenset((99,))})
        empty=SupplierEnumerator().catalogue(ReactionSystem({'f'},[]))
        self.assertEqual(empty.members,());self.assertEqual(zero_excess_reliability(empty),0)
        with self.assertRaises(ValueError): SupplierEnumerator(2).global_catalogue(coupled_gates(2))
        with self.assertRaises(ValueError): ReactionSystem({'f'},[Reaction(0,set(),set(),set())]*2)
        with self.assertRaises(ValueError): zero_excess_reliability(SupplierEnumerator().catalogue(coupled_gates(2)))


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