"""
Test that Python exceptions in fitting callbacks are properly caught and reported.

This test creates a simulation builder function that raises an OverflowError.
The test verifies that:
1. The exception is caught (no segfault)
2. A proper error message is produced

Before the fix in j.h branch, this would cause a segfault.
After the fix, it should raise a RuntimeError with a meaningful message.
"""

import unittest
import bornagain as ba
from bornagain import deg, nm, nm2


def get_sample():
    """Simple sample with a cylinder."""
    vacuum = ba.Vacuum()
    substrate_color = (0.55, 0.55, 0.55)
    substrate_mat = ba.RefractiveMaterial("Substrate", substrate_color, 6e-6, 2e-8)
    particle_color = (0.86, 0.24, 0.18)
    particle_mat = ba.RefractiveMaterial("Particle", particle_color, 6e-4, 2e-8)

    ff = ba.Cylinder(5*nm, 5*nm)
    particle = ba.Particle(particle_mat, ff)

    vacuum_layer = ba.Layer(vacuum)
    vacuum_layer.deposit2D(ba.Dilute2D(0.001/nm2, particle))
    substrate_layer = ba.Layer(substrate_mat)

    sample = ba.Sample()
    sample.addLayer(vacuum_layer)
    sample.addLayer(substrate_layer)
    return sample


def get_simulation_with_overflow(P):
    """
    Simulation builder that triggers Python OverflowError.
    This mimics what happens when lg_background grows unbounded.

    Note: P is a dict with float values when called from FitObjective.
    """
    # This will raise OverflowError: (34, 'Numerical result out of range')
    # when lg_bg >= 309
    lg_bg = P['lg_background']
    background_value = 10.0 ** lg_bg

    beam = ba.Beam(1e8, 0.1*nm, 0.2*deg)
    det = ba.SphericalDetector(5, -1*deg, 1*deg, 5, 0, 2*deg)
    sample = get_sample()

    simulation = ba.ScatteringSimulation(beam, sample, det)
    simulation.setBackground(ba.ConstantBackground(background_value))
    return simulation


class PythonCallbackExceptionTest(unittest.TestCase):

    def test_overflow_in_simulation_builder(self):
        """
        Verify that a Python OverflowError in a simulation builder callback
        is caught and reported as a RuntimeError, not a segfault.
        """
        # Create fake experimental data using a dict (as the callback receives)
        P_true = {'lg_background': 1.0}
        sim_true = get_simulation_with_overflow(P_true)
        exp_data = sim_true.simulate()

        # Set up fit objective with the overflowing simulation builder
        fit_objective = ba.FitObjective()
        fit_objective.addFitPair(get_simulation_with_overflow, exp_data)

        # Create parameters with a value that will cause overflow (309 > 308)
        P = {'lg_background': 309.0}  # 10.0**309 overflows

        # This should raise RuntimeError, not segfault
        with self.assertRaises(RuntimeError) as context:
            fit_objective.evaluate(P)

        # Check that the error message mentions the overflow or Python exception
        error_msg = str(context.exception)
        self.assertTrue(
            "OverflowError" in error_msg or "Python" in error_msg,
            f"Expected error about Python/Overflow, got: {error_msg}"
        )


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