"""Unified action/obs flow — host-only set_body, auto transfer, stream API, DRL loop pattern.

Requires GPU."""

import unittest

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

from CelerisLab.simulation import Simulation


class TestUnifiedObs(unittest.TestCase):
    """Test unified action/obs flow."""

    def test_set_body_then_run_read_body(self):
        """set_body (host-only), run, read_body returns finite force."""
        sim = Simulation(device_id=0)
        nx = sim.lbm_cfg.nx
        ny = sim.lbm_cfg.ny
        sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
        sim.initialize()
        sim.run(50)

        # set_body should not trigger H2D (no error expected)
        sim.set_body(0, omega=0.001)

        # run will auto-upload action
        sim.run(50)
        data = sim.read_body(0)
        self.assertTrue(np.all(np.isfinite(data.force)),
                        f"Force finite: {data.force}")
        sim.close()

    def test_skip_transfer(self):
        """run(upload_act=False, sync_obs=False) should not crash."""
        sim = Simulation(device_id=0)
        nx = sim.lbm_cfg.nx
        ny = sim.lbm_cfg.ny
        sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
        sim.initialize()
        sim.run(50, upload_act=False, sync_obs=False)
        # After no-sync run, step count should still advance
        self.assertEqual(sim.stepper.step_count, 50)
        sim.close()

    def test_external_stream(self):
        """Providing an external CUDA stream should not crash."""
        sim = Simulation(device_id=0)
        nx = sim.lbm_cfg.nx
        ny = sim.lbm_cfg.ny
        sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
        sim.initialize()
        s = cuda.Stream()
        sim.run(50, stream=s)
        force = sim.read_force(0)
        self.assertTrue(np.all(np.isfinite(force)),
                        f"Force finite with external stream: {force}")
        sim.close()

    def test_read_body_before_run_returns_zeros(self):
        """read_body before any run() should return zero force (buffer is
        initialized to zero during sync_to_gpu)."""
        sim = Simulation(device_id=0)
        sim.add_body("circle", center=(128, 128), radius=8)
        sim.initialize()
        force = sim.read_force(0)
        np.testing.assert_array_equal(force, np.zeros(2, dtype=np.float32))
        sim.close()

    def test_drl_pattern(self):
        """DRL-style loop: run → read → set → run → read."""
        sim = Simulation(device_id=0)
        nx = sim.lbm_cfg.nx
        ny = sim.lbm_cfg.ny
        sim.add_body("circle", center=(nx // 4, ny // 2), radius=8)
        sim.initialize()
        for i in range(3):
            sim.run(50)
            data = sim.read_body(0)
            self.assertTrue(np.all(np.isfinite(data.force)))
            sim.set_body(0, omega=0.001 * i)
        self.assertEqual(sim.stepper.step_count, 150)
        sim.close()


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