from dataclasses import asdict, dataclass, field
from typing import List
from orionis.foundation.config.exceptions.integrity import OrionisIntegrityException
from orionis.services.system.workers import Workers
from orionis.test.enums.test_mode import ExecutionMode

@dataclass(unsafe_hash=True, kw_only=True)
class Testing:
    """
    Testing is a dataclass that holds configuration options for running tests.

    Attributes:
        verbosity (int): The verbosity level of the test output. Default is 2.
            - 0: Silent
            - 1: Minimal output
            - 2: Detailed output (default)
        execution_mode (ExecutionMode): The mode of test execution. Default is ExecutionMode.SEQUENTIAL.
            - ExecutionMode.SEQUENTIAL: Tests are executed one after another.
            - ExecutionMode.PARALLEL: Tests are executed in parallel.
        max_workers (int): The maximum number of worker threads/processes to use when running tests in parallel. Default is 4.
        fail_fast (bool): Whether to stop execution after the first test failure. Default is False.
        print_result (bool): Whether to print the test results to the console. Default is True.
        throw_exception (bool): Whether to throw an exception if a test fails. Default is False.
        base_path (str): The base directory where tests are located. Default is 'tests'.
        folder_path (str): The folder path pattern to search for tests. Default is '*'.
        pattern (str): The filename pattern to identify test files. Default is 'test_*.py'.
        test_name_pattern (str | None): A pattern to match specific test names. Default is None.
        tags (List[str] | None): A list of tags to filter tests. Default is None.
    """

    verbosity: int = field(
        default=2,
        metadata={
            "description": "The verbosity level of the test output. Default is 2.",
            "required": True,
            "default": 2
        }
    )

    execution_mode : str | ExecutionMode = field(
        default=ExecutionMode.SEQUENTIAL,
        metadata={
            "description": "The mode of test execution. Default is ExecutionMode.SEQUENTIAL",
            "required": True,
            "default": "ExecutionMode.SEQUENTIAL"
        }
    )

    max_workers: int = field(
        default_factory=lambda : Workers().calculate(),
        metadata={
            "description": "The maximum number of worker threads/processes to use when running tests in parallel. Default is 4.",
            "required": True,
            "default": "Workers.calculate()"
        }
    )

    fail_fast: bool = field(
        default=False,
        metadata={
            "description": "Whether to stop execution after the first test failure. Default is False.",
            "required": True,
            "default": False
        }
    )

    print_result: bool = field(
        default=True,
        metadata={
            "description": "Whether to print the test results to the console. Default is True.",
            "required": True,
            "default": True
        }
    )

    throw_exception: bool = field(
        default=False,
        metadata={
            "description": "Whether to throw an exception if a test fails. Default is False.",
            "required": True,
            "default": False
        }
    )

    base_path: str = field(
        default='tests',
        metadata={
            "description": "The base directory where tests are located. Default is 'tests'.",
            "required": True,
            "default": 'tests'
        }
    )

    folder_path: str | list = field(
        default='*',
        metadata={
            "description": "The folder path pattern to search for tests. Default is '*'.",
            "required": True,
            "default": '*'
        }
    )

    pattern: str = field(
        default='test_*.py',
        metadata={
            "description": "The filename pattern to identify test files. Default is 'test_*.py'.",
            "required": True,
            "default": 'test_*.py'
        }
    )

    test_name_pattern: str | None = field(
        default=None,
        metadata={
            "description": "A pattern to match specific test names. Default is None.",
            "required": False,
            "default": None
        }
    )

    tags: List[str] | None = field(
        default_factory=lambda:[],
        metadata={
            "description": "A list of tags to filter tests. Default is an empty list.",
            "required": False,
            "default": []
        }
    )

    def __post_init__(self):
        """
        Post-initialization validation for the testing configuration entity.
        This method performs type and value checks on the instance attributes to ensure they meet the expected constraints:
        - `verbosity` must be an integer between 0 and 2 (inclusive).
        - `execution_mode` must not be None.
        - `max_workers` must be a positive integer.
        - `fail_fast`, `print_result`, and `throw_exception` must be booleans.
        - `base_path`, `folder_path`, and `pattern` must be strings.
        - `test_name_pattern` must be either a string or None.
        - `tags` must be either None or a list of strings.
        Raises:
            OrionisIntegrityException: If any of the attributes do not meet the specified constraints.
        """

        if not isinstance(self.verbosity, int) or self.verbosity < 0 or self.verbosity > 2:
            raise OrionisIntegrityException(
                f"Invalid value for 'verbosity': {self.verbosity}. It must be an integer between 0 (silent) and 2 (detailed output)."
            )

        if not isinstance(self.execution_mode, (str, ExecutionMode)):
            raise OrionisIntegrityException(
                f"Invalid type for 'execution_mode': {type(self.execution_mode).__name__}. It must be a string or an instance of ExecutionMode."
            )

        if isinstance(self.execution_mode, str):
            options_modes = ExecutionMode._member_names_
            _value = str(self.execution_mode).upper().strip()
            if _value not in options_modes:
                raise OrionisIntegrityException(
                    f"Invalid value for 'execution_mode': {self.execution_mode}. It must be one of: {str(options_modes)}."
                )
            else:
                self.execution_mode = ExecutionMode[_value].value
        elif isinstance(self.execution_mode, ExecutionMode):
            self.execution_mode = self.execution_mode.value

        if not isinstance(self.max_workers, int) or self.max_workers < 1:
            raise OrionisIntegrityException(
                f"Invalid value for 'max_workers': {self.max_workers}. It must be a positive integer greater than 0."
            )

        if not isinstance(self.fail_fast, bool):
            raise OrionisIntegrityException(
                f"Invalid type for 'fail_fast': {type(self.fail_fast).__name__}. It must be a boolean (True or False)."
            )

        if not isinstance(self.print_result, bool):
            raise OrionisIntegrityException(
                f"Invalid type for 'print_result': {type(self.print_result).__name__}. It must be a boolean (True or False)."
            )

        if not isinstance(self.throw_exception, bool):
            raise OrionisIntegrityException(
                f"Invalid type for 'throw_exception': {type(self.throw_exception).__name__}. It must be a boolean (True or False)."
            )

        if not isinstance(self.base_path, str):
            raise OrionisIntegrityException(
                f"Invalid type for 'base_path': {type(self.base_path).__name__}. It must be a string representing the base directory for tests."
            )

        if not (isinstance(self.folder_path, str) or isinstance(self.folder_path, list)):
            raise OrionisIntegrityException(
            f"Invalid type for 'folder_path': {type(self.folder_path).__name__}. It must be a string or a list of strings representing the folder path pattern."
            )
        if isinstance(self.folder_path, list):
            for i, folder in enumerate(self.folder_path):
                if not isinstance(folder, str):
                    raise OrionisIntegrityException(
                        f"Invalid type for folder at index {i} in 'folder_path': {type(folder).__name__}. Each folder path must be a string."
                    )

        if not isinstance(self.pattern, str):
            raise OrionisIntegrityException(
                f"Invalid type for 'pattern': {type(self.pattern).__name__}. It must be a string representing the filename pattern for test files."
            )

        if self.test_name_pattern is not None and not isinstance(self.test_name_pattern, str):
            raise OrionisIntegrityException(
                f"Invalid type for 'test_name_pattern': {type(self.test_name_pattern).__name__}. It must be a string or None."
            )

        if self.tags is not None:
            if not isinstance(self.tags, list):
                raise OrionisIntegrityException(
                    f"Invalid type for 'tags': {type(self.tags).__name__}. It must be a list of strings or None."
                )
            for i, tag in enumerate(self.tags):
                if not isinstance(tag, str):
                    raise OrionisIntegrityException(
                        f"Invalid type for tag at index {i} in 'tags': {type(tag).__name__}. Each tag must be a string."
                    )

    def toDict(self) -> dict:
        """
        Convert the object to a dictionary representation.
        Returns:
            dict: A dictionary representation of the Dataclass object.
        """
        return asdict(self)

