# Copyright 2025 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Integration test for the compile commands database merger.

This script has dependencies on compile command fragments, and uses the
merger to create compile commands databases. Since the collected compile
commands won't be deterministic (if the build directory has trailing compile
commands fragments from prior builds), compile commands correctness checks
are filtered by a regex of known files patterns that should be validate.

Because this does nested `bazel` calls under the hood, and because the merger
depends on BUILD_WORKSPACE_DIRECTORY, this test must be `bazel run` rather
than `bazel test`.
"""

import json
import os
import pathlib
import re
import shlex
import subprocess
import tempfile
import unittest

from python.runfiles import runfiles  # type: ignore

# These are generated by the pw_py_importable_runfile rules in the BUILD.bazel
# file.
from pw_ide.bazel.compile_commands import clangd_binary
from pw_ide.bazel.compile_commands import update_compile_commands_binary


_FILE_ALLOWLIST = (re.compile(r'.*pw_ide/bazel/compile_commands/test/.*\.cc?'),)


class CompileCommandsIntegrationTest(unittest.TestCase):
    """Integration test for the compile commands database merger."""

    def setUp(self):
        self.runfiles = runfiles.Create()
        self.clangd_path = self.runfiles.Rlocation(*clangd_binary.RLOCATION)
        self.updater_path = self.runfiles.Rlocation(
            *update_compile_commands_binary.RLOCATION
        )
        assert 'BUILD_WORKSPACE_DIRECTORY' in os.environ, (
            'This must be `bazel run` to work properly, and cannot be tested '
            'via `bazel test`'
        )
        self.project_root = os.environ.get('BUILD_WORKSPACE_DIRECTORY')

    def test_run_clangd_check_on_generated_commands(self):
        """Runs clangd --check on files with compile commands."""
        files_checked = []
        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = pathlib.Path(temp_dir_str)

            # Run the compile commands updater.
            update_result = subprocess.run(
                [self.updater_path, f'--out-dir={temp_dir}'],
                capture_output=True,
                text=True,
                check=False,
            )
            self.assertEqual(
                update_result.returncode,
                0,
                f'update_compile_commands failed: {update_result.stderr}',
            )

            # For each platform's compile_commands.json...
            compile_db_paths = list(temp_dir.rglob('compile_commands.json'))
            self.assertGreater(
                len(compile_db_paths), 0, 'No compile_commands.json files found'
            )

            for db_path in compile_db_paths:
                # TODO: https://pwbug.dev/444224547 - This fails on gcc builds
                # because `-fno-canonical-system-headers` is unknown.
                if 'stm32f429i' in str(db_path):
                    continue

                with open(db_path, 'r') as f:
                    compile_commands = json.load(f)

                self.assertIsInstance(compile_commands, list)
                if not compile_commands:
                    continue

                # Run clangd --check for each file.
                for command in compile_commands:
                    file_path = command['file']

                    if not any(exp.match(file_path) for exp in _FILE_ALLOWLIST):
                        continue

                    clangd_result = subprocess.run(
                        [
                            self.clangd_path,
                            f'--compile-commands-dir={db_path.parent}',
                            f'--check={file_path}',
                        ],
                        capture_output=True,
                        text=True,
                        check=False,
                        # The compile commands need to run from the project root
                        # for relative path resolution to work.
                        cwd=self.project_root,
                    )

                    self.assertEqual(
                        clangd_result.returncode,
                        0,
                        '\n'.join(
                            (
                                f'clangd --check in {db_path} failed:',
                                f'ENTRY: {command}',
                                f'CMD: {shlex.join(clangd_result.args)}',
                                f'STDOUT:\n{clangd_result.stdout}',
                                f'STDERR:\n{clangd_result.stderr}',
                            )
                        ),
                    )
                    files_checked.append(file_path)
        self.assertGreater(len(files_checked), 0)


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