# SPDX-License-Identifier: MIT

import json
import os
import tempfile
import unittest
from pathlib import Path

import clanganalyzer.compilation as sut


class CompilationEntryTest(unittest.TestCase):
    def test_from_db_entry_with_arguments(self):
        """Test parsing compilation database entry with 'arguments' field."""
        entry_data = {
            "directory": "/home/user/project",
            "file": "src/main.c",
            "arguments": ["gcc", "-c", "-Wall", "src/main.c"],
        }

        entry = sut.CompilationEntry.from_db_entry(entry_data)

        self.assertEqual(entry.directory, "/home/user/project")
        self.assertEqual(entry.file, "src/main.c")
        self.assertEqual(entry.arguments, ["gcc", "-c", "-Wall", "src/main.c"])

    def test_from_db_entry_with_command(self):
        """Test parsing compilation database entry with 'command' field."""
        entry_data = {"directory": "/home/user/project", "file": "src/main.cpp", "command": "g++ -c -std=c++17 src/main.cpp"}

        entry = sut.CompilationEntry.from_db_entry(entry_data)

        self.assertEqual(entry.directory, "/home/user/project")
        self.assertEqual(entry.file, "src/main.cpp")
        self.assertEqual(entry.arguments, ["g++", "-c", "-std=c++17", "src/main.cpp"])

    def test_from_db_entry_with_quoted_command(self):
        """Test parsing compilation database entry with quoted arguments in command."""
        entry_data = {
            "directory": "/home/user/project",
            "file": "src/test.c",
            "command": 'gcc -DVERSION="1.0.0" -c src/test.c',
        }

        entry = sut.CompilationEntry.from_db_entry(entry_data)

        self.assertEqual(entry.directory, "/home/user/project")
        self.assertEqual(entry.file, "src/test.c")
        self.assertEqual(entry.arguments, ["gcc", "-DVERSION=1.0.0", "-c", "src/test.c"])


class CompilationTest(unittest.TestCase):
    def test_c_compilation(self):
        """Test creation of C compilation from entry."""
        entry = sut.CompilationEntry(directory="/project", file="main.c", arguments=["gcc", "-Wall", "-O2", "main.c"])

        compilation = sut.Compilation.from_entry(entry)

        self.assertEqual(compilation.compiler, "c")
        self.assertEqual(compilation.flags, ["-Wall", "-O2", "main.c"])
        self.assertEqual(compilation.directory, os.path.normpath("/project"))
        self.assertEqual(compilation.source, os.path.normpath("/project/main.c"))

    def test_cpp_compilation(self):
        """Test creation of C++ compilation from entry."""
        entry = sut.CompilationEntry(
            directory="/project", file="main.cpp", arguments=["g++", "-std=c++17", "-Wall", "main.cpp"]
        )

        compilation = sut.Compilation.from_entry(entry)

        self.assertEqual(compilation.compiler, "c++")
        self.assertEqual(compilation.flags, ["-std=c++17", "-Wall", "main.cpp"])
        self.assertEqual(compilation.directory, os.path.normpath("/project"))
        self.assertEqual(compilation.source, os.path.normpath("/project/main.cpp"))

    def test_clang_cpp_compilation(self):
        """Test C++ detection with clang++."""
        entry = sut.CompilationEntry(directory="/project", file="test.cc", arguments=["clang++", "-O3", "test.cc"])

        compilation = sut.Compilation.from_entry(entry)

        self.assertEqual(compilation.compiler, "c++")
        self.assertEqual(compilation.flags, ["-O3", "test.cc"])

    def test_absolute_file_path(self):
        """Test handling of absolute file paths."""
        entry = sut.CompilationEntry(directory="/project", file="/absolute/path/to/file.c", arguments=["gcc", "file.c"])

        compilation = sut.Compilation.from_entry(entry)

        self.assertEqual(compilation.source, os.path.normpath("/absolute/path/to/file.c"))

    def test_empty_arguments(self):
        """Test handling of empty arguments list."""
        entry = sut.CompilationEntry(directory="/project", file="empty.c", arguments=[])

        compilation = sut.Compilation.from_entry(entry)

        self.assertEqual(compilation.compiler, "c")
        self.assertEqual(compilation.flags, [])

    def test_equality(self):
        """Test compilation equality comparison."""
        entry1 = sut.CompilationEntry("/project", "main.c", ["gcc", "main.c"])
        entry2 = sut.CompilationEntry("/project", "main.c", ["gcc", "main.c"])
        entry3 = sut.CompilationEntry("/project", "other.c", ["gcc", "other.c"])

        comp1 = sut.Compilation.from_entry(entry1)
        comp2 = sut.Compilation.from_entry(entry2)
        comp3 = sut.Compilation.from_entry(entry3)

        self.assertEqual(comp1, comp2)
        self.assertNotEqual(comp1, comp3)
        self.assertNotEqual(comp1, "not a compilation")

    def test_as_dict(self):
        """Test conversion to dictionary."""
        entry = sut.CompilationEntry(directory="/project", file="main.c", arguments=["gcc", "-Wall", "main.c"])

        compilation = sut.Compilation.from_entry(entry)
        result = compilation.as_dict()

        expected = {
            "source": os.path.normpath("/project/main.c"),
            "directory": os.path.normpath("/project"),
            "compiler": "c",
            "flags": ["-Wall", "main.c"],
        }

        self.assertEqual(result, expected)


