"""test_launch_orchestrator.py — Unit tests for saltybot_bringup launch profiles (Issue #577).

Tests are deliberately ROS2-free; run with:
    python3 -m pytest jetson/ros2_ws/src/saltybot_bringup/test/test_launch_orchestrator.py -v
"""

from __future__ import annotations

import sys
import os

# Allow import without ROS2 install
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "saltybot_bringup"))

import pytest
from launch_profiles import Profile, get_profile, profile_names, _minimal, _full, _debug


# ─── Profile factory basics ───────────────────────────────────────────────────

class TestProfileNames:
    def test_returns_list(self):
        names = profile_names()
        assert isinstance(names, list)

    def test_contains_three_profiles(self):
        assert len(profile_names()) == 3

    def test_expected_names_present(self):
        names = profile_names()
        assert "minimal" in names
        assert "full" in names
        assert "debug" in names

    def test_names_sorted(self):
        names = profile_names()
        assert names == sorted(names)


class TestGetProfile:
    def test_returns_profile_instance(self):
        p = get_profile("minimal")
        assert isinstance(p, Profile)

    def test_unknown_raises_value_error(self):
        with pytest.raises(ValueError, match="Unknown launch profile"):
            get_profile("turbo")

    def test_error_message_lists_valid_profiles(self):
        with pytest.raises(ValueError) as exc_info:
            get_profile("bogus")
        msg = str(exc_info.value)
        assert "debug" in msg
        assert "full" in msg
        assert "minimal" in msg

    def test_get_minimal_name(self):
        assert get_profile("minimal").name == "minimal"

    def test_get_full_name(self):
        assert get_profile("full").name == "full"

    def test_get_debug_name(self):
        assert get_profile("debug").name == "debug"


# ─── Minimal profile ──────────────────────────────────────────────────────────

class TestMinimalProfile:
    def setup_method(self):
        self.p = _minimal()

    def test_name(self):
        assert self.p.name == "minimal"

    def test_drivers_enabled(self):
        assert self.p.enable_esp32_bridge is True
        assert self.p.enable_sensors is True
        assert self.p.enable_motor_daemon is True
        assert self.p.enable_imu is True

    def test_perception_disabled(self):
        assert self.p.enable_uwb is False
        assert self.p.enable_person_detection is False
        assert self.p.enable_object_detection is False
        assert self.p.enable_depth_costmap is False
        assert self.p.enable_gimbal is False

    def test_navigation_disabled(self):
        assert self.p.enable_slam is False
        assert self.p.enable_nav2 is False
        assert self.p.enable_follower is False
        assert self.p.enable_docking is False
        assert self.p.enable_lidar_avoid is False

    def test_ui_disabled(self):
        assert self.p.enable_rosbridge is False
        assert self.p.enable_audio is False
        assert self.p.enable_social is False
        assert self.p.enable_csi_cameras is False

    def test_debug_features_disabled(self):
        assert self.p.enable_rviz is False
        assert self.p.enable_verbose_logs is False
        assert self.p.enable_bag_recorder is False

    def test_timing_zero(self):
        # Unused timings are zeroed so there are no unnecessary waits
        assert self.p.t_perception == 0.0
        assert self.p.t_navigation == 0.0
        assert self.p.t_ui == 0.0

    def test_diagnostics_enabled(self):
        assert self.p.enable_diagnostics is True


# ─── Full profile ─────────────────────────────────────────────────────────────

class TestFullProfile:
    def setup_method(self):
        self.p = _full()

    def test_name(self):
        assert self.p.name == "full"

    def test_drivers_enabled(self):
        assert self.p.enable_esp32_bridge is True
        assert self.p.enable_sensors is True
        assert self.p.enable_motor_daemon is True
        assert self.p.enable_imu is True

    def test_perception_enabled(self):
        assert self.p.enable_uwb is True
        assert self.p.enable_person_detection is True
        assert self.p.enable_object_detection is True
        assert self.p.enable_depth_costmap is True
        assert self.p.enable_gimbal is True

    def test_navigation_enabled(self):
        assert self.p.enable_slam is True
        assert self.p.enable_nav2 is True
        assert self.p.enable_follower is True
        assert self.p.enable_docking is True
        assert self.p.enable_lidar_avoid is True

    def test_ui_enabled(self):
        assert self.p.enable_rosbridge is True
        assert self.p.enable_audio is True
        assert self.p.enable_social is True
        assert self.p.enable_csi_cameras is True

    def test_diagnostics_enabled(self):
        assert self.p.enable_diagnostics is True

    def test_debug_features_disabled(self):
        assert self.p.enable_rviz is False
        assert self.p.enable_verbose_logs is False

    def test_timing_positive(self):
        assert self.p.t_drivers == 0.0
        assert self.p.t_perception > 0.0
        assert self.p.t_navigation > self.p.t_perception
        assert self.p.t_ui > self.p.t_navigation

    def test_perception_gate_8s(self):
        assert self.p.t_perception == 8.0

    def test_navigation_gate_16s(self):
        assert self.p.t_navigation == 16.0

    def test_ui_gate_22s(self):
        assert self.p.t_ui == 22.0


