from typing import ClassVar, List, Dict, Any, Optional, Type
from abc import ABC, abstractmethod
from dataclasses import dataclass, asdict

@dataclass
class Token:
    type: str
    value: Any


@dataclass
class Operation:
    function: str
    type: str
    value: Any


@dataclass
class StackItem:
    type: str
    value: Any


@dataclass
class RuntimeError:
    message: str


@dataclass
class TestCase:
    name: str
    code: str
    tokens: List[Dict[str, Any]]
    operations: Optional[List[Dict[str, Any]]] = None
    stack_final: Optional[List[Dict[str, Any]]] = None
    runtime_error: Optional[Dict[str, str]] = None


def to_dict(obj) -> Dict[str, Any]:
    """Convert dataclass to dict, removing None values."""
    if obj is None:
        raise ValueError("Obj cannot be None")
    d = asdict(obj) if hasattr(obj, '__dataclass_fields__') else obj
    return {k: v for k, v in d.items() if v is not None}


class BaseTestGenerator(ABC):
    """
    Abstract base class for test case generators.

    Provides common functionality for generating test cases including
    test creation, operation building, and error handling.
    """

    ENABLE_UNICODE = False
    ENABLE_EXPONENTIAL_LITERALS = False
    ENABLE_CHAR_HEX_ESCAPE = False
    ENABLE_STRINGS = False

    __generators: "ClassVar[List[Type[BaseTestGenerator]]]" = []

    def __init_subclass__(cls):
        BaseTestGenerator.__generators.append(cls)

    def __init__(self):
        """Initialize the test generator with an empty test list."""
        self.tests: List[Dict[str, Any]] = []

    # =========================================================================
    # Test Case Management
    # =========================================================================

    def add_test(self, name: str, code: str, tokens: List[Token],
                 operations: Optional[List[Operation]] = None,
                 stack_final: Optional[List[StackItem]] = None,
                 runtime_error: Optional[RuntimeError] = None):
        """
        Add a test case to the test suite.

        Args:
            name: Descriptive name for the test
            code: Source code being tested
            tokens: List of expected tokens from lexing
            operations: Optional list of operations for evaluation phase
            stack_final: Optional final stack state after execution
            runtime_error: Optional runtime error if test should fail
        """
        test = TestCase(
            name=name,
            code=code,
            tokens=[to_dict(t) for t in tokens],
            operations=[to_dict(o) for o in operations] if operations else None,
            stack_final=[to_dict(s) for s in stack_final] if stack_final else None,
            runtime_error=to_dict(runtime_error) if runtime_error else None
        )
        self.tests.append(to_dict(test))

    # =========================================================================
    # Factory Methods
    # =========================================================================

    def make_push_op(self, type_name: str, value: Any) -> Operation:
        """
        Create a push operation.

        Args:
            type_name: Type of the value being pushed
            value: The value to push

        Returns:
            Operation object representing a push
        """
        return Operation(function="push", type=type_name, value=value)

    def make_stack_item(self, type_name: str, value: Any) -> StackItem:
        """
        Create a stack item.

        Args:
            type_name: Type of the stack item
            value: The value of the stack item

        Returns:
            StackItem object
        """
        return StackItem(type=type_name, value=value)

    def make_error_token(self, message: str) -> Token:
        """
        Create an error token.

        Args:
            message: Error message

        Returns:
            Token object with type "error"
        """
        return Token(type="error", value=message)

    def make_runtime_error(self, message: str) -> RuntimeError:
        """
        Create a runtime error.

        Args:
            message: Error message

        Returns:
            RuntimeError object
        """
        return RuntimeError(message=message)

    # =========================================================================
    # Convenience Test Creators
    # =========================================================================

    def make_success_test(self, name: str, code: str, type_name: str, value: Any):
        """
        Create a successful test case with standard push operation.

        Args:
            name: Test name
            code: Source code
            type_name: Type of the value
            value: The value
        """
        token = Token(type=type_name, value=value)
        op = self.make_push_op(type_name, value)
        stack = self.make_stack_item(type_name, value)
        self.add_test(name, code, [token], [op], [stack])

    def make_error_test(self, name: str, code: str, error_msg: str):
        """
        Create an error test case (lexing error).

        Args:
            name: Test name
            code: Source code
            error_msg: Expected error message
        """
        token = self.make_error_token(error_msg)
        self.add_test(name, code, [token])

    def make_runtime_error_test(self, name: str, code: str, tokens: List[Token],
                                 operations: List[Operation], error_msg: str):
        """
        Create a runtime error test case.

        Args:
            name: Test name
            code: Source code
            tokens: Tokens that were successfully lexed
            operations: Operations that led to the error
            error_msg: Expected runtime error message
        """
        runtime_error = self.make_runtime_error(error_msg)
        self.add_test(name, code, tokens, operations, None, runtime_error)

    def make_empty_test(self, name: str, code: str):
        """
        Create a test case with empty result (e.g., comments, whitespace).

        Args:
            name: Test name
            code: Source code
        """
        self.add_test(name, code, [], [], [])

    # =========================================================================
    # Multi-Value Test Helpers
    # =========================================================================

    def make_multi_value_test(self, name: str, code: str,
                             values: List[tuple[str, Any]]):
        """
        Create a test with multiple values on the stack.

        Args:
            name: Test name
            code: Source code
            values: List of (type_name, value) tuples in stack order
        """
        tokens = [Token(type=t, value=v) for t, v in values]
        operations = [self.make_push_op(t, v) for t, v in values]
        stack = [self.make_stack_item(t, v) for t, v in values]
        self.add_test(name, code, tokens, operations, stack)

    # =========================================================================
    # Test Suite Generation
    # =========================================================================

    @abstractmethod
    def generate_all_tests(self) -> List[Dict[str, Any]]:
        """
        Generate all test cases for this generator.

        Must be implemented by subclasses to define their specific test suite.

        Returns:
            List of test case dictionaries ready for serialization
        """
        pass

    def get_tests(self) -> List[Dict[str, Any]]:
        """
        Get the current list of tests.

        Returns:
            List of test case dictionaries
        """
        return self.tests

    def clear_tests(self):
        """Clear all tests from the generator."""
        self.tests = []

    def test_count(self) -> int:
        """
        Get the number of tests generated.

        Returns:
            Number of tests
        """
        return len(self.tests)

    @classmethod
    def generate_tests(cls) -> List[Dict[str, Any]]:
        gen = cls()
        tests = gen.generate_all_tests()
        gen.print_statistics()
        return tests

    @classmethod
    def run_all_generators(cls) -> List[Dict[str, Any]]:
        tests = []
        for sub_cls in cls.__generators:
            tests += sub_cls.generate_tests()
        return tests

    # =========================================================================
    # Test Organization Helpers
    # =========================================================================

    def add_test_group_comment(self, comment: str):
        """
        Add a comment to organize test groups (for documentation).

        Note: This doesn't add an actual test, just tracks organization
        in subclass implementations.

        Args:
            comment: Comment describing the test group
        """
        # Subclasses can override to add metadata or logging
        pass

    # =========================================================================
    # Validation Helpers
    # =========================================================================

    def validate_test_names_unique(self) -> bool:
        """
        Check if all test names are unique.

        Returns:
            True if all test names are unique, False otherwise
        """
        names = [test['name'] for test in self.tests]
        return len(names) == len(set(names))

    def get_duplicate_test_names(self) -> List[str]:
        """
        Get list of duplicate test names.

        Returns:
            List of test names that appear more than once
        """
        names = [test['name'] for test in self.tests]
        seen = set()
        duplicates = set()
        for name in names:
            if name in seen:
                duplicates.add(name)
            seen.add(name)
        return list(duplicates)

    # =========================================================================
    # Statistics
    # =========================================================================

    def get_test_statistics(self) -> Dict[str, int]:
        """
        Get statistics about the generated tests.

        Returns:
            Dictionary with test statistics
        """
        stats = {
            'total': len(self.tests),
            'success': 0,
            'lex_error': 0,
            'runtime_error': 0,
            'empty': 0,
        }

        for test in self.tests:
            if test.get('runtime_error'):
                stats['runtime_error'] += 1
            elif test.get('tokens') and test['tokens'][0].get('type') == 'error':
                stats['lex_error'] += 1
            elif not test.get('tokens'):
                stats['empty'] += 1
            else:
                stats['success'] += 1

        return stats

    def print_statistics(self):
        """Print test statistics to console."""
        stats = self.get_test_statistics()
        print(f"Test Statistics for {self.__class__.__name__}:")
        print(f"  Total tests: {stats['total']}")
        print(f"  Success tests: {stats['success']}")
        print(f"  Lex error tests: {stats['lex_error']}")
        print(f"  Runtime error tests: {stats['runtime_error']}")
        print(f"  Empty tests: {stats['empty']}")

        if not self.validate_test_names_unique():
            duplicates = self.get_duplicate_test_names()
            print(f"  WARNING: Duplicate test names found: {duplicates}")

    # =========================================================================
    # Token String Helpers
    # =========================================================================

    def make_token_string_token(self, inner_tokens: List[Token]) -> Token:
        """
        Create a token string token containing inner tokens.

        Args:
            inner_tokens: List of tokens inside the token string

        Returns:
            Token with type "token_string" and tokens array
        """
        return Token(type="token_string", value=[t.__dict__ for t in inner_tokens])

    def make_token_string_test(self, name: str, code: str, inner_tokens: List[Token]):
        """
        Create a successful token string test.

        Args:
            name: Test name
            code: Source code
            inner_tokens: Tokens inside the token string
        """
        token = self.make_token_string_token(inner_tokens)

        # For operations, we push the token string as a value
        op_value = {"tokens": [t.__dict__ for t in inner_tokens]}
        op = self.make_push_op("token_string", op_value)

        # On the stack, it's a token_string value
        stack = self.make_stack_item("token_string", op_value)

        self.add_test(name, code, [token], [op], [stack])
