"""QA Manager for handling testing, quality assurance, and regression prevention."""
import json
from datetime import datetime
from typing import Dict, List, Optional, Any, Union
from dataclasses import dataclass, asdict
import logging
import re
from pathlib import Path

logger = logging.getLogger(__name__)

@dataclass
class TestCase:
    """Represents a test case with steps and expected results."""
    id: str
    title: str
    description: str
    requirement_ids: List[str]
    component_id: str
    test_type: str  # 'unit', 'integration', 'e2e', 'performance'
    priority: int
    steps: List[str]
    expected_results: List[str]
    prerequisites: List[str]
    automated: bool
    test_data: Dict[str, Any]
    created_at: str
    updated_at: str

@dataclass
class TestRun:
    """Represents a test execution run."""
    id: str
    test_case_id: str
    executed_by: str
    status: str
    start_time: str
    end_time: Optional[str] = None
    actual_results: Optional[str] = None
    error_logs: Optional[str] = None
    screenshots: List[str] = None
    environment: Dict[str, str] = None

@dataclass
class QualityMetrics:
    """Represents quality metrics for a component or feature."""
    code_coverage: float
    test_coverage: float
    bug_density: float
    technical_debt_ratio: float
    performance_score: float
    security_score: float
    maintainability_index: float
    timestamp: str

