#!/usr/bin/env python3
"""
Architectural Layer Independence Test Runner with Chaos Engineering

This module implements a sophisticated chaos engineering system that validates
architectural layer independence by injecting controlled failures and monitoring
the impact on dependent and independent layers.

The system systematically tests that:
1. Failures in lower layers propagate only to dependent upper layers
2. Independent layers remain unaffected by failures in unrelated layers
3. The dependency matrix matches the intended architectural design

Usage:
    python chaos_test_runner.py [options]

Commands:
    validate-independence    Run full architectural independence validation
    inject-layer-failure     Inject failure into specific layer
    dependency-matrix        Show architectural dependency matrix
    chaos-report            Generate comprehensive chaos test report
"""

import os
import sys
import json
import time
import pytest
import tempfile
import traceback
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Set, Optional, Tuple, Any
from dataclasses import dataclass, asdict
from contextlib import contextmanager
from unittest.mock import patch, MagicMock
import logging


@dataclass
class ArchitecturalLayer:
    """Represents an architectural layer with its properties."""
    name: str
    level: int
    description: str
    test_pattern: str
    dependencies: List[str]  # Layers this layer depends on
    modules: List[str]       # Python modules in this layer


@dataclass
class ChaosInjectionResult:
    """Results from a chaos injection test."""
    target_layer: str
    injection_type: str
    start_time: datetime
    end_time: datetime
    affected_layers: List[str]
    expected_affected: List[str]
    dependency_violations: List[str]
    test_results: Dict[str, Any]
    success: bool
    error_message: Optional[str] = None


@dataclass
class DependencyViolation:
    """Represents a detected architectural dependency violation."""
    violating_layer: str
    affected_layer: str
    violation_type: str
    expected_independence: bool
    actual_impact: bool
    severity: str
    description: str


