"""Exact finite semantics, independent brute-force checks, and resource-limit behavior."""
from fractions import Fraction as Q
from itertools import product
import math
import unittest
from example import (FOOD,C_STAR,PolymerCatalogue,ReversibleClosure,ProductiveHistories,
    FinitePrediction,StaticField,RepairConstruction,RationalConversion,EffectiveApproximation,
    ResourceLimit)


class ScientificChecks(unittest.TestCase):
    def test_catalogue_and_singleton_semantics(self):
        for mode,A,G,R16 in [('sp',248,36,1835012),('qt',234,34,1834798)]:
            cat=PolymerCatalogue(mode)
            for n in range(2,10):self.assertEqual(len(cat.channels(n)),cat.channel_count(n))
            self.assertEqual(cat.channel_count(16),R16)
            gates=[c for c in cat.channels(4) if c.enabled(FOOD)];self.assertEqual(len(gates),G)
            count=0
            for c in cat.channels(4):
                for x in cat.words(4):
                    raf=ReversibleClosure.maximal_raf([c],{c:{x}})
                    expected=c.enabled(FOOD) and x in ReversibleClosure.compute([c])
                    self.assertEqual(bool(raf),expected);count+=bool(raf)
            self.assertEqual(count,A)
        sp=PolymerCatalogue('sp');qt=PolymerCatalogue('qt')
        self.assertNotEqual(sp.channel('0','00'),sp.channel('00','0'))
        self.assertEqual(qt.channel('0','00'),qt.channel('00','0'))
        self.assertNotEqual(qt.channel('0','01'),qt.channel('01','0'))

    def test_product_catalysis_and_mutual_raf(self):
        cat=PolymerCatalogue('sp');a=cat.channel('00','0');b=cat.channel('00','1')
        self.assertEqual(ReversibleClosure.maximal_raf([a],{a:{'000'}}),frozenset([a]))
        marks={a:{'001'},b:{'000'}}
        self.assertFalse(ReversibleClosure.maximal_raf([a],marks));self.assertFalse(ReversibleClosure.maximal_raf([b],marks))
        self.assertEqual(ReversibleClosure.maximal_raf([a,b],marks),frozenset([a,b]))
        same=cat.channel('00','00')
        self.assertIn('0000',ReversibleClosure.compute([same]))  # one type, no molecule counts

    def test_history_census_and_exact_bounds(self):
        for mode,g,c2 in [('sp',32,2272),('qt',30,2130)]:
            h=ProductiveHistories(PolymerCatalogue(mode));rows=h.census(2)
            self.assertEqual(rows[0]['next_histories'],g);self.assertEqual(h.coefficient(2),c2)
            # Independent ordered enumeration to depth two, without state merging.
            cat=h.catalogue;count=0
            for ch in cat.productive(FOOD):count+=len(cat.productive(FOOD|ch.endpoints))
            self.assertEqual(count,rows[1]['next_histories'])
            for exponent,r,target in [(3,8,5),(4,12,15),(6,18,45),(20,65,629)]:
                self.assertLess(Q(h.coefficient(r),10**(exponent*r)),Q(1,10**target))
            for r in range(90):self.assertLessEqual(h.coefficient(r),36**r*2**(r*(r-1)//2))

    def test_finite_predictions_and_cap_restriction(self):
        for mode,lo,hi in [('sp',Q(216,10**9),Q(457,10**7)),('qt',Q(204,10**9),Q(431,10**7))]:
            p=FinitePrediction(PolymerCatalogue(mode),16);v=p.bounds(p.canonical_probability(Q(1,10000)))
            self.assertGreater(v['bonferroni_lower'],lo);self.assertLess(Q(v['interval']['upper']),hi)
            self.assertEqual(v['best']['r'],2)
            self.assertEqual(p.bounds(0)['singleton'],0);self.assertEqual(p.bounds(1)['singleton'],1)
            p4=FinitePrediction(PolymerCatalogue(mode),4)
            self.assertTrue(all(c['r'] is None for c in p4.bounds(Q(1,100))['candidates']))
        with self.assertRaises(ValueError):FinitePrediction(PolymerCatalogue('sp'),3)
        with self.assertRaises(ValueError):p.bounds(Q(11,10))

    def test_finite_field_against_all_configurations(self):
        for mode,expected in [('sp',Q(3,16)),('qt',Q(1,8))]:
            cat=PolymerCatalogue(mode);support=RepairConstruction(cat).support('000','101')
            channels=tuple(sorted(set(support)|{cat.channel('0','00'),cat.channel('00','0')}))
            field=StaticField(channels)
            for a in (Q(0),Q(1,3),Q(1,2),Q(1)):
                exact=Q(0)
                for flags in product((0,1),repeat=len(channels)):
                    c=ReversibleClosure.compute([ch for ch,b in zip(channels,flags) if b]);s=sum(flags)
                    if max(map(len,c))>4:exact+=a**s*(1-a)**(len(channels)-s)
                self.assertEqual(field.escape(a,4),exact)
            self.assertEqual(field.escape(Q(1,2),4),expected)
            self.assertEqual(StaticField.full_escape(cat,Q(1,2),2),1-Q(1,2)**cat.growth_count)
            with self.assertRaises(ResourceLimit):field.escape(Q(1,2),4,max_decisions=0)
            with self.assertRaises(ResourceLimit):StaticField.full_escape(cat,Q(1,2),100)

    def test_seed_repair_and_budgeted_evaluation(self):
        for mode,length in [('sp',4830),('qt',21520)]:
            cat=PolymerCatalogue(mode);e=EffectiveApproximation(cat)
            self.assertEqual(e.seed(Q(1,2),100),length)
            b=Q(1,2) if mode=='sp' else Q(1,4);k=length//10;factor=C_STAR*(100*101+1)
            self.assertLessEqual(factor*(1-b*b)**k,1);self.assertGreater(factor*(1-b*b)**(k-1),1)
            for target in cat.words(3):
                support=RepairConstruction(cat).support('010',target)
                self.assertIn(target,ReversibleClosure.compute(support,FOOD|{'010'}))
                self.assertTrue(all(len(ch.product)>3 for ch in support))
            # Small analogue independently checks the conservative record inequality.
            r=e.record_upper(Q(1,2),2,Q(1,10));self.assertLessEqual(8*(1-Q(1,8))**r,Q(1,10))
            for a in (0,1):self.assertEqual(e.evaluate(a,Q(1,100))['interval']['lower'],str(a))
            result=e.evaluate(Q(1,10000),Q(1,10**12));self.assertLess(Q(result['interval']['width']),Q(1,10**12))
            with self.assertRaises(ResourceLimit):e.evaluate(Q(1,2),Q(1,100))
            with self.assertRaises(ResourceLimit):e.seed(Q(1,10000),100)

    def test_parameter_conversion(self):
        for z in (Q(0),Q(1,10000),Q(1,2),Q(3)):
            v=RationalConversion.openness(z,Q(1,10**12));reference=-math.expm1(-float(z))
            self.assertLessEqual(float(v.lower),reference+1e-15);self.assertGreaterEqual(float(v.upper),reference-1e-15)
            self.assertLessEqual(v.width,Q(1,10**12))
        v=RationalConversion.openness(Q(1,10000),Q(1,10**16));self.assertLess(v.upper,Q(1,10000))
        v=RationalConversion.intensity(Q(1,2),Q(1,10**12));self.assertLess(float(v.lower),math.log(2));self.assertGreater(float(v.upper),math.log(2))
        with self.assertRaises(ValueError):RationalConversion.intensity(1,Q(1,100))


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