class QAManager:
    """Manages testing, quality assurance, and regression prevention."""
    
    def __init__(self, context_manager, intelligence_manager,
                 product_manager, engineering_manager):
        """Initialize the QA Manager."""
        self.context_manager = context_manager
        self.intelligence_manager = intelligence_manager
        self.product_manager = product_manager
        self.engineering_manager = engineering_manager
        self.test_cases: Dict[str, TestCase] = {}
        self.test_runs: Dict[str, List[TestRun]] = {}
        self.quality_metrics: Dict[str, List[QualityMetrics]] = {}
        self.load_qa_data()

    def load_qa_data(self):
        """Load QA data from context database."""
        context = self.context_manager.get_project_context()
        qa_data = context.get('qa_data', {})
        
        # Load test cases
        for case_dict in qa_data.get('test_cases', []):
            test_case = TestCase(**case_dict)
            self.test_cases[test_case.id] = test_case
            
        # Load test runs
        for run_dict in qa_data.get('test_runs', []):
            test_run = TestRun(**run_dict)
            if test_run.test_case_id not in self.test_runs:
                self.test_runs[test_run.test_case_id] = []
            self.test_runs[test_run.test_case_id].append(test_run)
            
        # Load quality metrics
        for component_id, metrics_list in qa_data.get('quality_metrics', {}).items():
            self.quality_metrics[component_id] = [
                QualityMetrics(**m) for m in metrics_list
            ]

    def save_qa_data(self):
        """Save QA data to context database."""
        qa_data = {
            'test_cases': [asdict(case) for case in self.test_cases.values()],
            'test_runs': [
                asdict(run)
                for runs in self.test_runs.values()
                for run in runs
            ],
            'quality_metrics': {
                component_id: [asdict(m) for m in metrics]
                for component_id, metrics in self.quality_metrics.items()
            }
        }
        
        self.context_manager.update_project_context({
            'qa_data': qa_data
        })

    def create_test_case(self, title: str, description: str,
                        requirement_ids: List[str], component_id: str,
                        test_type: str, steps: List[str],
                        expected_results: List[str],
                        prerequisites: List[str] = None,
                        priority: int = 1,
                        automated: bool = False,
                        test_data: Dict[str, Any] = None) -> TestCase:
        """Create a new test case."""
        test_id = f"test_{len(self.test_cases) + 1}"
        now = datetime.now().isoformat()
        
        test_case = TestCase(
            id=test_id,
            title=title,
            description=description,
            requirement_ids=requirement_ids,
            component_id=component_id,
            test_type=test_type,
            priority=priority,
            steps=steps,
            expected_results=expected_results,
            prerequisites=prerequisites or [],
            automated=automated,
            test_data=test_data or {},
            created_at=now,
            updated_at=now
        )
        
        self.test_cases[test_id] = test_case
        self.save_qa_data()
        
        # Add insight about new test case
        self.context_manager.add_insight(
            f"Created test case: {title}",
            'qa',
            'qa_manager'
        )
        
        return test_case

    def record_test_run(self, test_case_id: str, executed_by: str,
                       status: str, actual_results: Optional[str] = None,
                       error_logs: Optional[str] = None,
                       screenshots: List[str] = None,
                       environment: Dict[str, str] = None) -> TestRun:
        """Record a test execution run."""
        if test_case_id not in self.test_cases:
            raise ValueError(f"Test case {test_case_id} not found")
            
        run_id = f"run_{test_case_id}_{len(self.test_runs.get(test_case_id, [])) + 1}"
        now = datetime.now().isoformat()
        
        test_run = TestRun(
            id=run_id,
            test_case_id=test_case_id,
            executed_by=executed_by,
            status=status,
            start_time=now,
            end_time=now if status != "running" else None,
            actual_results=actual_results,
            error_logs=error_logs,
            screenshots=screenshots or [],
            environment=environment or {}
        )
        
        if test_case_id not in self.test_runs:
            self.test_runs[test_case_id] = []
        self.test_runs[test_case_id].append(test_run)
        self.save_qa_data()
        
        # Add insight about test run
        self.context_manager.add_insight(
            f"Test run completed: {self.test_cases[test_case_id].title} - {status}",
            'qa',
            'qa_manager'
        )
        
        return test_run

    def update_quality_metrics(self, component_id: str,
                             metrics: Dict[str, float]) -> QualityMetrics:
        """Update quality metrics for a component."""
        quality_metrics = QualityMetrics(
            code_coverage=metrics.get('code_coverage', 0.0),
            test_coverage=metrics.get('test_coverage', 0.0),
            bug_density=metrics.get('bug_density', 0.0),
            technical_debt_ratio=metrics.get('technical_debt_ratio', 0.0),
            performance_score=metrics.get('performance_score', 0.0),
            security_score=metrics.get('security_score', 0.0),
            maintainability_index=metrics.get('maintainability_index', 0.0),
            timestamp=datetime.now().isoformat()
        )
        
        if component_id not in self.quality_metrics:
            self.quality_metrics[component_id] = []
        self.quality_metrics[component_id].append(quality_metrics)
        self.save_qa_data()
        
        return quality_metrics

    def analyze_test_coverage(self, component_id: str) -> Dict[str, Any]:
        """Analyze test coverage for a component."""
        if component_id not in self.engineering_manager.components:
            raise ValueError(f"Component {component_id} not found")
            
        component = self.engineering_manager.components[component_id]
        requirements = self.product_manager.requirements
        
        # Get all test cases for component
        component_tests = [
            test for test in self.test_cases.values()
            if test.component_id == component_id
        ]
        
        # Analyze coverage
        coverage_analysis = {
            'total_requirements': len(requirements),
            'covered_requirements': set(),
            'test_types': {
                'unit': 0,
                'integration': 0,
                'e2e': 0,
                'performance': 0
            },
            'automated_tests': 0,
            'manual_tests': 0,
            'last_execution': None
        }
        
        for test in component_tests:
            # Count test types
            coverage_analysis['test_types'][test.test_type] += 1
            
            # Count automation status
            if test.automated:
                coverage_analysis['automated_tests'] += 1
            else:
                coverage_analysis['manual_tests'] += 1
            
            # Track covered requirements
            coverage_analysis['covered_requirements'].update(test.requirement_ids)
            
            # Check last execution
            if test.id in self.test_runs:
                last_run = max(
                    self.test_runs[test.id],
                    key=lambda r: r.start_time
                )
                if (not coverage_analysis['last_execution'] or
                    last_run.start_time > coverage_analysis['last_execution']):
                    coverage_analysis['last_execution'] = last_run.start_time
        
        coverage_analysis['covered_requirements'] = list(
            coverage_analysis['covered_requirements']
        )
        coverage_analysis['coverage_percentage'] = (
            len(coverage_analysis['covered_requirements']) /
            coverage_analysis['total_requirements']
            if coverage_analysis['total_requirements'] > 0 else 0
        ) * 100
        
        return coverage_analysis

    def get_test_history(self, test_case_id: str) -> List[Dict[str, Any]]:
        """Get execution history for a test case."""
        if test_case_id not in self.test_cases:
            raise ValueError(f"Test case {test_case_id} not found")
            
        test_case = self.test_cases[test_case_id]
        runs = self.test_runs.get(test_case_id, [])
        
        history = []
        for run in sorted(runs, key=lambda r: r.start_time):
            history.append({
                'run': asdict(run),
                'test_case': asdict(test_case),
                'duration': (
                    datetime.fromisoformat(run.end_time) -
                    datetime.fromisoformat(run.start_time)
                ).total_seconds() if run.end_time else None
            })
        
        return history

    def identify_flaky_tests(self) -> List[Dict[str, Any]]:
        """Identify tests with inconsistent results."""
        flaky_tests = []
        
        for test_id, runs in self.test_runs.items():
            if len(runs) < 3:  # Need at least 3 runs to determine flakiness
                continue
                
            # Calculate failure rate
            recent_runs = sorted(runs, key=lambda r: r.start_time)[-10:]  # Last 10 runs
            failure_rate = sum(
                1 for r in recent_runs
                if r.status == "failed"
            ) / len(recent_runs)
            
            # Check for alternating patterns
            statuses = [r.status for r in recent_runs]
            alternating = any(
                statuses[i] != statuses[i+1]
                for i in range(len(statuses)-1)
            )
            
            if failure_rate > 0.2 and alternating:  # 20% failure rate and alternating results
                flaky_tests.append({
                    'test_case': asdict(self.test_cases[test_id]),
                    'failure_rate': failure_rate,
                    'recent_runs': [asdict(r) for r in recent_runs],
                    'recommendation': self._generate_flaky_test_recommendation(
                        self.test_cases[test_id], recent_runs
                    )
                })
        
        return flaky_tests

    def generate_test_report(self) -> Dict[str, Any]:
        """Generate a comprehensive test report."""
        report = {
            'timestamp': datetime.now().isoformat(),
            'summary': {
                'total_test_cases': len(self.test_cases),
                'total_test_runs': sum(len(runs) for runs in self.test_runs.values()),
                'pass_rate': 0,
                'automation_rate': 0
            },
            'test_types': {
                'unit': 0,
                'integration': 0,
                'e2e': 0,
                'performance': 0
            },
            'components': {},
            'recent_failures': [],
            'flaky_tests': self.identify_flaky_tests()
        }
        
        # Calculate summary metrics
        total_runs = 0
        passed_runs = 0
        automated_tests = 0
        
        for test_case in self.test_cases.values():
            # Count test types
            report['test_types'][test_case.test_type] += 1
            
            # Count automated tests
            if test_case.automated:
                automated_tests += 1
            
            # Process test runs
            if test_case.id in self.test_runs:
                runs = self.test_runs[test_case.id]
                total_runs += len(runs)
                passed_runs += sum(1 for r in runs if r.status == "passed")
                
                # Track recent failures
                recent_failures = [
                    r for r in runs
                    if r.status == "failed" and
                    (datetime.now() - datetime.fromisoformat(r.start_time)).days <= 7
                ]
                if recent_failures:
                    report['recent_failures'].extend([
                        {
                            'test_case': asdict(test_case),
                            'run': asdict(failure)
                        }
                        for failure in recent_failures
                    ])
        
        report['summary']['pass_rate'] = (
            passed_runs / total_runs if total_runs > 0 else 0
        ) * 100
        report['summary']['automation_rate'] = (
            automated_tests / len(self.test_cases)
            if self.test_cases else 0
        ) * 100
        
        # Analyze component-level metrics
        for component_id in self.engineering_manager.components:
            coverage = self.analyze_test_coverage(component_id)
            metrics = self.quality_metrics.get(component_id, [])
            latest_metrics = metrics[-1] if metrics else None
            
            report['components'][component_id] = {
                'coverage_analysis': coverage,
                'quality_metrics': asdict(latest_metrics) if latest_metrics else None
            }
        
        return report

    def _generate_flaky_test_recommendation(self, test_case: TestCase,
                                          recent_runs: List[TestRun]) -> str:
        """Generate recommendations for fixing flaky tests."""
        patterns = []
        
        # Check for timing issues
        timing_variations = []
        for run in recent_runs:
            if run.end_time:
                duration = (
                    datetime.fromisoformat(run.end_time) -
                    datetime.fromisoformat(run.start_time)
                ).total_seconds()
                timing_variations.append(duration)
        
        if timing_variations and max(timing_variations) > 2 * min(timing_variations):
            patterns.append("timing")
        
        # Check for environment dependencies
        env_variations = set()
        for run in recent_runs:
            if run.environment:
                env_key = json.dumps(sorted(run.environment.items()))
                env_variations.add(env_key)
        
        if len(env_variations) > 1:
            patterns.append("environment")
        
        # Check for async/race conditions
        if any(
            run.error_logs and
            re.search(r"race|condition|async|concurrent", run.error_logs.lower())
            for run in recent_runs
        ):
            patterns.append("async")
        
        # Generate recommendation
        recommendations = []
        if "timing" in patterns:
            recommendations.append(
                "Consider adding explicit waits or retry mechanisms"
            )
        if "environment" in patterns:
            recommendations.append(
                "Ensure consistent test environment and cleanup"
            )
        if "async" in patterns:
            recommendations.append(
                "Review async operations and add proper synchronization"
            )
        
        if not recommendations:
            recommendations.append(
                "Review test implementation and add detailed logging"
            )
        
        return " | ".join(recommendations) 