"""sync_bodies pipeline without DDF patch — recompile, esopull guard, step count preservation.

Requires GPU."""

import unittest

import numpy as np
import pycuda.driver as cuda
import pycuda.autoinit

from CelerisLab.simulation import Simulation


# Use a small grid for fast compilation and test execution
NX, NY = 128, 64


class TestSyncBodiesSkeleton(unittest.TestCase):
    """Test sync_bodies() with real GPU -- skeleton without DDF patch."""

    def _make_sim(self) -> Simulation:
        """Create a Simulation with a small double_buffer D2Q9 grid."""
        return Simulation(device_id=0)

    def test_remove_body_sync_and_run(self):
        """Remove a body, sync, and continue running without crash."""
        sim = self._make_sim()
        sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
        sim.initialize()
        sim.run(50)

        # Remove the body and sync
        sim.remove_body(0)
        sim.sync_bodies()

        # Should be able to continue running
        sim.run(50)

        # No body left -- count should be 0
        self.assertEqual(sim.bodies.count, 0)
        sim.close()

    def test_add_body_after_initialize(self):
        """Add a body after initialize, sync, and run."""
        sim = self._make_sim()
        sim.initialize()
        sim.run(50)

        # Add a body (returns -1 since it's staged)
        result_id = sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
        self.assertEqual(result_id, -1)
        self.assertTrue(sim.bodies.has_pending_edit())

        # Sync commits the body
        sim.sync_bodies()
        self.assertFalse(sim.bodies.has_pending_edit())
        self.assertEqual(sim.bodies.count, 1)

        # Should be able to run with the new body
        sim.run(50)

        # Force readback should work
        force = sim.read_force(0)
        self.assertEqual(force.shape[0], 2)
        sim.close()

    def test_add_then_remove_body(self):
        """Add a body, then remove it, sync -- should result in zero bodies."""
        sim = self._make_sim()
        sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
        sim.initialize()
        sim.run(50)

        # Add another body and remove the original
        sim.add_body("circle", center=(NX // 2, NY // 2), radius=6)
        sim.remove_body(0)
        sim.sync_bodies()

        # One body remaining (the newly added one, now id=0)
        self.assertEqual(sim.bodies.count, 1)
        sim.run(50)
        sim.close()

    def test_sync_preserves_step_count(self):
        """sync_bodies() should not reset the step counter."""
        sim = self._make_sim()
        sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
        sim.initialize()
        sim.run(100)
        steps_before = sim.stepper.step_count

        sim.remove_body(0)
        sim.sync_bodies()

        steps_after = sim.stepper.step_count
        self.assertEqual(steps_after, steps_before)
        sim.close()

    def test_no_pending_edit_is_noop(self):
        """sync_bodies() with no pending edits should be a no-op."""
        sim = self._make_sim()
        sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
        sim.initialize()
        sim.run(50)

        # No edits -- sync should return immediately
        sim.sync_bodies()
        self.assertEqual(sim.bodies.count, 1)
        sim.close()

    def test_run_discards_pending_without_sync(self):
        """Running without sync_bodies() should discard pending edits."""
        sim = self._make_sim()
        sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
        sim.initialize()
        sim.run(50)

        # Stage a removal but don't sync
        sim.remove_body(0)
        self.assertTrue(sim.bodies.has_pending_edit())

        # run() auto-discards pending
        sim.run(50)
        self.assertFalse(sim.bodies.has_pending_edit())
        self.assertEqual(sim.bodies.count, 1)
        sim.close()

    def test_esopull_raises_not_implemented(self):
        """sync_bodies() with esopull should raise NotImplementedError."""
        # Create a sim with esopull streaming
        from CelerisLab.config import load_lbm_config
        cfg = load_lbm_config()
        cfg.streaming = "esopull"
        # We need to build the sim manually to override streaming
        sim = Simulation.__new__(Simulation)
        sim._stream = None
        sim.lbm_cfg = cfg
        from CelerisLab.config import BodyConfig
        sim.body_cfg = BodyConfig()
        from CelerisLab.cuda.context import CudaContext
        sim.ctx = CudaContext(0)
        from CelerisLab.cuda import compiler_v2 as compiler
        arch = sim._resolve_compile_arch = lambda: sim.ctx.sm_arch
        arch_val = CudaContext(0).sm_arch
        compiler.generate_config(cfg, n_objects=0)
        ptx_path = compiler.compile_kernel(arch=arch_val)
        module = compiler.load_module(ptx_path)
        sim._ptx_path = ptx_path
        sim._module = module
        from CelerisLab.lbm.field import LBMField
        sim.field = LBMField(cfg, module)
        from CelerisLab.lbm.stepper import LBMStepper
        sim.stepper = LBMStepper(sim.field, module, cfg)
        from CelerisLab.body.manager import ObjectManager
        sim.bodies = ObjectManager(
            cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
        sim._initialized = True

        sim.add_body("circle", center=(NX // 4, NY // 2), radius=8)
        with self.assertRaises(NotImplementedError):
            sim.sync_bodies()
        sim.close()


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