import ast
import pathlib
import unittest
import xml.etree.ElementTree as ET

import yaml


PACKAGE_ROOT = pathlib.Path(__file__).resolve().parents[1]
BASE_LAUNCH = PACKAGE_ROOT / "launch" / "hobot_usb_cam.launch.py"
WEB_LAUNCH = PACKAGE_ROOT / "launch" / "hobot_usb_cam_websocket.launch.py"
CAMERA_CONFIG = PACKAGE_ROOT / "config" / "usb_camera.yaml"
PACKAGE_XML = PACKAGE_ROOT / "package.xml"


def string_constants(path):
    tree = ast.parse(path.read_text(encoding="utf-8"))
    return {
        node.value
        for node in ast.walk(tree)
        if isinstance(node, ast.Constant) and isinstance(node.value, str)
    }


def call_names(path):
    tree = ast.parse(path.read_text(encoding="utf-8"))
    names = []
    for node in ast.walk(tree):
        if not isinstance(node, ast.Call):
            continue
        if isinstance(node.func, ast.Name):
            names.append(node.func.id)
        elif isinstance(node.func, ast.Attribute):
            names.append(node.func.attr)
    return names


def calls_named(path, name):
    tree = ast.parse(path.read_text(encoding="utf-8"))
    calls = []
    for node in ast.walk(tree):
        if not isinstance(node, ast.Call):
            continue
        function_name = None
        if isinstance(node.func, ast.Name):
            function_name = node.func.id
        elif isinstance(node.func, ast.Attribute):
            function_name = node.func.attr
        if function_name == name:
            calls.append(node)
    return calls


def assigned_call(path, variable_name):
    tree = ast.parse(path.read_text(encoding="utf-8"))
    for node in ast.walk(tree):
        if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
            continue
        if any(
            isinstance(target, ast.Name) and target.id == variable_name
            for target in node.targets
        ):
            return node.value
    raise AssertionError(f"No call assigned to {variable_name}")


class UnifiedLaunchConfigTest(unittest.TestCase):
    def test_yaml_defines_final_native_mjpeg_configuration(self):
        document = yaml.safe_load(CAMERA_CONFIG.read_text(encoding="utf-8"))
        params = document["hobot_usb_cam"]["ros__parameters"]
        self.assertEqual(params["video_device"], "/dev/video0")
        self.assertEqual(params["image_width"], 1280)
        self.assertEqual(params["image_height"], 720)
        self.assertEqual(params["framerate"], 30)
        self.assertEqual(params["pixel_format"], "mjpeg")
        self.assertEqual(params["io_method"], "mmap")
        self.assertFalse(params["zero_copy"])
        self.assertNotIn("camera_calibration_file_path", params)

    def test_base_launch_owns_the_only_camera_node(self):
        constants = string_constants(BASE_LAUNCH)
        self.assertIn("usb_camera.yaml", constants)
        self.assertIn("usb_camera_calibration.yaml", constants)
        self.assertIn("hobot_usb_cam", constants)
        for launch_arg in (
            "usb_frame_id",
            "usb_framerate",
            "usb_image_height",
            "usb_image_width",
            "usb_io_method",
            "usb_pixel_format",
            "usb_video_device",
            "usb_zero_copy",
        ):
            self.assertIn(launch_arg, constants)
        node_calls = calls_named(BASE_LAUNCH, "Node")
        self.assertEqual(len(node_calls), 1)
        parameters = next(
            keyword.value
            for keyword in node_calls[0].keywords
            if keyword.arg == "parameters"
        )
        parameters_tree = ast.dump(parameters)
        self.assertIn("camera_config", parameters_tree)
        self.assertIn("camera_calibration_file_path", parameters_tree)
        for launch_arg in (
            "usb_frame_id",
            "usb_framerate",
            "usb_image_height",
            "usb_image_width",
            "usb_io_method",
            "usb_pixel_format",
            "usb_video_device",
            "usb_zero_copy",
        ):
            self.assertIn(launch_arg, parameters_tree)

    def test_web_launch_only_adds_websocket(self):
        constants = string_constants(WEB_LAUNCH)
        self.assertIn("hobot_usb_cam.launch.py", constants)
        self.assertIn("websocket.launch.py", constants)
        self.assertEqual(call_names(WEB_LAUNCH).count("Node"), 0)
        self.assertEqual(len(calls_named(WEB_LAUNCH, "IncludeLaunchDescription")), 2)
        camera_include = assigned_call(WEB_LAUNCH, "camera_launch")
        self.assertIn("launch_arguments", {item.arg for item in camera_include.keywords})
        camera_include_tree = ast.dump(camera_include)
        for launch_arg in (
            "usb_zero_copy",
            "usb_image_width",
            "usb_image_height",
            "usb_pixel_format",
            "usb_video_device",
        ):
            self.assertIn(launch_arg, camera_include_tree)
        for forbidden in (
            "hobot_codec",
            "/image_mjpeg",
            "yuyv2rgb",
        ):
            self.assertNotIn(forbidden, constants)

    def test_manifest_declares_launch_and_test_dependencies(self):
        root = ET.parse(PACKAGE_XML).getroot()
        runtime_dependencies = {item.text for item in root.findall("exec_depend")}
        self.assertTrue({
            "ament_index_python",
            "launch",
            "launch_ros",
            "hobot_usb_cam",
            "hobot_shm",
            "websocket",
        }.issubset(runtime_dependencies))
        test_dependencies = {item.text for item in root.findall("test_depend")}
        self.assertIn("ament_cmake_pytest", test_dependencies)
        self.assertIn("python3-yaml", test_dependencies)


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