from pathlib import Path
from typing import List, Optional

from yanga.domain.component_analyzer import ComponentAnalyzer
from yanga.domain.execution_context import (
    ExecutionContext,
    UserRequest,
    UserRequestScope,
    UserRequestTarget,
)

from .cmake_backend import (
    CMakeAddExecutable,
    CMakeAddSubdirectory,
    CMakeCommand,
    CMakeComment,
    CMakeContent,
    CMakeCustomCommand,
    CMakeCustomTarget,
    CMakeElement,
    CMakeEmptyLine,
    CMakeEnableTesting,
    CMakeInclude,
    CMakeIncludeDirectories,
    CMakeListAppend,
    CMakePath,
    CMakeVariable,
)
from .generator import CMakeGenerator


class GTestCMakeGenerator(CMakeGenerator):
    """Generates CMake elements to build an executable for a variant."""

    def __init__(self, execution_context: ExecutionContext, output_dir: Path) -> None:
        super().__init__(execution_context, output_dir)
        # The directory where the CMakeLists.txt file is located
        self.cmake_source_dir = CMakePath(self.output_dir, "CMAKE_CURRENT_LIST_DIR")
        # The directory where the build files will be generated
        self.cmake_build_dir = CMakePath(self.output_dir, "CMAKE_BUILD_DIR", Path("build"))

    @property
    def variant_name(self) -> Optional[str]:
        return self.execution_context.variant_name

    def generate(self) -> List[CMakeElement]:
        elements: List[CMakeElement] = []
        elements.append(CMakeComment(f"Generated by {self.__class__.__name__}"))
        elements.extend(self.create_variant_cmake_elements())
        elements.extend(self.create_components_cmake_elements())
        return elements

    def create_variant_cmake_elements(self) -> List[CMakeElement]:
        elements: List[CMakeElement] = []
        elements.append(CMakeVariable("CMAKE_CXX_STANDARD", "14"))
        elements.append(CMakeVariable("CMAKE_CXX_STANDARD_REQUIRED", "ON"))
        elements.append(CMakeVariable("gtest_force_shared_crt", "ON", True, "BOOL", "", True))
        elements.append(CMakeComment("Add local GoogleTest directory"))
        artifacts_locator = self.execution_context.create_artifacts_locator()
        elements.append(
            CMakeAddSubdirectory(
                # TODO: this should be handled by an artifacts/dependency provider
                CMakePath(artifacts_locator.locate_artifact("external/gtest", [artifacts_locator.build_dir])),
                self.cmake_build_dir.joinpath(".gtest"),
            )
        )
        elements.append(CMakeInclude("GoogleTest"))
        elements.append(CMakeComment("Enable testing"))
        elements.append(CMakeInclude("CTest"))
        elements.append(CMakeListAppend("CMAKE_CTEST_ARGUMENTS", ["--output-on-failure"]))
        elements.append(CMakeEnableTesting())
        elements.append(self.get_include_directories())
        return elements

    def get_include_directories(self) -> CMakeIncludeDirectories:
        collector = ComponentAnalyzer(
            self.execution_context.components, self.execution_context.create_artifacts_locator()
        )
        include_dirs = collector.collect_include_directories() + self.execution_context.include_directories
        return CMakeIncludeDirectories([CMakePath(path) for path in include_dirs])

    def create_components_cmake_elements(self) -> List[CMakeElement]:
        elements: List[CMakeElement] = []
        for component in self.execution_context.components:
            component_analyzer = ComponentAnalyzer([component], self.execution_context.create_artifacts_locator())
            # Skip components without tests
            if not component_analyzer.is_testable():
                continue
            elements.append(CMakeComment(f"Component {component.name}"))
            sources = component_analyzer.collect_sources() + component_analyzer.collect_test_sources()
            component_test_executable_target = f"{component.name}"
            elements.append(
                CMakeAddExecutable(
                    component_test_executable_target,
                    [CMakePath(source) for source in sources],
                    ["GTest::gtest_main"],
                    [
                        "-ggdb",  # Include detailed debug information to be able to debug the executable.
                        "--coverage",  # Enable coverage tracking information to be generated.
                    ],
                    ["--coverage"],  # Enable coverage analysis.
                )
            )
            command = CMakeCommand(
                "${CMAKE_CTEST_COMMAND}",
                [
                    "${CMAKE_CTEST_ARGUMENTS}",
                    "--output-junit",
                    f"{component.name}_junit.xml",
                    "||",
                    "${CMAKE_COMMAND}",
                    "-E",
                    "true",
                ],
            )
            outputs = [self.cmake_build_dir.joinpath(f"{component.name}_junit.xml")]
            elements.append(
                CMakeCustomCommand(
                    "Run the test executable, generate JUnit report and return success independent of the test result",
                    outputs,
                    [component_test_executable_target],
                    [command],
                )
            )
            elements.extend(
                [
                    CMakeCustomTarget(
                        UserRequest(
                            UserRequestScope.COMPONENT, self.variant_name, component.name, UserRequestTarget.TEST
                        ).target_name,
                        f"Execute tests for {component.name}",
                        [],
                        outputs,  # type: ignore
                        True,
                    ),
                    CMakeCustomTarget(
                        UserRequest(
                            UserRequestScope.COMPONENT, self.variant_name, component.name, UserRequestTarget.BUILD
                        ).target_name,
                        f"Execute tests for {component.name}",
                        [],
                        outputs,  # type: ignore
                        True,
                    ),
                ]
            )
            elements.append(CMakeContent(f"gtest_discover_tests({component_test_executable_target})"))
            elements.append(CMakeEmptyLine())
        return elements
