import json from datetime import UTC, datetime from production_analytics.calculations.material_consumption import ( MaterialConsumptionIntegrator, MaterialIntegrationState, MaterialIntegratorConfig, MaterialSample, ) from production_analytics.calculations.material_state import ( load_material_state, material_state_from_dict, material_state_to_dict, save_material_state, ) def test_material_state_dict_roundtrip() -> None: original = MaterialIntegrationState( cumulative_consumption_kg=51.42714233398436, integrated_running_seconds=180.0, last_processed_timestamp=datetime(2026, 9, 3, 5, 10, 50, tzinfo=UTC), last_material_rate_kg_per_hour=1028.5428466796875, last_gate_value=2.1673481464385986, integration_active=True, ) restored = material_state_from_dict(material_state_to_dict(original)) assert restored == original def test_material_state_json_file_roundtrip(tmp_path) -> None: original = MaterialIntegrationState( cumulative_consumption_kg=12.5, integrated_running_seconds=90.0, last_processed_timestamp=datetime(2026, 9, 3, 5, 10, 50, tzinfo=UTC), last_material_rate_kg_per_hour=500.0, last_gate_value=2.0, integration_active=True, ) path = tmp_path / "material-state.json" save_material_state(str(path), original) restored = load_material_state(str(path)) assert restored == original raw = json.loads(path.read_text(encoding="utf-8")) assert raw["last_processed_timestamp"] == "2026-09-03T05:10:50+00:00" def test_integrator_restore_continues_without_double_counting(tmp_path) -> None: config = MaterialIntegratorConfig( gate_threshold=0.5, max_sample_gap_seconds=20.0, ) samples = [ MaterialSample( timestamp=datetime(2026, 9, 3, 5, 9, 40, tzinfo=UTC), material_rate_kg_per_hour=3600.0, gate_value=1.0, ), MaterialSample( timestamp=datetime(2026, 9, 3, 5, 9, 50, tzinfo=UTC), material_rate_kg_per_hour=3600.0, gate_value=1.0, ), MaterialSample( timestamp=datetime(2026, 9, 3, 5, 10, 0, tzinfo=UTC), material_rate_kg_per_hour=3600.0, gate_value=1.0, ), ] continuous = MaterialConsumptionIntegrator(config) continuous.process_many(samples) before_restart = MaterialConsumptionIntegrator(config) before_restart.process_many(samples[:2]) path = tmp_path / "material-state.json" save_material_state(str(path), before_restart.state) restored = MaterialConsumptionIntegrator( config, initial_state=load_material_state(str(path)), ) # Polling deliberately starts inclusively at the last processed timestamp. restored.process_many(samples[1:]) assert restored.state == continuous.state assert restored.state.cumulative_consumption_kg == 20.0 assert restored.state.integrated_running_seconds == 20.0