# ─── Debug profile ────────────────────────────────────────────────────────────

class TestDebugProfile:
    def setup_method(self):
        self.p = _debug()

    def test_name(self):
        assert self.p.name == "debug"

    def test_inherits_full_stack(self):
        full = _full()
        # All full flags must be True in debug too
        assert self.p.enable_slam == full.enable_slam
        assert self.p.enable_nav2 == full.enable_nav2
        assert self.p.enable_rosbridge == full.enable_rosbridge

    def test_rviz_enabled(self):
        assert self.p.enable_rviz is True

    def test_verbose_logs_enabled(self):
        assert self.p.enable_verbose_logs is True

    def test_bag_recorder_enabled(self):
        assert self.p.enable_bag_recorder is True

    def test_timing_slower_than_full(self):
        full = _full()
        # Debug extends gates to ensure stability
        assert self.p.t_navigation >= full.t_navigation
        assert self.p.t_ui >= full.t_ui

    def test_navigation_gate_20s(self):
        assert self.p.t_navigation == 20.0

    def test_ui_gate_26s(self):
        assert self.p.t_ui == 26.0


# ─── Profile hierarchy checks ─────────────────────────────────────────────────

class TestProfileHierarchy:
    """Minimal ⊂ Full ⊂ Debug — every flag true in minimal must be true in full/debug."""

    def _enabled_flags(self, p: Profile) -> set:
        import dataclasses
        return {
            f.name
            for f in dataclasses.fields(p)
            if f.name.startswith("enable_") and getattr(p, f.name) is True
        }

    def test_minimal_subset_of_full(self):
        minimal_flags = self._enabled_flags(_minimal())
        full_flags = self._enabled_flags(_full())
        assert minimal_flags.issubset(full_flags), (
            f"Full missing flags that Minimal has: {minimal_flags - full_flags}"
        )

    def test_full_subset_of_debug(self):
        full_flags = self._enabled_flags(_full())
        debug_flags = self._enabled_flags(_debug())
        assert full_flags.issubset(debug_flags), (
            f"Debug missing flags that Full has: {full_flags - debug_flags}"
        )

    def test_debug_has_extra_flags(self):
        full_flags = self._enabled_flags(_full())
        debug_flags = self._enabled_flags(_debug())
        extras = debug_flags - full_flags
        assert len(extras) > 0, "Debug should have at least one extra flag over Full"

    def test_debug_extras_are_debug_features(self):
        full_flags = self._enabled_flags(_full())
        debug_flags = self._enabled_flags(_debug())
        extras = debug_flags - full_flags
        for flag in extras:
            assert "rviz" in flag or "verbose" in flag or "bag" in flag or "debug" in flag, (
                f"Unexpected extra flag in debug: {flag}"
            )


# ─── to_dict ──────────────────────────────────────────────────────────────────

class TestToDict:
    def test_returns_dict(self):
        p = _minimal()
        d = p.to_dict()
        assert isinstance(d, dict)

    def test_name_in_dict(self):
        d = _minimal().to_dict()
        assert d["name"] == "minimal"

    def test_all_bool_flags_present(self):
        d = _full().to_dict()
        for key in ("enable_slam", "enable_nav2", "enable_rosbridge", "enable_rviz"):
            assert key in d, f"Missing key: {key}"

    def test_timing_fields_present(self):
        d = _full().to_dict()
        for key in ("t_drivers", "t_perception", "t_navigation", "t_ui"):
            assert key in d, f"Missing timing key: {key}"

    def test_values_are_native_types(self):
        d = _debug().to_dict()
        for v in d.values():
            assert isinstance(v, (bool, int, float, str)), (
                f"Expected native type, got {type(v)} for value {v!r}"
            )


# ─── Safety parameter defaults ────────────────────────────────────────────────

class TestSafetyDefaults:
    def test_watchdog_timeout_positive(self):
        for name in profile_names():
            p = get_profile(name)
            assert p.watchdog_timeout_s > 0, f"{name}: watchdog_timeout_s must be > 0"

    def test_max_linear_vel_bounded(self):
        for name in profile_names():
            p = get_profile(name)
            assert 0 < p.max_linear_vel <= 2.0, (
                f"{name}: max_linear_vel {p.max_linear_vel} outside safe range"
            )

    def test_follow_distance_positive(self):
        for name in profile_names():
            p = get_profile(name)
            assert p.follow_distance_m > 0, f"{name}: follow_distance_m must be > 0"

    def test_cmd_vel_deadman_positive(self):
        for name in profile_names():
            p = get_profile(name)
            assert p.cmd_vel_deadman_s > 0, f"{name}: cmd_vel_deadman_s must be > 0"


# ─── Hardware port defaults ────────────────────────────────────────────────────

class TestHardwarePortDefaults:
    def test_esp32_port_set(self):
        p = _minimal()
        assert p.esp32_port.startswith("/dev/")

    def test_uwb_ports_set(self):
        p = _full()
        assert p.uwb_port_a.startswith("/dev/")
        assert p.uwb_port_b.startswith("/dev/")

    def test_gimbal_port_set(self):
        p = _full()
        assert p.gimbal_port.startswith("/dev/")
