"""Independent identities, boundary cases, and full-flow accounting."""
import unittest
from fractions import Fraction as Q
import numpy as np
from example import (A,B,Y,I,WEIGHTS,STOICH,INITIAL_STATE,BASE_RETENTION,LocalChemistry,
    CommonExchange,Pulse,ReactorNetwork,HarvestMission,HistoryPolicy,MissionCertificate,
    PhaseCertificate,algebra_checks,pilot)


class ScientificChecks(unittest.TestCase):
    def test_exact_chemistry_and_phase(self):
        result=algebra_checks()
        self.assertLess(Q(result['selective']['coupled_Y_drift']),0)
        self.assertGreater(Q(result['phase']['certified_floor']),Q(1,160))
        np.testing.assert_array_equal(A@STOICH,0);np.testing.assert_array_equal(B@STOICH,0)
        self.assertEqual(MissionCertificate(4).bounds(30)['net_synthesis'],Q(1,35))
        self.assertLess(MissionCertificate(4).bounds(29)['net_synthesis'],0)
        self.assertEqual(MissionCertificate(4).bounds(28,False)['net_synthesis'],Q(33,280))

    def test_transport_conservation_and_minimum(self):
        rng=np.random.default_rng(450)
        for n in (1,3,9):
            k=rng.random((n,n));k=k+k.T;np.fill_diagonal(k,0);g=CommonExchange(k)
            c=rng.random((n,6));dc=g.field(c)
            np.testing.assert_allclose(dc.sum(axis=0),0,atol=1e-14)
            for weight in WEIGHTS:
                np.testing.assert_allclose(dc@weight,g.D@(c@weight),atol=1e-14)
                self.assertGreaterEqual((dc@weight)[np.argmin(c@weight)],-1e-14)
            total=sum(rate*c[i,SPECIES_INDEX[s]] for i,j,s,rate in g.directed_labels())
            self.assertAlmostEqual(total,g.gross_handling(c))
        with self.assertRaises(ValueError):CommonExchange([[0,1],[0,0]])
        with self.assertRaises(ValueError):CommonExchange([[1]])

    def test_pulse_accounting_and_regions(self):
        c=np.array(INITIAL_STATE);net=ReactorNetwork(LocalChemistry(),CommonExchange.four_nodes('path'))
        self.assertTrue(net.in_region(c,True))
        for q in (.25,.75):
            after,withdraw,loss,food=Pulse((q,)*4,(.98,)*6,(.005,-.005)).apply(c)
            original=after+withdraw+loss;original[:,:2]-=food
            np.testing.assert_allclose(original,c,atol=1e-15)
            self.assertTrue(net.in_region(after))
            np.testing.assert_allclose(after@I+withdraw@I+loss@I,c@I)
        with self.assertRaises(ValueError):Pulse((.24,)*4).apply(c)

    def test_full_flow_and_literal_pilot(self):
        p,part=pilot('uncoupled')
        self.assertAlmostEqual(p['collection_I_min'],.2681556621,places=7)
        self.assertAlmostEqual(p['collection_X_min'],.1243312990,places=7)
        self.assertLess(p['material_error'],1e-8);self.assertLess(p['inventory_error'],1e-8)
        self.assertGreater(p['final_Y_min'],.05)
        p,part=pilot('ring',scale=5)
        self.assertGreater(p['collection_X_min'],1/160)
        self.assertLess(p['handling'],p['handling_bound'])

    def test_history_and_global_inventory(self):
        class Recording(HistoryPolicy):
            def __init__(self):self.seen=[]
            def choose(self,c,history):
                self.seen.append((c.copy(),len(history)))
                return Pulse(BASE_RETENTION)
        policy=Recording();net=ReactorNetwork(LocalChemistry(),CommonExchange.four_nodes('path'))
        result=HarvestMission(net,policy).run(INITIAL_STATE,2,conditioning=False)
        self.assertEqual([v[1] for v in policy.seen],[0,1])
        self.assertGreater(np.max(abs(policy.seen[1][0]-policy.seen[0][0])),.01)
        self.assertLess(result['inventory_residual'],1e-8)
        self.assertGreater(result['withdrawn_I'],0);self.assertGreater(result['lost_I'],0)
        self.assertLess(max(result['food']),float(MissionCertificate(4).bounds(2,False)['each_food']))
        self.assertLess(result['gross_service'],float(MissionCertificate(4).bounds(2,False)['gross_service']))

    def test_budget_sizing_and_input_rejection(self):
        c=MissionCertificate(4);s=c.size('3/2','1/5',64,300,280,12)
        self.assertEqual((s['required_cycles'],s['funded_cycles']),(11,12));self.assertTrue(s['feasible'])
        self.assertEqual(c.bounds(11)['each_food'],Q(6506,25))
        self.assertFalse(c.size(100,100,64,300,280,12)['feasible'])
        with self.assertRaises(ValueError):c.size(0,0,11,300,280,12)
        with self.assertRaises(ValueError):c.size(0,0,64,0,280,12)
        with self.assertRaises(ValueError):c.bounds(-1)
        self.assertFalse(LocalChemistry([18],[.03]).certified)
        net=ReactorNetwork(LocalChemistry([20],[.03]),CommonExchange([[0]]))
        with self.assertRaises(ValueError):HarvestMission(net,HistoryPolicy()).run([[1,1,0,0,0,0]],1)

    def test_phase_is_lower_system_not_jacobian(self):
        chem=LocalChemistry([21],[.04]);M=np.array(PhaseCertificate.M,float)
        rng=np.random.default_rng(451)
        for _ in range(200):
            c=rng.random(6);c*=1.1/max(A@c,B@c)
            self.assertGreaterEqual(np.min(chem.field(c[None,:])[0,2:]-M@c[2:]),-1e-12)
        # Same Y at two nodes: common exchange gives zero Y transport; X-only does not.
        c=np.array(INITIAL_STATE)[[0,2]];g=CommonExchange([[0,2],[2,0]])
        np.testing.assert_allclose(g.field(c)@Y,0,atol=1e-16)
        selective=np.zeros_like(c);selective[:,2]=g.field(c)[:,2]
        self.assertAlmostEqual((selective@Y)[0],-.1)


SPECIES_INDEX={s:i for i,s in enumerate(('U','W','X','C1','C2','Z'))}
if __name__=='__main__':unittest.main()