class ArchitecturalChaosEngine:
    """
    Chaos engineering engine for validating architectural layer independence.

    This engine systematically injects failures into each architectural layer
    and monitors the impact on other layers to validate the dependency matrix.
    """

    def __init__(self, project_root: Optional[Path] = None):
        self.project_root = project_root or Path(__file__).parent
        self.test_dir = self.project_root / "tests"
        self.results_dir = self.project_root / "chaos_results"
        self.results_dir.mkdir(exist_ok=True)

        # Set up logging
        self.logger = self._setup_logging()

        # Define architectural layers with their dependencies
        self.layers = self._define_architectural_layers()
        self.dependency_matrix = self._build_dependency_matrix()

        # Chaos injection mechanisms
        self.injection_strategies = {
            'import_failure': self._inject_import_failure,
            'function_failure': self._inject_function_failure,
            'class_failure': self._inject_class_failure,
            'module_unavailable': self._inject_module_unavailable,
            'database_failure': self._inject_database_failure,
            'network_failure': self._inject_network_failure,
            'filesystem_failure': self._inject_filesystem_failure
        }

    def _setup_logging(self) -> logging.Logger:
        """Set up logging for chaos engineering operations."""
        logger = logging.getLogger('chaos_engine')
        logger.setLevel(logging.INFO)

        if not logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter(
                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
            )
            handler.setFormatter(formatter)
            logger.addHandler(handler)

        return logger

    def _define_architectural_layers(self) -> Dict[str, ArchitecturalLayer]:
        """Define the architectural layers and their relationships."""
        return {
            'L1_Presentation': ArchitecturalLayer(
                name='L1_Presentation',
                level=1,
                description='CLI Interface and User Interaction',
                test_pattern='test_l1_*.py',
                dependencies=['L2_Application'],
                modules=['cli', 'markitect.cli']
            ),
            'L2_Application': ArchitecturalLayer(
                name='L2_Application',
                level=2,
                description='Feature Workflows and Use Cases',
                test_pattern='test_l2_*.py',
                dependencies=['L3_Domain', 'L4_Service'],
                modules=['application', 'tddai', 'markitect.issues']
            ),
            'L3_Domain': ArchitecturalLayer(
                name='L3_Domain',
                level=3,
                description='Business Logic and Domain Models',
                test_pattern='test_l3_*.py',
                dependencies=['L4_Service'],
                modules=['domain', 'markitect.schema_generator', 'markitect.metaschema']
            ),
            'L4_Service': ArchitecturalLayer(
                name='L4_Service',
                level=4,
                description='Application Services and Orchestration',
                test_pattern='test_l4_*.py',
                dependencies=['L5_Infrastructure'],
                modules=['services', 'markitect.ast_service', 'markitect.document_manager']
            ),
            'L5_Infrastructure': ArchitecturalLayer(
                name='L5_Infrastructure',
                level=5,
                description='Technical Infrastructure',
                test_pattern='test_l5_*.py',
                dependencies=['L6_Integration', 'L7_Foundation'],
                modules=['infrastructure', 'markitect.cache_service', 'markitect.ast_cache']
            ),
            'L6_Integration': ArchitecturalLayer(
                name='L6_Integration',
                level=6,
                description='External API and System Integration',
                test_pattern='test_l6_*.py',
                dependencies=['L7_Foundation'],
                modules=['gitea', 'markitect.issues.plugins']
            ),
            'L7_Foundation': ArchitecturalLayer(
                name='L7_Foundation',
                level=7,
                description='Core Components and Utilities',
                test_pattern='test_l7_*.py',
                dependencies=[],  # Foundation depends on nothing
                modules=['markitect.database', 'markitect.parser', 'markitect.frontmatter']
            )
        }

    def _build_dependency_matrix(self) -> Dict[str, Set[str]]:
        """Build the complete dependency matrix including transitive dependencies."""
        matrix = {}

        for layer_name, layer in self.layers.items():
            # Start with direct dependencies
            all_deps = set(layer.dependencies)

            # Add transitive dependencies
            to_check = list(layer.dependencies)
            while to_check:
                dep = to_check.pop(0)
                if dep in self.layers:
                    for transitive_dep in self.layers[dep].dependencies:
                        if transitive_dep not in all_deps:
                            all_deps.add(transitive_dep)
                            to_check.append(transitive_dep)

            matrix[layer_name] = all_deps

        return matrix

    def show_dependency_matrix(self):
        """Display the architectural dependency matrix."""
        print("🏗️  Architectural Layer Dependency Matrix")
        print("=" * 50)

        for layer_name in sorted(self.layers.keys()):
            layer = self.layers[layer_name]
            deps = self.dependency_matrix[layer_name]

            print(f"\n{layer.name} (L{layer.level})")
            print(f"  Description: {layer.description}")
            print(f"  Direct Dependencies: {layer.dependencies}")
            print(f"  All Dependencies: {sorted(deps)}")
            print(f"  Test Pattern: {layer.test_pattern}")

    @contextmanager
    def _chaos_injection_context(self, layer_name: str, injection_type: str):
        """Context manager for safe chaos injection with cleanup."""
        self.logger.info(f"🔥 Starting chaos injection: {injection_type} on {layer_name}")

        # Store original state for restoration
        original_modules = sys.modules.copy()
        import builtins
        original_import = builtins.__import__

        try:
            yield
        except Exception as e:
            self.logger.error(f"❌ Chaos injection failed: {e}")
            raise
        finally:
            # Restore original state
            builtins.__import__ = original_import
            self._restore_system_state(original_modules, {})
            self.logger.info(f"✅ Chaos injection cleanup completed for {layer_name}")

    def _restore_system_state(self, original_modules: Dict, original_builtins: Dict):
        """Restore system state after chaos injection."""
        # Restore modules
        modules_to_remove = set(sys.modules.keys()) - set(original_modules.keys())
        for module in modules_to_remove:
            if module in sys.modules:
                del sys.modules[module]

        # Restore modified modules
        for module_name, module in original_modules.items():
            sys.modules[module_name] = module

    def _inject_import_failure(self, layer_name: str, **kwargs):
        """Inject import failure for modules in the specified layer."""
        layer = self.layers[layer_name]
        failed_modules = []

        # Store original import function
        import builtins
        original_import = builtins.__import__

        def patched_import(name, *args, **kwargs):
            # Check if this import should fail
            if any(name.startswith(mod) for mod in layer.modules):
                raise ImportError(f"Chaos injection: {name} module failure")
            return original_import(name, *args, **kwargs)

        # Apply the patch
        builtins.__import__ = patched_import
        failed_modules.extend(layer.modules)

        return {'failed_modules': failed_modules}

    def _inject_function_failure(self, layer_name: str, **kwargs):
        """Inject function-level failures in the specified layer."""
        layer = self.layers[layer_name]
        patched_functions = []

        # This would patch specific functions based on the layer
        # Implementation would depend on the specific layer's key functions
        return {'patched_functions': patched_functions}

    def _inject_class_failure(self, layer_name: str, **kwargs):
        """Inject class-level failures in the specified layer."""
        layer = self.layers[layer_name]
        patched_classes = []

        # This would patch specific classes based on the layer
        return {'patched_classes': patched_classes}

    def _inject_module_unavailable(self, layer_name: str, **kwargs):
        """Make entire modules unavailable for the specified layer."""
        layer = self.layers[layer_name]

        for module_name in layer.modules:
            if module_name in sys.modules:
                # Temporarily remove the module
                del sys.modules[module_name]

        return {'removed_modules': layer.modules}

    def _inject_database_failure(self, layer_name: str, **kwargs):
        """Inject database failures for infrastructure layer."""
        if layer_name != 'L5_Infrastructure':
            return {'message': 'Database failure only applicable to Infrastructure layer'}

        # Patch database operations to fail
        patches = []
        return {'database_patches': patches}

    def _inject_network_failure(self, layer_name: str, **kwargs):
        """Inject network failures for integration layer."""
        if layer_name != 'L6_Integration':
            return {'message': 'Network failure only applicable to Integration layer'}

        # Patch network operations to fail
        patches = []
        return {'network_patches': patches}

    def _inject_filesystem_failure(self, layer_name: str, **kwargs):
        """Inject filesystem failures."""
        patches = []
        return {'filesystem_patches': patches}

    def run_layer_tests(self, layer_name: str) -> Dict[str, Any]:
        """Run tests for a specific layer and return results."""
        layer = self.layers[layer_name]
        test_files = list(self.test_dir.glob(layer.test_pattern))

        if not test_files:
            return {
                'success': False,
                'error': f'No test files found for pattern {layer.test_pattern}',
                'test_count': 0,
                'failures': 0
            }

        # Run pytest for the layer
        cmd = ['python', '-m', 'pytest'] + [str(f) for f in test_files] + [
            '--tb=short', '--quiet', '--disable-warnings'
        ]

        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                cwd=self.project_root,
                timeout=120  # 2 minute timeout per layer
            )

            # Parse pytest output for test counts
            output = result.stdout + result.stderr
            test_count = self._extract_test_count(output)
            failures = self._extract_failure_count(output)

            return {
                'success': result.returncode == 0,
                'test_count': test_count,
                'failures': failures,
                'output': output[:1000],  # Truncate for storage
                'return_code': result.returncode
            }

        except subprocess.TimeoutExpired:
            return {
                'success': False,
                'error': 'Test execution timeout',
                'test_count': 0,
                'failures': 1
            }
        except Exception as e:
            return {
                'success': False,
                'error': str(e),
                'test_count': 0,
                'failures': 1
            }

    def _extract_test_count(self, output: str) -> int:
        """Extract total test count from pytest output."""
        import re
        patterns = [
            r'(\d+) passed',
            r'collected (\d+) items',
            r'(\d+) failed',
        ]

        for pattern in patterns:
            match = re.search(pattern, output)
            if match:
                return int(match.group(1))

        return 0

    def _extract_failure_count(self, output: str) -> int:
        """Extract failure count from pytest output."""
        import re
        patterns = [
            r'(\d+) failed',
            r'FAILED.*::.*(\d+)',
        ]

        for pattern in patterns:
            match = re.search(pattern, output)
            if match:
                return int(match.group(1))

        return 0

    def inject_chaos_and_test(self, target_layer: str, injection_type: str) -> ChaosInjectionResult:
        """Inject chaos into a layer and run all tests to measure impact."""
        start_time = datetime.now()

        try:
            with self._chaos_injection_context(target_layer, injection_type):
                # Perform the chaos injection
                injection_result = self.injection_strategies[injection_type](target_layer)

                # Run tests on all layers to see impact
                test_results = {}
                affected_layers = []

                for layer_name in self.layers.keys():
                    self.logger.info(f"🧪 Testing {layer_name} under chaos conditions")
                    layer_result = self.run_layer_tests(layer_name)
                    test_results[layer_name] = layer_result

                    # Consider layer affected if tests fail
                    if not layer_result['success']:
                        affected_layers.append(layer_name)

                # Determine expected affected layers
                expected_affected = self._calculate_expected_impact(target_layer)

                # Detect violations
                violations = self._detect_dependency_violations(
                    target_layer, affected_layers, expected_affected
                )

                end_time = datetime.now()

                return ChaosInjectionResult(
                    target_layer=target_layer,
                    injection_type=injection_type,
                    start_time=start_time,
                    end_time=end_time,
                    affected_layers=affected_layers,
                    expected_affected=expected_affected,
                    dependency_violations=[v.violating_layer for v in violations],
                    test_results=test_results,
                    success=len(violations) == 0
                )

        except Exception as e:
            end_time = datetime.now()
            return ChaosInjectionResult(
                target_layer=target_layer,
                injection_type=injection_type,
                start_time=start_time,
                end_time=end_time,
                affected_layers=[],
                expected_affected=[],
                dependency_violations=[],
                test_results={},
                success=False,
                error_message=str(e)
            )

    def _calculate_expected_impact(self, target_layer: str) -> List[str]:
        """Calculate which layers should be affected by failure in target layer."""
        expected_affected = [target_layer]  # Target layer should always be affected

        # Find all layers that depend on the target layer
        for layer_name, dependencies in self.dependency_matrix.items():
            if target_layer in dependencies:
                expected_affected.append(layer_name)

        return expected_affected

    def _detect_dependency_violations(self, target_layer: str,
                                    actual_affected: List[str],
                                    expected_affected: List[str]) -> List[DependencyViolation]:
        """Detect violations of architectural dependencies."""
        violations = []

        # Check for unexpected impacts (layers that shouldn't be affected but were)
        for layer in actual_affected:
            if layer not in expected_affected:
                violations.append(DependencyViolation(
                    violating_layer=layer,
                    affected_layer=target_layer,
                    violation_type='unexpected_dependency',
                    expected_independence=True,
                    actual_impact=True,
                    severity='HIGH',
                    description=f'{layer} was affected by {target_layer} failure but should be independent'
                ))

        # Check for missing impacts (layers that should be affected but weren't)
        for layer in expected_affected:
            if layer not in actual_affected and layer != target_layer:
                violations.append(DependencyViolation(
                    violating_layer=layer,
                    affected_layer=target_layer,
                    violation_type='missing_dependency',
                    expected_independence=False,
                    actual_impact=False,
                    severity='MEDIUM',
                    description=f'{layer} should be affected by {target_layer} failure but was not'
                ))

        return violations

    def validate_architectural_independence(self) -> Dict[str, Any]:
        """Run comprehensive architectural independence validation."""
        self.logger.info("🚀 Starting comprehensive architectural independence validation")

        validation_results = {
            'start_time': datetime.now(),
            'layer_results': {},
            'violations': [],
            'summary': {
                'total_injections': 0,
                'successful_injections': 0,
                'total_violations': 0,
                'layers_tested': len(self.layers)
            }
        }

        # Test each layer with different injection types
        injection_types = ['import_failure', 'module_unavailable']

        for layer_name in self.layers.keys():
            layer_results = {}

            for injection_type in injection_types:
                self.logger.info(f"🔥 Testing {layer_name} with {injection_type}")

                result = self.inject_chaos_and_test(layer_name, injection_type)
                layer_results[injection_type] = result

                validation_results['summary']['total_injections'] += 1
                if result.success:
                    validation_results['summary']['successful_injections'] += 1

                validation_results['summary']['total_violations'] += len(result.dependency_violations)

            validation_results['layer_results'][layer_name] = layer_results

        validation_results['end_time'] = datetime.now()
        validation_results['duration'] = (
            validation_results['end_time'] - validation_results['start_time']
        ).total_seconds()

        # Save results
        self._save_validation_results(validation_results)

        return validation_results

    def _save_validation_results(self, results: Dict[str, Any]):
        """Save validation results to file."""
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f"chaos_validation_{timestamp}.json"
        filepath = self.results_dir / filename

        # Convert datetime objects to strings for JSON serialization
        serializable_results = self._make_json_serializable(results)

        with open(filepath, 'w') as f:
            json.dump(serializable_results, f, indent=2)

        self.logger.info(f"📄 Results saved to {filepath}")

    def _make_json_serializable(self, obj):
        """Convert objects to JSON-serializable format."""
        if isinstance(obj, datetime):
            return obj.isoformat()
        elif isinstance(obj, dict):
            return {k: self._make_json_serializable(v) for k, v in obj.items()}
        elif isinstance(obj, list):
            return [self._make_json_serializable(item) for item in obj]
        elif hasattr(obj, '__dict__'):
            return self._make_json_serializable(asdict(obj))
        else:
            return obj

    def generate_chaos_report(self, results_file: Optional[str] = None) -> str:
        """Generate a comprehensive chaos engineering report."""
        if results_file:
            with open(results_file, 'r') as f:
                results = json.load(f)
        else:
            # Use the most recent results file
            result_files = sorted(self.results_dir.glob("chaos_validation_*.json"))
            if not result_files:
                return "No chaos validation results found"

            with open(result_files[-1], 'r') as f:
                results = json.load(f)

        report = self._build_text_report(results)

        # Save report
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        report_file = self.results_dir / f"chaos_report_{timestamp}.md"
        with open(report_file, 'w') as f:
            f.write(report)

        return report

    def _build_text_report(self, results: Dict[str, Any]) -> str:
        """Build a formatted text report from results."""
        summary = results['summary']

        report = f"""# Architectural Independence Chaos Engineering Report

## Executive Summary

**Validation Date**: {results['start_time']}
**Duration**: {results.get('duration', 0):.2f} seconds
**Layers Tested**: {summary['layers_tested']}
**Total Chaos Injections**: {summary['total_injections']}
**Successful Injections**: {summary['successful_injections']}
**Total Violations Detected**: {summary['total_violations']}

**Overall Health**: {'✅ PASS' if summary['total_violations'] == 0 else '❌ VIOLATIONS DETECTED'}

## Architectural Layer Overview

"""

        for layer_name, layer in self.layers.items():
            dependencies = ', '.join(layer.dependencies) if layer.dependencies else 'None'
            report += f"- **{layer.name}**: {layer.description} (Dependencies: {dependencies})\n"

        report += "\n## Dependency Matrix\n\n"
        for layer_name, deps in self.dependency_matrix.items():
            deps_str = ', '.join(sorted(deps)) if deps else 'None'
            report += f"- **{layer_name}**: {deps_str}\n"

        report += "\n## Chaos Injection Results\n\n"

        layer_results = results.get('layer_results', {})
        for layer_name, injections in layer_results.items():
            report += f"### {layer_name}\n\n"

            for injection_type, result in injections.items():
                status = '✅ PASS' if result['success'] else '❌ FAIL'
                violations = len(result['dependency_violations'])

                report += f"**{injection_type}**: {status}\n"
                report += f"- Affected Layers: {', '.join(result['affected_layers'])}\n"
                report += f"- Expected Affected: {', '.join(result['expected_affected'])}\n"
                report += f"- Violations: {violations}\n"

                if result.get('error_message'):
                    report += f"- Error: {result['error_message']}\n"

                report += "\n"

        report += "\n## Recommendations\n\n"

        if summary['total_violations'] == 0:
            report += "✅ **Excellent**: All architectural boundaries are properly maintained.\n"
            report += "✅ **No violations detected**: The system demonstrates proper layer independence.\n"
        else:
            report += "❌ **Action Required**: Architectural violations detected that need attention.\n"
            report += "🔧 **Priority**: Review and refactor components with dependency violations.\n"
            report += "📊 **Monitor**: Run chaos tests regularly to prevent regression.\n"

        return report