class CompilationDatabaseTest(unittest.TestCase):
    def test_load_simple_database(self):
        """Test loading a simple compilation database."""
        database_content = [
            {"directory": "/project", "file": "main.c", "arguments": ["gcc", "-c", "main.c"]},
            {"directory": "/project", "file": "utils.cpp", "command": "g++ -std=c++11 -c utils.cpp"},
        ]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(database_content, f)
            temp_path = f.name

        try:
            compilations = list(sut.CompilationDatabase.load(temp_path))

            self.assertEqual(len(compilations), 2)

            # Check first compilation (C)
            comp1 = compilations[0]
            self.assertEqual(comp1.compiler, "c")
            self.assertEqual(comp1.source, os.path.normpath("/project/main.c"))
            self.assertEqual(comp1.flags, ["-c", "main.c"])

            # Check second compilation (C++)
            comp2 = compilations[1]
            self.assertEqual(comp2.compiler, "c++")
            self.assertEqual(comp2.source, os.path.normpath("/project/utils.cpp"))
            self.assertEqual(comp2.flags, ["-std=c++11", "-c", "utils.cpp"])

        finally:
            os.unlink(temp_path)

    def test_load_empty_database(self):
        """Test loading an empty compilation database."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump([], f)
            temp_path = f.name

        try:
            compilations = list(sut.CompilationDatabase.load(temp_path))
            self.assertEqual(len(compilations), 0)
        finally:
            os.unlink(temp_path)


class SourceClassifierTest(unittest.TestCase):
    def test_sources(self):
        """Test source file classification by extension."""
        tests = [
            ("header.h", None),
            ("impl.c", "c"),
            ("impl.cc", "c++"),
            ("impl.cpp", "c++"),
            ("impl.cxx", "c++"),
            ("impl.C", "c++"),
            ("impl.c++", "c++"),
            ("preprocessed.i", "c-cpp-output"),
            ("preprocessed.ii", "c++-cpp-output"),
            ("objc.m", "objective-c"),
            ("objcpp.mm", "objective-c++"),
            ("template.txx", "c++"),
            ("unknown.xyz", None),
        ]

        for filename, expected in tests:
            with self.subTest(filename=filename):
                result = sut.classify_source(filename)
                self.assertEqual(result, expected)

    def test_c_compiler_context(self):
        """Test source classification with C compiler context."""
        # With C compiler context, .i files are C preprocessed output
        result = sut.classify_source("test.i", c_compiler=True)
        self.assertEqual(result, "c-cpp-output")

        # With C++ compiler context, .i files are C++ preprocessed output
        result = sut.classify_source("test.i", c_compiler=False)
        self.assertEqual(result, "c++-cpp-output")

        # .c files change meaning based on compiler context
        result = sut.classify_source("test.c", c_compiler=True)
        self.assertEqual(result, "c")

        result = sut.classify_source("test.c", c_compiler=False)
        self.assertEqual(result, "c++")


class CommonPrefixTest(unittest.TestCase):
    def test_empty(self):
        self.assertEqual(sut._commonprefix([]), None)

    @unittest.skipIf(os.name == "nt", "windows has different path patterns")
    def test_with_different_filenames(self):
        self.assertEqual(sut._commonprefix(["/tmp/a.c", "/tmp/b.c"]), Path("/tmp"))

    @unittest.skipIf(os.name == "nt", "windows has different path patterns")
    def test_with_different_dirnames(self):
        self.assertEqual(sut._commonprefix(["/tmp/abs/a.c", "/tmp/ack/b.c"]), Path("/tmp"))

    @unittest.skipIf(os.name == "nt", "windows has different path patterns")
    def test_no_common_prefix(self):
        self.assertEqual(sut._commonprefix(["/tmp/abs/a.c", "/usr/ack/b.c"]), Path("/"))

    @unittest.skipIf(os.name == "nt", "windows has different path patterns")
    def test_with_single_file(self):
        self.assertEqual(sut._commonprefix(["/tmp/a.c"]), Path("/tmp"))

    @unittest.skipIf(os.name != "nt", "windows has different path patterns")
    def test_with_different_filenames_on_windows(self):
        self.assertEqual(sut._commonprefix(["c:\\tmp\\a.c", "c:\\tmp\\b.c"]), Path("c:\\tmp"))

    @unittest.skipIf(os.name != "nt", "windows has different path patterns")
    def test_with_different_dirnames_on_windows(self):
        self.assertEqual(sut._commonprefix(["c:\\tmp\\abs\\a.c", "c:\\tmp\\ack\\b.c"]), Path("c:\\tmp"))

    @unittest.skipIf(os.name != "nt", "windows has different path patterns")
    def test_no_common_prefix_on_windows(self):
        self.assertEqual(sut._commonprefix(["z:\\tmp\\abs\\a.c", "z:\\usr\\ack\\b.c"]), Path("z:\\"))

    @unittest.skipIf(os.name != "nt", "windows has different path patterns")
    def test_different_drive_on_windows(self):
        self.assertEqual(sut._commonprefix(["c:\\tmp\\abs\\a.c", "z:\\usr\\ack\\b.c"]), None)

    @unittest.skipIf(os.name != "nt", "windows has different path patterns")
    def test_with_single_file_on_windows(self):
        self.assertEqual(sut._commonprefix(["z:\\tmp\\a.c"]), Path("z:\\tmp"))


class CommonPrefixFromTest(unittest.TestCase):
    def test_commonprefix_from(self):
        """Test commonprefix_from with a compilation database file."""
        database_content = [
            {"directory": "/project", "file": "/project/src/main.c"},
            {"directory": "/project", "file": "/project/src/utils.c"},
            {"directory": "/project", "file": "/project/include/header.h"},
        ]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(database_content, f)
            temp_path = f.name

        try:
            result = sut.CompilationDatabase.file_commonprefix(temp_path)
            self.assertEqual(result, Path("/project"))
        finally:
            os.unlink(temp_path)

    def test_commonprefix_from_empty_database(self):
        """Test commonprefix_from with an empty compilation database."""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump([], f)
            temp_path = f.name

        try:
            result = sut.CompilationDatabase.file_commonprefix(temp_path)
            self.assertEqual(result, None)
        finally:
            os.unlink(temp_path)
