import unittest
from fractions import Fraction as Q
from dataclasses import replace
import numpy as np
from resident import Cell,ChemicalRegions,A
from population import ChemicalModel,PopulationSource,UniformTransfer,SerialProtocol
from selection import MissionCertificate,WeightedTransfer,AbsorbingAudit,population_design,event_ceiling,exp_negative_upper,G,RHOS

class ScientificChecks(unittest.TestCase):
    def test_full_source_and_growth_division_conservation(self):
        chem=ChemicalModel();chem.certificates();cell=Cell('H',19,(31,13,40,21))
        np.testing.assert_allclose(chem.rates(cell),[float(c.rate(cell)) for c in chem.channels],rtol=2e-16)
        source=PopulationSource(10,'2');pop=source.refill([cell]);new=source.step(pop,0,13,np.random.default_rng(1),allocation=(10,5,18,10))
        self.assertEqual(new.size+new.precursor,pop.size+pop.precursor);self.assertEqual([c.size for c in new.cells],[10,10]);self.assertEqual(tuple(sum(c.counts[i] for c in new.cells) for i in range(4)),(31,13,39,21))
    def test_intact_transfer_phase_and_recovery(self):
        cells=tuple(Cell('H' if i<4 else 'L',10+i,(30,20,30,20)) for i in range(8));selected,discarded=UniformTransfer().sample(cells,4,np.random.default_rng(8))
        self.assertEqual(set(selected)|set(discarded),set(cells));self.assertFalse(set(selected)&set(discarded))
        source=PopulationSource(10,'2');pop=source.refill(selected);self.assertEqual(pop.precursor,4*sum(c.size for c in selected))
        recovering=replace(pop,precursor=0);new,record=source.run(recovering,.01,np.random.default_rng(3),10000,collect_endpoint=False)
        self.assertEqual(record['status'],'duration');self.assertEqual([c.size for c in new.cells],[c.size for c in selected]);self.assertEqual(new.precursor,0)
    def test_weighted_without_replacement_law(self):
        for sizes,fail in [([10]*8,Q(17,35)),([10,10,19,19]*2,Q(27,35))]:
            result=WeightedTransfer.enumerate(sizes,['H']*4+['L']*4,4,Q(1,50));self.assertEqual(result['failure'],fail)
            for tag,(mean,var) in result['moments'].items():
                self.assertEqual(sum((r[tag]-mean)**2 for r in result['rows'])/70,var)
        self.assertEqual(WeightedTransfer.moments([7],1),(7,0));self.assertEqual(WeightedTransfer.moments([7],0),(0,0))
        self.assertEqual(WeightedTransfer.minority_loss(400,100,4),Q(13231647,42029596));self.assertEqual(WeightedTransfer.minority_loss(5,5,1),0);self.assertEqual(WeightedTransfer.minority_loss(0,0,0),1)
    def test_joint_certificate_and_service_material(self):
        c=MissionCertificate();r=c.evaluate();self.assertGreater(Q(r['joint_success_lower']),Q(997,1000));self.assertEqual(r['minority_count_floor'],36862)
        self.assertGreater(MissionCertificate(M=10**13,refined=False).evaluate()['joint_success_decimal'],.994)
        self.assertLess(c.K*sum(c.chemistry().values()),c.binary_replay());a=c.resources()
        self.assertLess(Q(a['actual_batch_allowance']),c.batch_allowance);self.assertLess(Q(a['actual_recovery_allowance']),c.recovery_allowance)
        self.assertEqual(a['operating_precursor_upper'],76*c.N*c.M);self.assertEqual(a['total_with_terminal_refill_upper'],84*c.N*c.M)
        self.assertGreater(G[0],Q(3,4));self.assertEqual(exp_negative_upper(0),1)
    def test_inverse_bound_and_scope(self):
        design=population_design(2,'1/100');self.assertEqual(design['status'],'certified_transfer_design');self.assertLessEqual(Q(design['bound']),Q(1,100));self.assertGreater(Q(design['previous_even_bound']),Q(1,100))
        self.assertEqual(population_design(10,'1/100',max_M=2)['status'],'unknown_population_cap')
        self.assertEqual(event_ceiling(4000000000,31)['status'],'impossible_target_event');self.assertEqual(event_ceiling(4000000000,10)['status'],'not_excluded')
        with self.assertRaises(ValueError):MissionCertificate(M=3)
        with self.assertRaises(ValueError):MissionCertificate(gamma=Q(1,100))
    def test_readiness_and_absorption(self):
        regions=ChemicalRegions();N=65536*10**18
        for tag in ('H','L'):
            cell=regions.newborn(tag,N);self.assertTrue(regions.admits(cell,A,closed=True));self.assertEqual(cell.readout,tag)
        audit=AbsorbingAudit();self.assertEqual(audit.observe('bad',False),'failed');self.assertEqual(audit.observe('later',True),'failed');self.assertEqual(len(audit.records),1)
        audit=AbsorbingAudit();self.assertEqual(audit.observe('budget',None),'unresolved');self.assertEqual(audit.observe('later',True),'unresolved')
    def test_unfinished_physical_history_is_not_success(self):
        cells=[Cell('H',10,(30,20,30,20)),Cell('L',10,(30,20,10,20))]
        result=SerialProtocol(PopulationSource(10,'2'),2,.01).run(cells,3,10,0)
        self.assertFalse(result['complete']);self.assertEqual(result['partial']['status'],'event_budget');self.assertEqual(result['events'],0)

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