"""Tests for sampling profiler CLI argument parsing and functionality."""

import io
import json
import os
import subprocess
import sys
import tempfile
import unittest
from types import SimpleNamespace
from unittest import mock

try:
    import _remote_debugging  # noqa: F401
except ImportError:
    raise unittest.SkipTest(
        "Test only runs when _remote_debugging is available"
    )

from test.support import (
    force_not_colorized,
    is_emscripten,
    requires_remote_subprocess_debugging,
)

from profiling.sampling.cli import (
    FORMAT_EXTENSIONS,
    _create_collector,
    _generate_output_filename,
    _handle_output,
    main,
)
from profiling.sampling.constants import (
    PROFILING_MODE_ALL,
    PROFILING_MODE_CPU,
    PROFILING_MODE_WALL,
)
from profiling.sampling.errors import SamplingScriptNotFoundError, SamplingModuleNotFoundError, SamplingUnknownProcessError
from profiling.sampling.jsonl_collector import JsonlCollector

class TestSampleProfilerCLI(unittest.TestCase):
    def _setup_sync_mocks(self, mock_socket, mock_popen):
        """Helper to set up socket and process mocks for coordinator tests."""
        # Mock the sync socket with context manager support
        mock_sock_instance = mock.MagicMock()
        mock_sock_instance.getsockname.return_value = ("127.0.0.1", 12345)

        # Mock the connection with context manager support
        mock_conn = mock.MagicMock()
        mock_conn.recv.return_value = b"ready"
        mock_conn.__enter__.return_value = mock_conn
        mock_conn.__exit__.return_value = None

        # Mock accept() to return (connection, address) and support indexing
        mock_accept_result = mock.MagicMock()
        mock_accept_result.__getitem__.return_value = (
            mock_conn  # [0] returns the connection
        )
        mock_sock_instance.accept.return_value = mock_accept_result

        # Mock socket with context manager support
        mock_sock_instance.__enter__.return_value = mock_sock_instance
        mock_sock_instance.__exit__.return_value = None
        mock_socket.return_value = mock_sock_instance

        # Mock the subprocess
        mock_process = mock.MagicMock()
        mock_process.pid = 12345
        mock_process.poll.return_value = None
        mock_popen.return_value = mock_process
        return mock_process

    def _verify_coordinator_command(self, mock_popen, expected_target_args):
        """Helper to verify the coordinator command was called correctly."""
        args, kwargs = mock_popen.call_args
        coordinator_cmd = args[0]
        self.assertEqual(coordinator_cmd[0], sys.executable)
        self.assertEqual(coordinator_cmd[1], "-m")
        self.assertEqual(
            coordinator_cmd[2], "profiling.sampling._sync_coordinator"
        )
        self.assertEqual(coordinator_cmd[3], "12345")  # port
        # cwd is coordinator_cmd[4]
        self.assertEqual(coordinator_cmd[5:], expected_target_args)

    @unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
    @requires_remote_subprocess_debugging()
    def test_cli_module_argument_parsing(self):
        test_args = ["profiling.sampling.cli", "run", "-m", "mymodule"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch("subprocess.Popen") as mock_popen,
            mock.patch("socket.socket") as mock_socket,
            mock.patch("profiling.sampling.cli._wait_for_ready_signal"),
            mock.patch("importlib.util.find_spec", return_value=True),
        ):
            self._setup_sync_mocks(mock_socket, mock_popen)
            main()

            self._verify_coordinator_command(mock_popen, ("-m", "mymodule"))
            # Verify sample was called once (exact arguments will vary with the new API)
            mock_sample.assert_called_once()

    @unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
    @requires_remote_subprocess_debugging()
    def test_cli_module_with_arguments(self):
        test_args = [
            "profiling.sampling.cli",
            "run",
            "-m",
            "mymodule",
            "arg1",
            "arg2",
            "--flag",
        ]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch("subprocess.Popen") as mock_popen,
            mock.patch("socket.socket") as mock_socket,
            mock.patch("profiling.sampling.cli._wait_for_ready_signal"),
            mock.patch("importlib.util.find_spec", return_value=True),
        ):
            self._setup_sync_mocks(mock_socket, mock_popen)
            main()

            self._verify_coordinator_command(
                mock_popen, ("-m", "mymodule", "arg1", "arg2", "--flag")
            )
            mock_sample.assert_called_once()

    @unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
    def test_cli_script_argument_parsing(self):
        test_args = ["profiling.sampling.cli", "run", "myscript.py"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch("subprocess.Popen") as mock_popen,
            mock.patch("socket.socket") as mock_socket,
            mock.patch("profiling.sampling.cli._wait_for_ready_signal"),
            mock.patch("os.path.exists", return_value=True),
        ):
            self._setup_sync_mocks(mock_socket, mock_popen)
            main()

            self._verify_coordinator_command(mock_popen, ("myscript.py",))
            mock_sample.assert_called_once()

    @unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
    def test_cli_script_with_arguments(self):
        test_args = [
            "profiling.sampling.cli",
            "run",
            "myscript.py",
            "arg1",
            "arg2",
            "--flag",
        ]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch("subprocess.Popen") as mock_popen,
            mock.patch("socket.socket") as mock_socket,
            mock.patch("profiling.sampling.cli._wait_for_ready_signal"),
            mock.patch("os.path.exists", return_value=True),
        ):
            # Use the helper to set up mocks consistently
            mock_process = self._setup_sync_mocks(mock_socket, mock_popen)
            # Override specific behavior for this test
            mock_process.wait.side_effect = [
                subprocess.TimeoutExpired(test_args, 0.1),
                None,
            ]

            main()

            # Verify the coordinator command was called
            args, kwargs = mock_popen.call_args
            coordinator_cmd = args[0]
            self.assertEqual(coordinator_cmd[0], sys.executable)
            self.assertEqual(coordinator_cmd[1], "-m")
            self.assertEqual(
                coordinator_cmd[2], "profiling.sampling._sync_coordinator"
            )
            self.assertEqual(coordinator_cmd[3], "12345")  # port
            # cwd is coordinator_cmd[4]
            self.assertEqual(
                coordinator_cmd[5:], ("myscript.py", "arg1", "arg2", "--flag")
            )

    def test_cli_mutually_exclusive_pid_module(self):
        # In new CLI, attach and run are separate subcommands, so this test
        # verifies that mixing them causes an error
        test_args = [
            "profiling.sampling.cli",
            "attach",  # attach subcommand uses PID
            "12345",
            "-m",  # -m is only for run subcommand
            "mymodule",
        ]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("unrecognized arguments", error_msg)

    def test_cli_mutually_exclusive_pid_script(self):
        # In new CLI, you can't mix attach (PID) with run (script)
        # This would be caught by providing a PID to run subcommand
        test_args = ["profiling.sampling.cli", "run", "12345"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SamplingScriptNotFoundError) as cm,
        ):
            main()

        # Verify the error is about the non-existent script
        self.assertIn("12345", str(cm.exception))

    def test_cli_no_target_specified(self):
        # In new CLI, must specify a subcommand
        test_args = ["profiling.sampling.cli", "-d", "5"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("invalid choice", error_msg)

    @unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
    @requires_remote_subprocess_debugging()
    def test_cli_module_with_profiler_options(self):
        test_args = [
            "profiling.sampling.cli",
            "run",
            "-r",
            "1000",
            "-d",
            "30",
            "-a",
            "--sort",
            "tottime",
            "-l",
            "20",
            "-m",
            "mymodule",
        ]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch("subprocess.Popen") as mock_popen,
            mock.patch("socket.socket") as mock_socket,
            mock.patch("profiling.sampling.cli._wait_for_ready_signal"),
            mock.patch("importlib.util.find_spec", return_value=True),
        ):
            self._setup_sync_mocks(mock_socket, mock_popen)
            main()

            self._verify_coordinator_command(mock_popen, ("-m", "mymodule"))
            mock_sample.assert_called_once()

    @unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
    def test_cli_script_with_profiler_options(self):
        """Test script with various profiler options."""
        test_args = [
            "profiling.sampling.cli",
            "run",
            "-r",
            "500",
            "-d",
            "60",
            "--collapsed",
            "-o",
            "output.txt",
            "myscript.py",
            "scriptarg",
        ]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch("subprocess.Popen") as mock_popen,
            mock.patch("socket.socket") as mock_socket,
            mock.patch("profiling.sampling.cli._wait_for_ready_signal"),
            mock.patch("os.path.exists", return_value=True),
        ):
            self._setup_sync_mocks(mock_socket, mock_popen)
            main()

            self._verify_coordinator_command(
                mock_popen, ("myscript.py", "scriptarg")
            )
            # Verify profiler was called
            mock_sample.assert_called_once()

    def test_cli_empty_module_name(self):
        test_args = ["profiling.sampling.cli", "run", "-m"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("required: target", error_msg)  # argparse error for missing positional arg

    @unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
    @requires_remote_subprocess_debugging()
    def test_cli_long_module_option(self):
        test_args = [
            "profiling.sampling.cli",
            "run",
            "-m",
            "mymodule",
            "arg1",
        ]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch("subprocess.Popen") as mock_popen,
            mock.patch("socket.socket") as mock_socket,
            mock.patch("profiling.sampling.cli._wait_for_ready_signal"),
            mock.patch("importlib.util.find_spec", return_value=True),
        ):
            self._setup_sync_mocks(mock_socket, mock_popen)
            main()

            self._verify_coordinator_command(
                mock_popen, ("-m", "mymodule", "arg1")
            )

    def test_cli_complex_script_arguments(self):
        test_args = [
            "profiling.sampling.cli",
            "run",
            "script.py",
            "--input",
            "file.txt",
            "-v",
            "--output=/tmp/out",
            "positional",
        ]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
            mock.patch(
                "profiling.sampling.cli._run_with_sync"
            ) as mock_run_with_sync,
            mock.patch("os.path.exists", return_value=True),
        ):
            mock_process = mock.MagicMock()
            mock_process.pid = 12345
            mock_process.wait.side_effect = [
                subprocess.TimeoutExpired(test_args, 0.1),
                None,
            ]
            mock_process.poll.return_value = None
            mock_run_with_sync.return_value = mock_process

            main()

            mock_run_with_sync.assert_called_once_with(
                (
                    sys.executable,
                    "script.py",
                    "--input",
                    "file.txt",
                    "-v",
                    "--output=/tmp/out",
                    "positional",
                ),
                suppress_output=False
            )

    def test_cli_collapsed_format_validation(self):
        """Test that CLI properly validates incompatible options with collapsed format."""
        test_cases = [
            # Test sort option is invalid with collapsed
            (
                [
                    "profiling.sampling.cli",
                    "attach",
                    "12345",
                    "--collapsed",
                    "--sort",
                    "tottime",  # Changed from nsamples (default) to trigger validation
                ],
                "sort",
            ),
            # Test limit option is invalid with collapsed
            (
                [
                    "profiling.sampling.cli",
                    "attach",
                    "12345",
                    "--collapsed",
                    "-l",
                    "20",
                ],
                "limit",
            ),
            # Test no-summary option is invalid with collapsed
            (
                [
                    "profiling.sampling.cli",
                    "attach",
                    "12345",
                    "--collapsed",
                    "--no-summary",
                ],
                "summary",
            ),
        ]

        for test_args, expected_error_keyword in test_cases:
            with (
                mock.patch("sys.argv", test_args),
                mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
                mock.patch("profiling.sampling.cli.sample"),  # Prevent actual profiling
                self.assertRaises(SystemExit) as cm,
            ):
                main()

            self.assertEqual(cm.exception.code, 2)  # argparse error code
            error_msg = mock_stderr.getvalue()
            self.assertIn("error:", error_msg)
            self.assertIn("only valid with --pstats", error_msg)

    def test_cli_default_collapsed_filename(self):
        """Test that collapsed format gets a default filename when not specified."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--collapsed"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
        ):
            main()

            # Check that sample was called (exact filename depends on implementation)
            mock_sample.assert_called_once()

    def test_cli_custom_output_filenames(self):
        """Test custom output filenames for both formats."""
        test_cases = [
            (
                [
                    "profiling.sampling.cli",
                    "attach",
                    "12345",
                    "--pstats",
                    "-o",
                    "custom.pstats",
                ],
                "custom.pstats",
                "pstats",
            ),
            (
                [
                    "profiling.sampling.cli",
                    "attach",
                    "12345",
                    "--collapsed",
                    "-o",
                    "custom.txt",
                ],
                "custom.txt",
                "collapsed",
            ),
        ]

        for test_args, expected_filename, expected_format in test_cases:
            with (
                mock.patch("sys.argv", test_args),
                mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
                mock.patch("profiling.sampling.cli.sample") as mock_sample,
            ):
                main()

                mock_sample.assert_called_once()

    def test_cli_missing_required_arguments(self):
        """Test that CLI requires subcommand."""
        with (
            mock.patch("sys.argv", ["profiling.sampling.cli"]),
            mock.patch("sys.stderr", io.StringIO()),
        ):
            with self.assertRaises(SystemExit):
                main()

    def test_cli_mutually_exclusive_format_options(self):
        """Test that pstats and collapsed options are mutually exclusive."""
        with (
            mock.patch(
                "sys.argv",
                [
                    "profiling.sampling.cli",
                    "attach",
                    "12345",
                    "--pstats",
                    "--collapsed",
                ],
            ),
            mock.patch("sys.stderr", io.StringIO()),
        ):
            with self.assertRaises(SystemExit):
                main()

    def test_argument_parsing_basic(self):
        test_args = ["profiling.sampling.cli", "attach", "12345"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
        ):
            main()

            mock_sample.assert_called_once()

    def _run_dump_cli(
        self,
        *cli_args,
        dump_return=None,
        dump_side_effect=None,
        process_running=True,
    ):
        """Run main() for a `dump` invocation, returning (mock_dump, mock_print)."""
        argv = ["profiling.sampling.cli", "dump", *cli_args]
        dump_kwargs = {}
        if dump_side_effect is not None:
            dump_kwargs["side_effect"] = dump_side_effect
        else:
            dump_kwargs["return_value"] = [] if dump_return is None else dump_return
        with (
            mock.patch("sys.argv", argv),
            mock.patch(
                "profiling.sampling.cli._is_process_running",
                return_value=process_running,
            ),
            mock.patch("profiling.sampling.cli.dump_stack", **dump_kwargs) as mock_dump_stack,
            mock.patch("profiling.sampling.cli.print_stack_dump") as mock_print_stack_dump,
        ):
            main()
        return mock_dump_stack, mock_print_stack_dump

    def _run_dump_cli_expecting_exit(self, *cli_args, capture="stderr"):
        """Run main() for a `dump` invocation expected to SystemExit, return (cm, captured_text)."""
        argv = ["profiling.sampling.cli", "dump", *cli_args]
        buf = io.StringIO()
        stream = "sys.stderr" if capture == "stderr" else "sys.stdout"
        with (
            mock.patch("sys.argv", argv),
            mock.patch(stream, buf),
            self.assertRaises(SystemExit) as cm,
        ):
            main()
        return cm, buf.getvalue()

    def test_cli_dump_subcommand(self):
        stack_frames = [mock.sentinel.stack_frames]
        mock_dump_stack, mock_print_stack_dump = self._run_dump_cli(
            "12345", dump_return=stack_frames
        )

        mock_dump_stack.assert_called_once()
        self.assertEqual(mock_dump_stack.call_args.args, (12345,))
        call_kwargs = mock_dump_stack.call_args.kwargs
        self.assertFalse(call_kwargs["all_threads"])
        self.assertIsNone(call_kwargs["async_aware"])
        self.assertFalse(call_kwargs["native"])
        self.assertTrue(call_kwargs["gc"])
        self.assertFalse(call_kwargs["opcodes"])
        self.assertFalse(call_kwargs["blocking"])
        self.assertEqual(call_kwargs["mode"], PROFILING_MODE_ALL)
        mock_print_stack_dump.assert_called_once_with(stack_frames, pid=12345)

    def test_cli_dump_subcommand_options(self):
        mock_dump_stack, _ = self._run_dump_cli(
            "-a", "--native", "--no-gc", "--opcodes", "--blocking", "12345"
        )

        call_kwargs = mock_dump_stack.call_args.kwargs
        self.assertTrue(call_kwargs["all_threads"])
        self.assertTrue(call_kwargs["native"])
        self.assertFalse(call_kwargs["gc"])
        self.assertTrue(call_kwargs["opcodes"])
        self.assertTrue(call_kwargs["blocking"])
        self.assertEqual(call_kwargs["mode"], PROFILING_MODE_ALL)

    def test_cli_dump_rejects_mode_option(self):
        cm, stderr = self._run_dump_cli_expecting_exit("12345", "--mode", "cpu")
        self.assertEqual(cm.exception.code, 2)
        self.assertIn("unrecognized arguments: --mode", stderr)

    def test_cli_dump_async_aware_defaults_to_all(self):
        mock_dump_stack, _ = self._run_dump_cli("--async-aware", "12345")
        call_kwargs = mock_dump_stack.call_args.kwargs
        self.assertEqual(call_kwargs["async_aware"], "all")
        self.assertEqual(call_kwargs["mode"], PROFILING_MODE_WALL)

    def test_cli_dump_async_mode_all_is_forwarded(self):
        mock_dump_stack, _ = self._run_dump_cli(
            "--async-aware", "--async-mode", "all", "12345"
        )
        call_kwargs = mock_dump_stack.call_args.kwargs
        self.assertEqual(call_kwargs["async_aware"], "all")
        self.assertEqual(call_kwargs["mode"], PROFILING_MODE_WALL)

    def test_cli_dump_async_mode_running_is_forwarded(self):
        mock_dump_stack, _ = self._run_dump_cli(
            "--async-aware", "--async-mode", "running", "12345"
        )
        call_kwargs = mock_dump_stack.call_args.kwargs
        self.assertEqual(call_kwargs["async_aware"], "running")
        self.assertEqual(call_kwargs["mode"], PROFILING_MODE_WALL)

    def test_cli_dump_async_mode_requires_async_aware(self):
        cm, stderr = self._run_dump_cli_expecting_exit(
            "--async-mode", "running", "12345"
        )
        self.assertEqual(cm.exception.code, 2)
        self.assertIn("--async-mode requires --async-aware", stderr)

    def test_cli_dump_rejects_async_aware_with_all_threads(self):
        cm, stderr = self._run_dump_cli_expecting_exit(
            "--async-aware", "-a", "12345"
        )
        self.assertEqual(cm.exception.code, 2)
        self.assertIn("--all-threads", stderr)
        self.assertIn("incompatible with --async-aware", stderr)

    def test_cli_dump_rejects_async_aware_with_native(self):
        cm, stderr = self._run_dump_cli_expecting_exit(
            "--async-aware", "--native", "12345"
        )
        self.assertEqual(cm.exception.code, 2)
        self.assertIn("--native", stderr)
        self.assertIn("incompatible with --async-aware", stderr)

    def test_cli_dump_rejects_async_aware_with_no_gc(self):
        cm, stderr = self._run_dump_cli_expecting_exit(
            "--async-aware", "--no-gc", "12345"
        )
        self.assertEqual(cm.exception.code, 2)
        self.assertIn("--no-gc", stderr)
        self.assertIn("incompatible with --async-aware", stderr)

    def test_cli_dump_unknown_process(self):
        with self.assertRaises(SamplingUnknownProcessError):
            self._run_dump_cli("12345", process_running=False)

    def test_cli_dump_process_exits_before_snapshot(self):
        with self.assertRaises(SystemExit) as cm:
            self._run_dump_cli("12345", dump_side_effect=ProcessLookupError)
        self.assertIn(
            "No stack dump collected - process 12345 exited",
            str(cm.exception.code),
        )

    @force_not_colorized
    def test_cli_dump_help_lists_dump_options_without_mode(self):
        cm, stdout = self._run_dump_cli_expecting_exit("--help", capture="stdout")
        self.assertEqual(cm.exception.code, 0)
        self.assertIn("--async-mode {running,all}", stdout)
        self.assertIn("--opcodes", stdout)
        self.assertNotIn("--mode {wall,cpu,gil,exception}", stdout)

    def test_sort_options(self):
        sort_options = [
            ("nsamples", 0),
            ("tottime", 1),
            ("cumtime", 2),
            ("sample-pct", 3),
            ("cumul-pct", 4),
            ("name", -1),
        ]

        for option, expected_sort_value in sort_options:
            test_args = ["profiling.sampling.cli", "attach", "12345", "--sort", option]

            with (
                mock.patch("sys.argv", test_args),
                mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
                mock.patch("profiling.sampling.cli.sample") as mock_sample,
            ):
                main()

                mock_sample.assert_called_once()
                mock_sample.reset_mock()

    def test_async_aware_flag_defaults_to_running(self):
        """Test --async-aware flag enables async profiling with default 'running' mode."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
        ):
            main()

            mock_sample.assert_called_once()
            # Verify async_aware was passed with default "running" mode
            call_kwargs = mock_sample.call_args[1]
            self.assertEqual(call_kwargs.get("async_aware"), "running")

    def test_handle_output_browser_not_opened_when_export_fails(self):
        for format_type in ("flamegraph", "diff_flamegraph", "heatmap"):
            with self.subTest(format=format_type):
                collector = mock.MagicMock()
                collector.export.return_value = False
                args = SimpleNamespace(
                    format=format_type,
                    outfile="profile.html",
                    browser=True,
                )

                with (
                    mock.patch("profiling.sampling.cli.os.path.isdir", return_value=False),
                    mock.patch("profiling.sampling.cli._open_in_browser") as mock_open,
                ):
                    _handle_output(collector, args, pid=12345, mode=0)

                collector.export.assert_called_once_with("profile.html")
                mock_open.assert_not_called()

    def test_async_aware_with_async_mode_all(self):
        """Test --async-aware with --async-mode all."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--async-mode", "all"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
        ):
            main()

            mock_sample.assert_called_once()
            call_kwargs = mock_sample.call_args[1]
            self.assertEqual(call_kwargs.get("async_aware"), "all")

    def test_async_aware_default_is_none(self):
        """Test async_aware defaults to None when --async-aware not specified."""
        test_args = ["profiling.sampling.cli", "attach", "12345"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("profiling.sampling.cli._is_process_running", return_value=True),
            mock.patch("profiling.sampling.cli.sample") as mock_sample,
        ):
            main()

            mock_sample.assert_called_once()
            call_kwargs = mock_sample.call_args[1]
            self.assertIsNone(call_kwargs.get("async_aware"))

    def test_async_mode_invalid_choice(self):
        """Test --async-mode with invalid choice raises error."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--async-mode", "invalid"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()),
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error

    def test_async_mode_requires_async_aware(self):
        """Test --async-mode without --async-aware raises error."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-mode", "all"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("--async-mode requires --async-aware", error_msg)

    def test_async_aware_incompatible_with_native(self):
        """Test --async-aware is incompatible with --native."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--native"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("--native", error_msg)
        self.assertIn("incompatible with --async-aware", error_msg)

    def test_async_aware_incompatible_with_no_gc(self):
        """Test --async-aware is incompatible with --no-gc."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--no-gc"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("--no-gc", error_msg)
        self.assertIn("incompatible with --async-aware", error_msg)

    def test_async_aware_incompatible_with_both_native_and_no_gc(self):
        """Test --async-aware is incompatible with both --native and --no-gc."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--native", "--no-gc"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("--native", error_msg)
        self.assertIn("--no-gc", error_msg)
        self.assertIn("incompatible with --async-aware", error_msg)

    def test_async_aware_incompatible_with_mode(self):
        """Test --async-aware is incompatible with --mode (non-wall)."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--mode", "cpu"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("--mode=cpu", error_msg)
        self.assertIn("incompatible with --async-aware", error_msg)

    def test_async_aware_incompatible_with_all_threads(self):
        """Test --async-aware is incompatible with --all-threads."""
        test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--all-threads"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("--all-threads", error_msg)
        self.assertIn("incompatible with --async-aware", error_msg)

    def test_async_aware_incompatible_with_binary(self):
        """Test --async-aware is incompatible with --binary."""
        test_args = ["profiling.sampling.cli", "attach", "12345",
                     "--async-aware", "--binary"]

        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)  # argparse error
        error_msg = mock_stderr.getvalue()
        self.assertIn("--binary", error_msg)
        self.assertIn("incompatible with --async-aware", error_msg)

    @unittest.skipIf(is_emscripten, "subprocess not available")
    def test_run_nonexistent_script_exits_cleanly(self):
        """Test that running a non-existent script exits with a clean error."""
        with mock.patch("sys.argv", ["profiling.sampling.cli", "run", "/nonexistent/script.py"]):
            with self.assertRaisesRegex(SamplingScriptNotFoundError, "Script '[\\w/.]+' not found."):
                main()

    @unittest.skipIf(is_emscripten, "subprocess not available")
    def test_run_nonexistent_module_exits_cleanly(self):
        """Test that running a non-existent module exits with a clean error."""
        with mock.patch("sys.argv", ["profiling.sampling.cli", "run", "-m", "nonexistent_module_xyz"]):
            with self.assertRaisesRegex(SamplingModuleNotFoundError, "Module '[\\w/.]+' not found."):
                main()

    @unittest.skipIf(is_emscripten, "subprocess not available")
    def test_cli_attach_nonexistent_pid(self):
        fake_pid = "99999"
        with mock.patch("sys.argv", ["profiling.sampling.cli", "attach", fake_pid]):
            with self.assertRaises(SamplingUnknownProcessError) as cm:
                main()

            self.assertIn(fake_pid, str(cm.exception))

    def test_cli_replay_rejects_non_binary_profile(self):
        with tempfile.TemporaryDirectory() as tempdir:
            profile = os.path.join(tempdir, "output.prof")
            with open(profile, "wb") as file:
                file.write(b"not a binary sampling profile")

            with mock.patch("sys.argv", ["profiling.sampling.cli", "replay", profile]):
                with self.assertRaises(SystemExit) as cm:
                    main()

        error = str(cm.exception)
        self.assertIn("not a binary sampling profile", error)
        self.assertIn("--binary", error)

    def test_cli_replay_reader_errors_exit_cleanly(self):
        with tempfile.TemporaryDirectory() as tempdir:
            profile = os.path.join(tempdir, "output.bin")
            with open(profile, "wb") as file:
                file.write(b"HCAT" + (b"\0" * 60))

            with (
                mock.patch("sys.argv", ["profiling.sampling.cli", "replay", profile]),
                mock.patch(
                    "profiling.sampling.cli.BinaryReader",
                    side_effect=ValueError("Unsupported format version 2"),
                ),
            ):
                with self.assertRaises(SystemExit) as cm:
                    main()

        self.assertEqual(
            str(cm.exception),
            "Error: Unsupported format version 2",
        )

    def test_cli_jsonl_format_mutually_exclusive_with_pstats(self):
        """--jsonl and --pstats cannot be combined (mutually exclusive group)."""
        with (
            mock.patch(
                "sys.argv",
                [
                    "profiling.sampling.cli",
                    "attach",
                    "12345",
                    "--jsonl",
                    "--pstats",
                ],
            ),
            mock.patch("sys.stderr", io.StringIO()),
        ):
            with self.assertRaises(SystemExit):
                main()

    def test_cli_jsonl_extension_in_format_extensions(self):
        """FORMAT_EXTENSIONS maps 'jsonl' -> 'jsonl' so default filenames work."""
        self.assertEqual(FORMAT_EXTENSIONS["jsonl"], "jsonl")
        self.assertEqual(_generate_output_filename("jsonl", 12345), "jsonl_12345.jsonl")

    def test_cli_jsonl_create_collector_propagates_mode(self):
        """_create_collector('jsonl', ..., mode=X) lands X in the meta record."""
        collector = _create_collector(
            "jsonl",
            sample_interval_usec=1000,
            skip_idle=False,
            mode=PROFILING_MODE_CPU,
        )
        self.assertIsInstance(collector, JsonlCollector)

        with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False) as f:
            jsonl_path = f.name
        self.addCleanup(os.unlink, jsonl_path)
        collector.export(jsonl_path)
        with open(jsonl_path, "r", encoding="utf-8") as f:
            records = [json.loads(line) for line in f]
        meta = next(r for r in records if r["type"] == "meta")
        self.assertEqual(meta["mode"], "cpu")

    def test_cli_jsonl_rejects_opcodes_combination(self):
        """--opcodes is incompatible with --jsonl per opcodes_compatible_formats."""
        test_args = [
            "profiling.sampling.cli",
            "attach",
            "12345",
            "--jsonl",
            "--opcodes",
        ]
        with (
            mock.patch("sys.argv", test_args),
            mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
            mock.patch("profiling.sampling.cli.sample"),
            self.assertRaises(SystemExit) as cm,
        ):
            main()

        self.assertEqual(cm.exception.code, 2)
        self.assertIn("--opcodes", mock_stderr.getvalue())
