"""Independent RAF oracle, adversarial certificates and exact robustness laws."""
import unittest
from fractions import Fraction as F
from itertools import combinations,product
from example import (Reaction,ReactionSystem,RankedWitness,DeletionEngine,paired_source,
    greedy_portfolio,RobustnessPolynomial,FunctionalSource,OneSiteExposure,subsets)


def oracle(system,available):
    """Enumerate all RAFs, with an independent round-synchronous closure."""
    result=set()
    for selected in subsets(sorted(available)):
        pool=set(system.food)
        while True:
            following=pool|set().union(*(system.reactions[r].products for r in selected if system.reactions[r].reactants<=pool))
            if following==pool: break
            pool=following
        if all(system.reactions[r].reactants<=pool and system.reactions[r].catalysts&pool for r in selected): result.update(selected)
    return frozenset(result)


class DeletionTests(unittest.TestCase):
    def test_all_paired_deletions_and_portfolio_wall(self):
        system,pool=paired_source(3);s=system.identifiers
        for witnesses in (pool[:1],(pool[0],pool[-1]),pool):
            engine=DeletionEngine(system,witnesses)
            for deleted in subsets(sorted(s)):
                result=engine.query(deleted);expected=oracle(system,s-deleted)
                self.assertEqual(set(result['surviving']),expected)
                self.assertTrue(s-expected<=set(result['region']))
                if len(witnesses)==len(pool): self.assertEqual(set(result['region']),s-expected)
                if len(deleted)==1 and len(witnesses)>1: self.assertEqual(set(result['region']),s-expected)
        # Removing any normalized assignment leaves one mixed deletion unrecovered
        # by the region intersection, though the residual answer remains exact.
        for omitted,w in enumerate(pool):
            chosen=w.parents[6];deleted=set(range(6))-chosen
            result=DeletionEngine(system,pool[:omitted]+pool[omitted+1:]).query(deleted)
            self.assertGreater(result['excess'],0);self.assertNotIn(6,result['loss'])

    def test_perfect_query_and_catalysts_without_rank_restriction(self):
        system,pool=paired_source(3);s=system.identifiers
        for deleted in subsets(sorted(s)):
            retained=system.maximum(s-deleted);w=RankedWitness.build(system,s,retained)
            self.assertEqual(w.cone(deleted),s-retained)
        cyclic=ReactionSystem([Reaction(0,{'f'},{'h'},{'f'}),Reaction(1,{'h'},{'a'},{'b'}),Reaction(2,{'h'},{'b'},{'a'})])
        w=RankedWitness.build(cyclic);self.assertIn(2,w.parents[1]);self.assertLess(w.ranks[1],w.ranks[2])
        for deleted in subsets(range(3)):
            self.assertEqual(set(DeletionEngine(cyclic,[w]).query(deleted)['surviving']),oracle(cyclic,set(range(3))-deleted))
        invalid=RankedWitness(cyclic.identifiers,{0:set(),1:{2},2:{1}},dict.fromkeys(range(3),0))
        with self.assertRaises(ValueError): DeletionEngine(cyclic,[invalid])

    def test_certificate_rejection_falls_back_exactly(self):
        system,pool=paired_source(3);engine=DeletionEngine(system,pool[:1]);deletion={0,3}
        for certificate in ([],[{'available':[6],'schedule':[]}],[{'available':[6],'schedule':[999]}]):
            result=engine.query(deletion,certificate)
            self.assertTrue(result['fallback']);self.assertEqual(set(result['surviving']),system.maximum(system.identifiers-deletion))
        result=engine.query({6});self.assertEqual(result['route'],'closed seed');self.assertEqual(result['loss'],[6])
        # Engine snapshots a validated witness instead of retaining a mutable proposer.
        pool[0].parents[6]=frozenset()
        self.assertEqual(set(engine.query(deletion)['surviving']),system.maximum(system.identifiers-deletion))

    def test_greedy_training_coverage_bound(self):
        system,pool=paired_source(3);queries=tuple({r} for r in sorted(system.identifiers))
        chosen,receipt=greedy_portfolio(pool,queries,2)
        def coverage(witnesses): return len(set().union(*({(i,r) for i,k in enumerate(queries) for r in w.baseline-w.cone(k)} for w in witnesses)))
        optimum=max(coverage(pair) for pair in combinations(pool,2))
        self.assertEqual(receipt['rescued_training_events'],coverage(chosen))
        self.assertGreaterEqual(F(coverage(chosen)),F(3,4)*optimum)
        for deleted in subsets(sorted(system.identifiers)):
            self.assertEqual(set(DeletionEngine(system,chosen).query(deleted)['surviving']),system.maximum(system.identifiers-deleted))

    def test_curvature_all_elementary_three_reaction_graphs(self):
        choices=tuple(s for s in subsets(range(3)) if s);count=0
        for catalysts in product(choices,repeat=3):
            system=ReactionSystem([Reaction(r,{'f'},{f'x{r}'},{f'x{p}' for p in catalysts[r]}) for r in range(3)])
            poly=RobustnessPolynomial(system);poly.sensitivity();count+=1
            for available in subsets(range(3)): self.assertEqual(system.maximum(available),oracle(system,available))
        self.assertEqual(count,343)
        cooperative,_=paired_source(1);self.assertEqual(RobustnessPolynomial(cooperative).mean,[0,2,2,-1])
        self.assertEqual(RobustnessPolynomial(cooperative).sensitivity()['second_derivative'],-2)
        cycle=RobustnessPolynomial(FunctionalSource((1,2,0)).network())
        self.assertEqual(cycle.sensitivity()['second_derivative'],18)

    def test_all_three_reaction_one_site_exposures_cuts_and_gains(self):
        for parents in product(range(3),repeat=3):
            source=FunctionalSource(parents);base=source.network()
            for site,parent in product(range(3),repeat=2):
                addition=source.addition(site,parent);network=source.network(addition.alternative)
                for available in subsets(range(3)): self.assertEqual(addition.surviving(available),oracle(network,available))
                for target in range(3):
                    external=set(range(3))-{target}
                    cuts=[d for d in subsets(sorted(external)) if target not in network.maximum(set(range(3))-d)]
                    minimal=frozenset(d for d in cuts if not any(e<d for e in cuts))
                    self.assertEqual(addition.cuts(target),minimal)
                    for p in (F(1,4),F(1,2),F(3,4)):
                        probability=sum((p**len(a)*(1-p)**(3-len(a)) for a in subsets(range(3)) if target in network.maximum(a)),F(0))
                        original=sum((p**len(a)*(1-p)**(3-len(a)) for a in subsets(range(3)) if target in base.maximum(a)),F(0))
                        self.assertEqual(addition.probability(target,p),probability);self.assertEqual(addition.gain(target,p),probability-original)
        with self.assertRaises(ValueError): OneSiteExposure(FunctionalSource((0,0,0)),FunctionalSource((1,2,0)))

    def test_variance_controls_and_new_cycle(self):
        for parents,variance in [((0,2,0,4,0),F(9,4)),((0,2,1,1,1),F(33,16))]:
            source=FunctionalSource(parents);moments=source.moments(F(1,2));literal=RobustnessPolynomial(source.network()).moments(F(1,2))
            self.assertEqual(moments['mean'],F(5,4));self.assertEqual(moments['variance'],variance)
            self.assertEqual(moments['variance'],literal['variance'])
        for n in range(1,7):
            gateway=FunctionalSource((0,)*n);poly=RobustnessPolynomial(gateway.network())
            p=F(3,5);moments=poly.moments(p)
            self.assertEqual(moments['nonempty'],p)
            self.assertEqual(moments['variance'],p*(1-p)*(1+(n-1)*p)**2+(n-1)*p*p*(1-p))
        source=FunctionalSource((0,0));addition=source.addition(1,1)
        self.assertEqual(source.network().maximum({1}),set());self.assertEqual(addition.surviving({1}),{1})
        self.assertEqual(addition.cuts(1),set())


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