def main():
    """Main entry point for the chaos test runner."""
    import argparse

    parser = argparse.ArgumentParser(
        description="Architectural Layer Independence Test Runner with Chaos Engineering"
    )

    parser.add_argument('command', choices=[
        'validate-independence',
        'inject-layer-failure',
        'dependency-matrix',
        'chaos-report'
    ], help='Command to execute')

    parser.add_argument('--layer', type=str, help='Target layer for injection')
    parser.add_argument('--injection-type', type=str, default='import_failure',
                       choices=['import_failure', 'module_unavailable', 'function_failure'],
                       help='Type of chaos injection')
    parser.add_argument('--results-file', type=str, help='Results file for report generation')
    parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')

    args = parser.parse_args()

    # Set up logging level
    if args.verbose:
        logging.getLogger('chaos_engine').setLevel(logging.DEBUG)

    engine = ArchitecturalChaosEngine()

    if args.command == 'dependency-matrix':
        engine.show_dependency_matrix()

    elif args.command == 'inject-layer-failure':
        if not args.layer:
            print("❌ Error: --layer is required for inject-layer-failure")
            sys.exit(1)

        print(f"🔥 Injecting {args.injection_type} into {args.layer}")
        result = engine.inject_chaos_and_test(args.layer, args.injection_type)

        print(f"\n📊 Results:")
        print(f"Success: {result.success}")
        print(f"Affected Layers: {result.affected_layers}")
        print(f"Expected Affected: {result.expected_affected}")
        print(f"Violations: {result.dependency_violations}")

    elif args.command == 'validate-independence':
        print("🚀 Starting comprehensive architectural independence validation")
        results = engine.validate_architectural_independence()

        summary = results['summary']
        print(f"\n📊 Validation Complete!")
        print(f"Total Injections: {summary['total_injections']}")
        print(f"Successful: {summary['successful_injections']}")
        print(f"Violations: {summary['total_violations']}")

        if summary['total_violations'] == 0:
            print("✅ All architectural boundaries properly maintained!")
        else:
            print("❌ Architectural violations detected - review results")

    elif args.command == 'chaos-report':
        print("📄 Generating chaos engineering report")
        report = engine.generate_chaos_report(args.results_file)
        print(report)


if __name__ == "__main__":
    main()