"""Test Jac cli module."""

import contextlib
import inspect
import io
import os
import re
import subprocess
import sys
import tempfile
import traceback
import unittest
from jaclang.cli import cli
from jaclang.cli.cmdreg import cmd_registry, extract_param_descriptions
from jaclang.runtimelib.builtin import printgraph
from jaclang.utils.test import TestCase


class JacCliTests(TestCase):
    """Test pass module."""

    def setUp(self) -> None:
        """Set up test."""
        return super().setUp()

    def test_jac_cli_run(self) -> None:
        """Basic test for pass."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        # Execute the function
        cli.run(self.fixture_abs_path("hello.jac"))

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()

        self.assertIn("Hello World!", stdout_value)

    def test_jac_cli_run_python_file(self) -> None:
        """Test running Python files with jac run command."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.run(self.fixture_abs_path("python_run_test.py"))

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()

        self.assertIn("Hello from Python!", stdout_value)
        self.assertIn("This is a test Python file.", stdout_value)
        self.assertIn("Result: 42", stdout_value)
        self.assertIn("Python execution completed.", stdout_value)
        self.assertIn("10", stdout_value)

    def test_jac_run_py_fstr(self) -> None:
        """Test running Python files with jac run command."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.run(self.fixture_abs_path("pyfunc_fstr.py"))

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()

        self.assertIn("Hello Peter", stdout_value)
        self.assertIn("Hello Peter Peter", stdout_value)
        self.assertIn("Peter squared is Peter Peter", stdout_value)
        self.assertIn("PETER!  wrong poem", stdout_value)
        self.assertIn(
            "Hello Peter , yoo mother is Mary. Myself, I am Peter.", stdout_value
        )
        self.assertIn("Left aligned: Apple | Price: 1.23", stdout_value)
        self.assertIn("name = Peter 🤔", stdout_value)

    def test_jac_run_py_fmt(self) -> None:
        """Test running Python files with jac run command."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.run(self.fixture_abs_path("pyfunc_fmt.py"))

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()

        self.assertIn("One", stdout_value)
        self.assertIn("Two", stdout_value)
        self.assertIn("Three", stdout_value)
        self.assertIn("baz", stdout_value)
        self.assertIn("Processing...", stdout_value)
        self.assertIn("Four", stdout_value)
        self.assertIn("The End.", stdout_value)

    def test_jac_run_pyfunc_kwesc(self) -> None:
        """Test running Python files with jac run command."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.run(self.fixture_abs_path("pyfunc_kwesc.py"))

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        out = stdout_value.split("\n")
        self.assertIn("89", out[0])
        self.assertIn("(13, (), {'a': 1, 'b': 2})", out[1])
        self.assertIn("Functions: [{'name': 'replace_lines'", out[2])
        self.assertIn("Dict: 90", out[3])

    def test_jac_cli_alert_based_err(self) -> None:
        """Basic test for pass."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        sys.stderr = captured_output

        try:
            cli.enter(self.fixture_abs_path("err2.jac"), entrypoint="speak", args=[])
        except Exception as e:
            print(f"Error: {e}")

        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__
        stdout_value = captured_output.getvalue()
        # print(stdout_value)
        self.assertIn("Error", stdout_value)

    def test_jac_cli_alert_based_runtime_err(self) -> None:
        """Test runtime errors with internal calls collapsed (default behavior)."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        sys.stderr = captured_output

        with self.assertRaises(SystemExit) as cm:
            cli.run(self.fixture_abs_path("err_runtime.jac"))

        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__

        # Verify exit code is 1
        self.assertEqual(cm.exception.code, 1)

        output = captured_output.getvalue()

        expected_stderr_values = (
            "Error: list index out of range",
            "    print(some_list[invalid_index]);",
            "          ^^^^^^^^^^^^^^^^^^^^^^^^",
            "  at bar() ",
            "  at foo() ",
            "  at <module> ",
            "... [internal runtime calls]",
        )
        for exp in expected_stderr_values:
            self.assertIn(exp, output)

        # Ensure internal runtime calls are collapsed and NOT shown individually
        internal_call_patterns = (
            "meta_importer.py",
            "machine.py",
            "/jaclang/vendor/",
            "pluggy",
            "_multicall",
            "_hookexec",
        )
        for pattern in internal_call_patterns:
            self.assertNotIn(pattern, output)

    def test_jac_cli_runtime_err_with_internal_stack(self) -> None:
        """Test runtime errors with internal calls shown when setting enabled."""
        from jaclang.settings import settings

        # Save original setting
        original_setting = settings.show_internal_stack_errs

        try:
            # Enable internal stack traces
            settings.show_internal_stack_errs = True

            captured_output = io.StringIO()
            sys.stdout = captured_output
            sys.stderr = captured_output

            with self.assertRaises(SystemExit) as cm:
                cli.run(self.fixture_abs_path("err_runtime.jac"))

            sys.stdout = sys.__stdout__
            sys.stderr = sys.__stderr__

            # Verify exit code is 1
            self.assertEqual(cm.exception.code, 1)

            output = captured_output.getvalue()

            # User frames should still be present
            expected_values = (
                "Error: list index out of range",
                "  at bar() ",
                "  at foo() ",
                "  at <module> ",
            )
            for exp in expected_values:
                self.assertIn(exp, output)

            # Internal runtime calls should be shown (not collapsed)
            internal_call_patterns = (
                "meta_importer.py",
                "machine.py",
            )
            for pattern in internal_call_patterns:
                self.assertIn(pattern, output)

            # The collapse marker should NOT appear
            self.assertNotIn("... [internal runtime calls]", output)

        finally:
            # Restore original setting
            settings.show_internal_stack_errs = original_setting

    def test_jac_impl_err(self) -> None:
        """Basic test for pass."""
        if "jaclang.tests.fixtures.err" in sys.modules:
            del sys.modules["jaclang.tests.fixtures.err"]
        captured_output = io.StringIO()
        sys.stdout = captured_output
        sys.stderr = captured_output

        try:
            cli.enter(self.fixture_abs_path("err.jac"), entrypoint="speak", args=[])
        except Exception:
            traceback.print_exc()

        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__
        stdout_value = captured_output.getvalue()
        # print(stdout_value)
        path_to_file = self.fixture_abs_path("err.impl.jac")
        self.assertIn(f'"{path_to_file}", line 2', stdout_value)

    def test_param_name_diff(self) -> None:
        """Test when parameter name from definitinon and declaration are mismatched."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        sys.stderr = captured_output
        with contextlib.suppress(Exception):
            cli.run(self.fixture_abs_path("decl_defn_param_name.jac"))
        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__

        expected_stdout_values = (
            "short_name = 42",
            "p1 = 64 , p2 = foobar",
        )
        output = captured_output.getvalue()
        for exp in expected_stdout_values:
            self.assertIn(exp, output)

    def test_jac_test_err(self) -> None:
        """Basic test for pass."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        sys.stderr = captured_output
        cli.test(self.fixture_abs_path("baddy.jac"))
        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__
        stdout_value = captured_output.getvalue()
        path_to_file = self.fixture_abs_path("baddy.test.jac")
        self.assertIn(f'"{path_to_file}", line 2,', stdout_value)

    def test_jac_ast_tool_pass_template(self) -> None:
        """Basic test for pass."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.tool("pass_template")

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn("Sub objects.", stdout_value)
        self.assertGreater(stdout_value.count("def exit_"), 10)

    def test_ast_print(self) -> None:
        """Testing for print AstTool."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.tool("ir", ["ast", f"{self.fixture_abs_path('hello.jac')}"])

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn("+-- Token", stdout_value)

    @unittest.skip("Skipping builtins loading test")
    def test_builtins_loading(self) -> None:
        """Testing for print AstTool."""
        from jaclang.settings import settings

        settings.ast_symbol_info_detailed = True
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.tool("ir", ["ast", f"{self.fixture_abs_path('builtins_test.jac')}"])

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        settings.ast_symbol_info_detailed = False

        self.assertRegex(
            stdout_value,
            r"2\:8 \- 2\:12.*BuiltinType - list - .*SymbolPath: builtins.list",
        )
        self.assertRegex(
            stdout_value,
            r"15\:5 \- 15\:8.*Name - dir - .*SymbolPath: builtins.dir",
        )
        self.assertRegex(
            stdout_value,
            r"13\:12 \- 13\:18.*Name - append - .*SymbolPath: builtins.list.append",
        )

    def test_ast_printgraph(self) -> None:
        """Testing for print AstTool."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.tool("ir", ["ast.", f"{self.fixture_abs_path('hello.jac')}"])

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn(
            '[label="MultiString" shape="oval" style="filled" fillcolor="#fccca4"]',
            stdout_value,
        )

    def test_cfg_printgraph(self) -> None:
        """Testing for print CFG."""
        captured_output = io.StringIO()
        sys.stdout = captured_output

        cli.tool("ir", ["cfg.", f"{self.fixture_abs_path('hello.jac')}"])

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        correct_graph = (
            "digraph G {\n"
            '  0 [label="BB0\\n\\nprint ( \\"im still here\\" ) ;", shape=box];\n'
            '  1 [label="BB1\\n\\"Hello World!\\" |> print ;", shape=box];\n'
            "}\n\n"
        )

        self.assertEqual(
            correct_graph,
            stdout_value,
        )

    def test_del_clean(self) -> None:
        """Testing for print AstTool."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        cli.check(f"{self.fixture_abs_path('del_clean.jac')}")
        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn("Errors: 0, Warnings: 0", stdout_value)

    def test_build_and_run(self) -> None:
        """Testing for print AstTool."""
        if os.path.exists(f"{self.fixture_abs_path('needs_import.jir')}"):
            os.remove(f"{self.fixture_abs_path('needs_import.jir')}")
        captured_output = io.StringIO()
        sys.stdout = captured_output
        cli.build(f"{self.fixture_abs_path('needs_import.jac')}")
        cli.run(f"{self.fixture_abs_path('needs_import.jir')}")
        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn("Errors: 0, Warnings: 0", stdout_value)
        self.assertIn("<module 'pyfunc' from", stdout_value)

    def test_run_test(self) -> None:
        """Basic test for pass."""
        process = subprocess.Popen(
            ["jac", "test", f"{self.fixture_abs_path('run_test.jac')}", "-m 2"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertIn("Ran 3 tests", stderr)
        self.assertIn("FAILED (failures=2)", stderr)
        self.assertIn("F.F", stderr)

        process = subprocess.Popen(
            [
                "jac",
                "test",
                "-d" + f"{self.fixture_abs_path('../../../')}",
                "-f" + "circle*",
                "-x",
            ],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertIn("circle", stdout)
        self.assertNotIn("circle_purfe.test", stdout)
        self.assertNotIn("circle_pure.impl", stdout)

        process = subprocess.Popen(
            ["jac", "test", "-f" + "*run_test.jac", "-m 3"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertIn("...F", stderr)
        self.assertIn("F.F", stderr)

    def test_run_specific_test_only(self) -> None:
        """Test a specific test case."""
        process = subprocess.Popen(
            [
                "jac",
                "test",
                "-t",
                "from_2_to_10",
                self.fixture_abs_path("jactest_main.jac"),
            ],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertIn("Ran 1 test", stderr)
        self.assertIn("Testing fibonacci numbers from 2 to 10.", stdout)
        self.assertNotIn("Testing first 2 fibonacci numbers.", stdout)
        self.assertNotIn("This test should not run after import.", stdout)

    def test_graph_coverage(self) -> None:
        """Test for coverage of graph cmd."""
        graph_params = set(inspect.signature(cli.dot).parameters.keys())
        printgraph_params = set(inspect.signature(printgraph).parameters.keys())
        printgraph_params = printgraph_params - {
            "node",
            "file",
            "edge_type",
            "format",
        }
        printgraph_params.update({"initial", "saveto", "connection", "session"})
        self.assertTrue(printgraph_params.issubset(graph_params))
        self.assertEqual(len(printgraph_params) + 2, len(graph_params))

    def test_graph(self) -> None:
        """Test for graph CLI cmd."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        cli.dot(f"{self.examples_abs_path('reference/connect_expressions_(osp).jac')}")
        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        if os.path.exists("connect_expressions_(osp).dot"):
            os.remove("connect_expressions_(osp).dot")
        self.assertIn(">>> Graph content saved to", stdout_value)
        self.assertIn("connect_expressions_(osp).dot\n", stdout_value)

    def test_py_to_jac(self) -> None:
        """Test for graph CLI cmd."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        cli.py2jac(f"{self.fixture_abs_path('../../tests/fixtures/pyfunc.py')}")
        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn("def my_print(x: object) -> None", stdout_value)
        self.assertIn("class MyClass {", stdout_value)
        self.assertIn('"""Print function."""', stdout_value)

    def test_lambda_arg_annotation(self) -> None:
        """Test for lambda argument annotation."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        cli.jac2py(
            f"{self.fixture_abs_path('../../tests/fixtures/lambda_arg_annotation.jac')}"
        )
        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn("x = lambda a, b: b + a", stdout_value)
        self.assertIn("y = lambda: 567", stdout_value)
        self.assertIn("f = lambda x: 'even' if x % 2 == 0 else 'odd'", stdout_value)

    def test_lambda_self(self) -> None:
        """Test for lambda argument annotation."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        cli.jac2py(f"{self.fixture_abs_path('../../tests/fixtures/lambda_self.jac')}")
        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue()
        self.assertIn("def travel(self, here: City) -> None:", stdout_value)
        self.assertIn("def foo(a: int) -> None:", stdout_value)
        self.assertIn("x = lambda a, b: b + a", stdout_value)
        self.assertIn("def visit_city(self, c: City) -> None:", stdout_value)
        self.assertIn(
            "sorted(users, key=lambda x: x['email'], reverse=True)", stdout_value
        )

    def test_param_arg(self) -> None:
        """Test for lambda argument annotation."""
        captured_output = io.StringIO()
        sys.stdout = captured_output
        from jaclang.compiler.program import JacProgram

        filename = self.fixture_abs_path(
            "../../tests/fixtures/params/test_complex_params.jac"
        )
        cli.jac2py(
            f"{self.fixture_abs_path('../../tests/fixtures/params/test_complex_params.jac')}"
        )
        py_code = JacProgram().compile(file_path=filename).gen.py

        # Create temporary Python file
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".py", delete=False
        ) as temp_file:
            temp_file.write(py_code)
            py_file_path = temp_file.name

        try:
            jac_code = (
                JacProgram().compile(use_str=py_code, file_path=py_file_path).unparse()
            )
            # Create temporary Jac file
            with tempfile.NamedTemporaryFile(
                mode="w", suffix=".jac", delete=False
            ) as temp_file:
                temp_file.write(jac_code)
                jac_file_path = temp_file.name
            cli.run(jac_file_path)
        finally:
            os.remove(py_file_path)
            os.remove(jac_file_path)

        sys.stdout = sys.__stdout__
        stdout_value = captured_output.getvalue().split("\n")
        self.assertEqual("ULTIMATE_MIN: 1|def|2.5|0|test|100|0", stdout_value[-7])
        self.assertEqual("ULTIMATE_FULL: 1|custom|3.14|3|req|200|1", stdout_value[-6])
        self.assertEqual("SEPARATORS: 42", stdout_value[-5])
        self.assertEqual("EDGE_MIX: 1-test-2-True-1", stdout_value[-4])
        self.assertEqual("RECURSIVE: 7 11", stdout_value[-3])
        self.assertEqual(
            "VALIDATION: x:1,y:2.5,z:10,args:1,w:True,kwargs:1", stdout_value[-2]
        )

    def test_caching_issue(self) -> None:
        """Test for Caching Issue."""
        test_file = self.fixture_abs_path("test_caching_issue.jac")
        test_cases = [(10, True), (11, False)]
        for x, is_passed in test_cases:
            with open(test_file, "w") as f:
                f.write(
                    f"""
                test mytest{{
                    assert 10 == {x};
                }}
                """
                )
            process = subprocess.Popen(
                ["jac", "test", test_file],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
            stdout, stderr = process.communicate()
            if is_passed:
                self.assertIn("Passed successfully.", stdout)
                self.assertIn(".", stderr)
            else:
                self.assertNotIn("Passed successfully.", stdout)
                self.assertIn("F", stderr)
        os.remove(test_file)

    def test_cli_docstring_parameters(self) -> None:
        """Test that all CLI command parameters are documented in their docstrings."""
        # Get all registered CLI commands
        commands = {}
        for name, _ in cmd_registry.registry.items():
            # Skip commands that might be registered from outside cli.py
            if hasattr(cli, name):
                commands[name] = getattr(cli, name)

        missing_params = {}

        for cmd_name, cmd_func in commands.items():
            # Get function parameters from signature
            signature_params = set(inspect.signature(cmd_func).parameters.keys())

            # Parse docstring to extract documented parameters
            docstring = cmd_func.__doc__ or ""

            # Check if the docstring has an Args section
            args_match = re.search(r"Args:(.*?)(?:\n\n|\Z)", docstring, re.DOTALL)
            if not args_match:
                missing_params[cmd_name] = list(signature_params)
                continue

            args_section = args_match.group(1)

            # Extract parameter names from the Args section
            # Looking for patterns like "param_name: Description" or "param_name (type): Description"
            doc_params = set()
            for line in args_section.strip().split("\n"):
                line = line.strip()
                if not line:
                    continue

                # Match parameter name at the beginning of the line
                param_match = re.match(r"\s*([a-zA-Z0-9_]+)(?:\s*\([^)]*\))?:\s*", line)
                if param_match:
                    doc_params.add(param_match.group(1))

            # Find parameters that are in the signature but not in the docstring
            undocumented_params = signature_params - doc_params
            if undocumented_params:
                missing_params[cmd_name] = list(undocumented_params)

        # Assert that there are no missing parameters
        self.assertEqual(
            missing_params,
            {},
            f"The following CLI commands have undocumented parameters: {missing_params}",
        )

    def test_cli_help_uses_docstring_descriptions(self) -> None:
        """Test that CLI help text uses parameter descriptions from docstrings."""
        # Get a command with well-documented parameters
        test_commands = ["run", "dot", "test"]

        for cmd_name in test_commands:
            # Skip if command doesn't exist
            if not hasattr(cli, cmd_name):
                continue

            cmd_func = getattr(cli, cmd_name)
            docstring = cmd_func.__doc__ or ""

            # Extract parameter descriptions from docstring
            docstring_param_descriptions = extract_param_descriptions(docstring)

            # Skip if no parameters are documented
            if not docstring_param_descriptions:
                continue

            # Get help text for the command
            captured_output = io.StringIO()
            sys.stdout = captured_output

            # Use subprocess to get the help text to ensure we're testing the actual CLI output
            process = subprocess.Popen(
                ["jac", cmd_name, "--help"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
            help_text, _ = process.communicate()

            sys.stdout = sys.__stdout__

            # Check that each documented parameter description appears in the help text
            for param_name, description in docstring_param_descriptions.items():
                # The description might be truncated or formatted differently in the help text,
                # so we'll just check for the first few words
                description_start = description.split()[:3]
                description_pattern = r"\s+".join(
                    re.escape(word) for word in description_start
                )
                # Check if the description appears in the help text
                self.assertRegex(
                    help_text,
                    description_pattern,
                    f"Parameter description for '{param_name}' not found in help text for '{cmd_name}'",
                )

    def test_run_jac_name_py(self) -> None:
        """Test a specific test case."""
        process = subprocess.Popen(
            [
                "jac",
                "run",
                self.fixture_abs_path("py_run.py"),
            ],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertIn("Hello, World!", stdout)
        self.assertIn("Sum: 8", stdout)

    def test_jac_run_py_bugs(self) -> None:
        """Test jac run python files."""
        process = subprocess.Popen(
            [
                "jac",
                "run",
                self.fixture_abs_path("jac_run_py_bugs.py"),
            ],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertIn("Hello, my name is Alice and I am 30 years old.", stdout)
        self.assertIn("MyModule initialized!", stdout)

    def test_cli_defaults_to_run_with_file(self) -> None:
        """jac myfile.jac should behave like jac run myfile.jac."""
        process = subprocess.Popen(
            [
                "jac",
                self.fixture_abs_path("hello.jac"),
            ],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertIn("Hello World!", stdout)

    def test_cli_error_exit_codes(self) -> None:
        """Test that CLI commands return non-zero exit codes on errors."""
        # Test run command with syntax error
        process = subprocess.Popen(
            ["jac", "run", self.fixture_abs_path("err2.jac")],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertEqual(
            process.returncode, 1, "run command should exit with code 1 on syntax error"
        )
        self.assertIn("Error", stderr)

        # Test build command with syntax error
        process = subprocess.Popen(
            ["jac", "build", self.fixture_abs_path("err2.jac")],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertEqual(
            process.returncode,
            1,
            "build command should exit with code 1 on compilation error",
        )

        # Test check command with syntax error
        process = subprocess.Popen(
            ["jac", "check", self.fixture_abs_path("err2.jac")],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertEqual(
            process.returncode,
            1,
            "check command should exit with code 1 on type check error",
        )

        # Test format command with non-existent file
        process = subprocess.Popen(
            ["jac", "format", "/nonexistent.jac"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertEqual(
            process.returncode,
            1,
            "format command should exit with code 1 on missing file",
        )
        self.assertIn("does not exist", stderr)

        # Test check command with invalid file type
        process = subprocess.Popen(
            ["jac", "check", "/nonexistent.txt"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertEqual(
            process.returncode,
            1,
            "check command should exit with code 1 on invalid file type",
        )
        self.assertIn("Not a .jac file", stderr)

        # Test tool command with non-existent tool
        process = subprocess.Popen(
            ["jac", "tool", "nonexistent_tool"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertEqual(
            process.returncode,
            1,
            "tool command should exit with code 1 on non-existent tool",
        )
        self.assertIn("not found", stderr)

        # Test successful run returns exit code 0
        process = subprocess.Popen(
            ["jac", "run", self.fixture_abs_path("hello.jac")],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
        stdout, stderr = process.communicate()
        self.assertEqual(
            process.returncode, 0, "run command should exit with code 0 on success"
        )
        self.assertIn("Hello World!", stdout)
