"""Finite independent audits of RAF semantics, counting, queries and constructions."""
import random
import unittest
from itertools import product
from fractions import Fraction as Q
from example import (FOOD,REACTIONS,CORE,CONTEXT_WEIGHTS,Reaction,CatalyticSystem,
    SupportedFamily,CoreAnalysis,ModuleDiagnostics,FoodReadyGraph,UnionClosedFamily,
    FamilyRealization,worked_system,subsets)


class ScientificChecks(unittest.TestCase):
    def test_literal_closure_ignores_catalysis_but_not_reactants(self):
        q=CatalyticSystem({'f'},{'a':Reaction({'f'},{'x'},{'y'}),'b':Reaction({'x'},{'y'},{'x'})})
        self.assertEqual(q.closure_stages({'a','b'}),({'f'},{'f','x'},{'f','x','y'}))
        self.assertTrue(q.is_raf({'a','b'}));self.assertFalse(q.fixed({'a'}))
        blocked=CatalyticSystem({'f'},{'a':Reaction({'x'},{'y'},{'f'}),'b':Reaction({'y'},{'x'},{'f'})})
        self.assertEqual(blocked.closure(blocked.names),{'f'})
        self.assertEqual(blocked.max_raf(),frozenset())
        self.assertEqual(FoodReadyGraph(blocked).abundance_candidates()['reason'],'no_nonempty_raf')
        self.assertEqual(FoodReadyGraph(q).abundance_candidates()['candidates'],['b'])
        with self.assertRaises(ValueError):q.fixed({'unknown'})

    def test_horizontal_vertical_injection_for_all_three_head_bodies(self):
        ground=frozenset('abc');cube=tuple(subsets(ground));nonempty=cube[1:]
        for choices in product(nonempty,repeat=3):
            bodies=dict(zip(sorted(ground),choices))
            for required in cube:
                report=SupportedFamily(ground,bodies,lambda S,R=required:R<=S).audit_injection()
                self.assertGreaterEqual(report['slack'],0)
        with self.assertRaises(ValueError):SupportedFamily(ground,{r:ground for r in ground},lambda S:not S).audit_injection()
        with self.assertRaises(ValueError):SupportedFamily({'a'},{'a':set()},lambda S:True)

    def test_changing_witness_weights_and_viable_core_requirement(self):
        q=CatalyticSystem(FOOD,REACTIONS);a=CoreAnalysis(q,CORE)
        report=a.report(CONTEXT_WEIGHTS)
        self.assertEqual(report['N_including_empty'],12)
        self.assertEqual(report['counts'],{'a':6,'b':6,'g':7,'h':7})
        self.assertEqual([f['slack'] for f in report['fibres']],[0,0,0,0])
        self.assertEqual(report['weighted_core_frequencies'],{'a':'5/9','b':'4/9'})
        for T,witness in [(frozenset({'g'}),'a'),(frozenset({'h'}),'b')]:
            conditioned=a.report({T:1})
            self.assertEqual(conditioned['weighted_abundant'],[witness])
        self.assertIsNone(a.report({})['weighted_core_frequencies'])
        with self.assertRaises(ValueError):a.report({():-1})
        with self.assertRaises(ValueError):CoreAnalysis(q,{'a'})
        feedback=worked_system('feedback')
        f=CoreAnalysis(feedback,{'a','b'}).fibre({'g'})
        self.assertEqual(f['family'],[['a','b']])  # g's product cannot bootstrap its missing reactant z

    def test_pruning_projection_cycles_against_exhaustive_random_systems(self):
        rng=random.Random(2001);molecules=('f','x','y','z');names=('a','b','c','d')
        for _ in range(75):
            def draw():return frozenset(m for m in molecules if rng.random()<.35)
            q=CatalyticSystem({'f'},{r:Reaction(draw(),draw(),draw()) for r in names})
            family=q.family()
            for availability in subsets(names):
                expected=frozenset().union(*(W for W in family if W<=availability))
                self.assertEqual(q.max_raf(availability),expected)
            E=q.food_ready
            self.assertEqual(ModuleDiagnostics(q,E).projection_queries()['preserves_projection'],all(q.fixed(W&E) for W in family))
            graph=FoodReadyGraph(q);cycle=graph.shortest_cycle()
            elementary=[W for W in family if W and W<=E]
            self.assertEqual(cycle is None,not elementary)
            if cycle:self.assertEqual(len(cycle),min(map(len,elementary)))
            packing=graph.greedy_packing()
            self.assertEqual(sum(map(len,packing)),len(set().union(*map(set,packing))))
            rare={r for r in names if 2*sum(r in W for W in family)<len(family)}
            self.assertIsNone(graph.shortest_cycle(rare))

    def test_projection_feedback_and_tight_defect_examples(self):
        q=worked_system('feedback');d=ModuleDiagnostics(q,{'a','b'})
        self.assertTrue(d.projection_queries()['preserves_projection'])
        self.assertTrue(q.reactions['g'].products&q.reactions['a'].catalysts)
        q=worked_system('failed_projection');d=ModuleDiagnostics(q,{'a','b'})
        self.assertFalse(d.projection_queries()['preserves_projection'])
        self.assertEqual(d.defect()['lhs'],4);self.assertEqual(d.defect()['rhs'],4)
        q=worked_system('false_average');family=q.family()
        self.assertLess(2*sum(map(len,family)),len(q.names)*len(family))
        for k in range(1,7):
            q=CatalyticSystem({'f'},{str(i):Reaction({'f'},{str(i)},{str((i-1)%k)}) for i in range(k)})
            self.assertEqual(set(q.family()),{frozenset(),q.names})

    def test_pair_realization_all_union_closed_families_on_three_coordinates(self):
        V=frozenset('abc');cube=tuple(subsets(V));checked=0
        for mask in range(1<<len(cube)):
            members=[cube[i] for i in range(len(cube)) if mask>>i&1]
            try:family=UnionClosedFamily(V,members)
            except ValueError:continue
            report=FamilyRealization(family).audit_pairs();checked+=1
            self.assertEqual(len(report['queries']),64)
            self.assertLessEqual(report['max_closure_growth_rounds'],2)
            singletons={v for v in V if frozenset({v}) in family.members}
            self.assertEqual(set(report['elementary_max_raf']),FamilyRealization.encode(singletons))
        self.assertGreater(checked,40)
        dead=FamilyRealization(UnionClosedFamily({'a','dead'},[(),('a',)]))
        self.assertIn(frozenset(),dead.blockers['dead'])
        report=dead.audit_pairs();self.assertEqual(report['frequencies']['dead'],0)
        coreless=FamilyRealization(UnionClosedFamily('abc',[(),('a','b'),('a','c'),('b','c'),('a','b','c')])).paired()
        self.assertIsNone(FoodReadyGraph(coreless).abundance_candidates()['candidates'])

    def test_exact_ground_food_generation_and_predecessor_factorization(self):
        family=UnionClosedFamily('abc',[(),('a',),('a','b'),('a','c'),('a','b','c')])
        P={'a':{'a'},'b':{'a'},'c':{'b'}}
        q=FamilyRealization(family).exact_ground(P)
        for S in subsets(q.names):
            self.assertEqual(q.food_generated(S),S in family.members)
            self.assertEqual(q.fixed(S),S in family.members and all(S&P[v] for v in S))
        inaccessible=UnionClosedFamily('ab',[(),('a','b')])
        with self.assertRaises(ValueError):FamilyRealization(inaccessible).exact_ground({'a':{'b'},'b':{'a'}})
        with self.assertRaises(ValueError):tuple(subsets(range(30)))


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