import random
import unittest
from example import (Q, Reaction, CatalyticSystem, CoreCertificate, CoreValidator,
    ExactFamily, FibreAnalyzer, WeightedAnalysis, strict_example, three_chains,
    chain_family_formula, subsets)


class ScientificChecks(unittest.TestCase):
    def test_local_certificate_combined_closure_and_negative_controls(self):
        q=strict_example();v=CoreValidator();c=v.certificate(q,{'a','b'})
        self.assertTrue(c.validate(q));self.assertFalse(q.is_raf({'e'}));self.assertTrue(q.is_raf({'a','b','e'}))
        self.assertEqual(v.propose(q,q.names)['status'],'FAILED_CANDIDATE')
        self.assertEqual(set(ExactFamily(q).members),{frozenset(),frozenset({'a','b'}),frozenset({'a','b','e'})})
        row=FibreAnalyzer(q,c).analyze({'e'});self.assertEqual(row['members'],[['a','b']]);self.assertEqual(row['slack'],2)
        cycle=CatalyticSystem({'f'},{'p':Reaction({'y'},{'x'},{'f'}),'q':Reaction({'x'},{'y'},{'f'})})
        self.assertEqual(ExactFamily(cycle).members,(frozenset(),));self.assertEqual(v.propose(cycle,cycle.names)['reason'],'internal substrate cycle')
        split=CatalyticSystem({'f'},{'p':Reaction({'f'},{'x'},{'f'}),'q':Reaction({'f'},{'y'},{'f'}),'r':Reaction({'x','y'},{'z'},{'f'})})
        self.assertTrue(split.is_raf(split.names));self.assertIn('no complete supplier',v.propose(split,split.names)['reason'])

    def test_every_internal_incidence_and_transport(self):
        q=strict_example();c=CoreValidator().certificate(q,{'a','b'})
        # Chosen suppliers still work, but an unchosen self-incidence invalidates rank.
        rx=dict(q.reactions);rx['b']=Reaction({'x'},{'x','y'},{'f'});loop=CatalyticSystem(q.food,rx)
        with self.assertRaisesRegex(ValueError,'internal substrate'):c.validate(loop)
        rx={'a':q.reactions['a'],'b':q.reactions['b'],'outside':Reaction({'z'},{'z'},{'z'})}
        edited=CatalyticSystem({'f','new'},rx);self.assertTrue(c.validate(edited));self.assertTrue(edited.is_raf(c.core))
        enlarged=CatalyticSystem({'f','x'},rx);self.assertTrue(c.validate(enlarged))
        broken=CoreCertificate(c.core,c.rank,{'a':'a','b':'a'})
        with self.assertRaisesRegex(ValueError,'Incomplete supplier'):broken.validate(q)
        # Local validation must not evaluate food closure.
        q.closure=lambda *_: (_ for _ in ()).throw(AssertionError('not local'))
        self.assertTrue(c.validate(q))

    def test_exact_chain_family_frequencies_and_obstruction(self):
        for ell in (3,4):
            q,cores,formula=chain_family_formula(ell);f=ExactFamily(q)
            self.assertEqual(set(f.members),formula);self.assertEqual(len(f.members),ell+8)
            self.assertEqual(set(f.irreducible()),set(cores.values()))
            for U in cores.values():self.assertTrue(CoreValidator().certificate(q,U).validate(q))
            self.assertEqual([f.counts['b'+str(j)] for j in range(1,ell+1)],[ell+4-j for j in range(1,ell+1)])
            self.assertEqual(Q(sum(f.counts[r] for r in cores['B']),len(cores['B'])*len(f.members)),Q(1,2))
            self.assertFalse(any(W and all(q.reactions[r].reactants<=q.food for r in W) for W in f.members))
            self.assertFalse(any(all(not W or bool(W&pair) for W in f.members) for pair in subsets(q.names) if len(pair)==2))
            self.assertTrue(all(not W or bool(W&{'gA','gB','gC'}) for W in f.members))
            self.assertFalse(q.fixed(cores['A']|{'gB'}));self.assertFalse(q.fixed(cores['C']|{'gB'}));self.assertTrue(q.fixed(cores['A']|cores['C']|{'gB'}))
            self.assertLessEqual(max(len(q.closure_stages(W))-1 for W in f.members),2)

    def test_fibre_adapter_and_cell_injection(self):
        q,cores=three_chains(3);f=ExactFamily(q);c=CoreValidator().certificate(q,cores['B']);analyzer=FibreAnalyzer(q,c)
        rows=[analyzer.analyze(T) for T in subsets(q.names-c.core)]
        self.assertEqual(sum(len(row['members']) for row in rows),len(f.members))
        self.assertEqual(sum(row['slack'] for row in rows),0)
        self.assertTrue(any(row['injection'] for row in rows))
        for row in rows:
            targets={(tuple(x['target_deleted']),x['target_marked']) for x in row['injection']}
            self.assertEqual(len(targets),len(row['injection']))
            self.assertEqual(row['bad_one_cells']-row['bad_zero_cells'],row['slack'])
        with self.assertRaises(ValueError):analyzer.analyze({'b1'})

    def test_weights_sharpness_and_invalid_sampling(self):
        strict=strict_example();q=CatalyticSystem(strict.food,{r:strict.reactions[r] for r in ('a','b')});c=CoreValidator().certificate(q,q.names);weighted=WeightedAnalysis(ExactFamily(q),c)
        for kappa in (1,2,Q(7,3),10):
            result=weighted.analyze(lambda W:1 if W else kappa,kappa)
            self.assertEqual(Q(result['normalized_core_occupancy']),1/(1+Q(kappa)))
        with self.assertRaises(ValueError):weighted.analyze(lambda W:0 if W else 1,10)
        with self.assertRaises(ValueError):weighted.analyze(lambda W:0)
        with self.assertRaises(ValueError):weighted.analyze(lambda W:1.0)
        f=ExactFamily(strict);c=CoreValidator().certificate(strict,{'a','b'});weighted=WeightedAnalysis(f,c)
        result=weighted.analyze(lambda W:1 if 'e' in W else 0)
        self.assertEqual(result['normalized_core_occupancy'],'1')
        self.assertEqual(weighted.analyze(lambda W:7 if 'e' in W else 1)['normalized_core_occupancy'],'8/9')

    def test_deletion_and_conservation_lift(self):
        q,cores=three_chains(3);f=ExactFamily(q);panel=f.deletion(cores['B'])
        self.assertEqual(panel['remaining'],{'b1':4,'b2':5,'b3':6,'gB':3});self.assertEqual(panel['uniform_core_expected_remaining'],'9/2')
        for r in q.names:self.assertEqual(len(ExactFamily(q.delete(r)).members)-1,len(f.members)-1-f.counts[r])
        lift,_=three_chains(3,True);self.assertEqual(set(f.members),set(ExactFamily(lift).members))
        for rx in lift.reactions.values():
            weight=lambda m:2 if m in ('f','zB') else 1
            self.assertEqual(sum(map(weight,rx.reactants)),sum(map(weight,rx.products)))
            self.assertNotIn('w',rx.reactants|rx.catalysts)

    def test_random_small_systems_and_input_budgets(self):
        rng=random.Random(35092026);v=CoreValidator();certified=0
        for _ in range(60):
            mols=('f','x','y','z');rx={}
            for i in range(4):
                fields=[frozenset(m for m in mols if rng.random()<p) for p in (.22,.35,.35)]
                rx['r'+str(i)]=Reaction(*fields)
            q=CatalyticSystem({'f'},rx);f=ExactFamily(q)
            self.assertEqual(CatalyticSystem.from_record(q.record()).record(),q.record())
            for U in subsets(q.names):
                proposal=v.propose(q,U)
                if proposal['status']!='CERTIFIED':continue
                certified+=1;c=v.certificate(q,U);self.assertTrue(q.is_raf(U))
                self.assertGreaterEqual(2*sum(f.counts[r] for r in U),len(U)*len(f.members))
                for T in subsets(q.names-U):FibreAnalyzer(q,c).analyze(T)
        self.assertGreater(certified,10)
        with self.assertRaisesRegex(ValueError,'budget'):ExactFamily(three_chains(6)[0])
        with self.assertRaises(ValueError):v.propose(strict_example(),{'missing'})


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