#!/usr/bin/env python3
"""Differential testing framework comparing Python croniter and Rust croniter_rs."""

import sys
import json
import time
import traceback
from datetime import datetime
from typing import List, Dict, Any, Optional, Tuple
from pathlib import Path

# Import both implementations
import croniter as py_croniter
import croniter_rs as rs_croniter


class DifferentialTester:
    """Compare Python croniter and Rust croniter_rs implementations."""

    def __init__(self, log_file: str = "/tmp/differential_tests.log"):
        self.log_file = log_file
        self.results = {
            'passed': 0,
            'failed': 0,
            'errors': 0,
            'skipped': 0,
            'failures': []
        }
        self.log_handle = open(log_file, 'w')

    def __del__(self):
        if hasattr(self, 'log_handle'):
            self.log_handle.close()

    def log(self, message: str):
        """Log message to file."""
        self.log_handle.write(f"{message}\n")
        self.log_handle.flush()

    def compare_get_next(self, expr: str, start: datetime, iterations: int = 10,
                        hash_id: Optional[str] = None, ret_type=None) -> Dict[str, Any]:
        """Compare get_next() between implementations."""
        result = {
            'expr': expr,
            'start': start.isoformat(),
            'iterations': iterations,
            'hash_id': hash_id,
            'passed': False,
            'error': None,
            'py_results': [],
            'rs_results': [],
            'differences': []
        }

        # Try to initialize Python croniter
        py_init_error = None
        py_cron = None
        try:
            py_cron = py_croniter.croniter(expr, start, hash_id=hash_id)
        except Exception as e:
            py_init_error = f"{type(e).__name__}: {str(e)}"

        # Try to initialize Rust croniter_rs
        rs_init_error = None
        rs_cron = None
        try:
            rs_cron = rs_croniter.croniter(expr, start, hash_id=hash_id)
        except Exception as e:
            rs_init_error = f"{type(e).__name__}: {str(e)}"

        # If both failed to initialize, check if they failed the same way
        if py_init_error and rs_init_error:
            # Both rejected - extract exception type
            py_exc_type = py_init_error.split(':')[0]
            rs_exc_type = rs_init_error.split(':')[0]
            if py_exc_type == rs_exc_type:
                # Both rejected with same exception type - this is a pass
                result['passed'] = True
                result['py_results'] = [py_init_error]
                result['rs_results'] = [rs_init_error]
            else:
                result['differences'].append(f"Init error mismatch: py={py_exc_type}, rs={rs_exc_type}")
                result['py_results'] = [py_init_error]
                result['rs_results'] = [rs_init_error]
            return result

        # If only one failed to initialize, that's a difference
        if py_init_error or rs_init_error:
            result['differences'].append(f"Init: py={'error' if py_init_error else 'ok'}, rs={'error' if rs_init_error else 'ok'}")
            result['py_results'] = [py_init_error] if py_init_error else []
            result['rs_results'] = [rs_init_error] if rs_init_error else []
            return result

        # Both initialized successfully, compare get_next()
        try:
            py_results = []
            for _ in range(iterations):
                try:
                    next_val = py_cron.get_next(ret_type or datetime)
                    if isinstance(next_val, datetime):
                        py_results.append(next_val.isoformat())
                    else:
                        py_results.append(float(next_val))
                except Exception as e:
                    py_results.append(f"ERROR: {str(e)}")
                    break

            rs_results = []
            for _ in range(iterations):
                try:
                    next_val = rs_cron.get_next(ret_type or datetime)
                    if isinstance(next_val, datetime):
                        rs_results.append(next_val.isoformat())
                    else:
                        rs_results.append(float(next_val))
                except Exception as e:
                    rs_results.append(f"ERROR: {str(e)}")
                    break

            result['py_results'] = py_results
            result['rs_results'] = rs_results

            # Compare results
            if len(py_results) != len(rs_results):
                result['differences'].append(f"Length mismatch: py={len(py_results)}, rs={len(rs_results)}")
            else:
                for i, (py_val, rs_val) in enumerate(zip(py_results, rs_results)):
                    if isinstance(py_val, str) and py_val.startswith("ERROR"):
                        if not (isinstance(rs_val, str) and rs_val.startswith("ERROR")):
                            result['differences'].append(f"Iteration {i}: py error but rs success")
                    elif isinstance(rs_val, str) and rs_val.startswith("ERROR"):
                        result['differences'].append(f"Iteration {i}: rs error but py success")
                    elif isinstance(py_val, float) and isinstance(rs_val, float):
                        if abs(py_val - rs_val) > 0.001:
                            result['differences'].append(f"Iteration {i}: py={py_val}, rs={rs_val}")
                    elif py_val != rs_val:
                        result['differences'].append(f"Iteration {i}: py={py_val}, rs={rs_val}")

            result['passed'] = len(result['differences']) == 0

        except Exception as e:
            result['error'] = f"{type(e).__name__}: {str(e)}"
            result['traceback'] = traceback.format_exc()

        return result

    def compare_get_prev(self, expr: str, start: datetime, iterations: int = 10,
                        hash_id: Optional[str] = None, ret_type=None) -> Dict[str, Any]:
        """Compare get_prev() between implementations."""
        result = {
            'expr': expr,
            'start': start.isoformat(),
            'iterations': iterations,
            'hash_id': hash_id,
            'passed': False,
            'error': None,
            'py_results': [],
            'rs_results': [],
            'differences': []
        }

        # Try to initialize Python croniter
        py_init_error = None
        py_cron = None
        try:
            py_cron = py_croniter.croniter(expr, start, hash_id=hash_id)
        except Exception as e:
            py_init_error = f"{type(e).__name__}: {str(e)}"

        # Try to initialize Rust croniter_rs
        rs_init_error = None
        rs_cron = None
        try:
            rs_cron = rs_croniter.croniter(expr, start, hash_id=hash_id)
        except Exception as e:
            rs_init_error = f"{type(e).__name__}: {str(e)}"

        # If both failed to initialize, check if they failed the same way
        if py_init_error and rs_init_error:
            py_exc_type = py_init_error.split(':')[0]
            rs_exc_type = rs_init_error.split(':')[0]
            if py_exc_type == rs_exc_type:
                result['passed'] = True
                result['py_results'] = [py_init_error]
                result['rs_results'] = [rs_init_error]
            else:
                result['differences'].append(f"Init error mismatch: py={py_exc_type}, rs={rs_exc_type}")
                result['py_results'] = [py_init_error]
                result['rs_results'] = [rs_init_error]
            return result

        # If only one failed to initialize, that's a difference
        if py_init_error or rs_init_error:
            result['differences'].append(f"Init: py={'error' if py_init_error else 'ok'}, rs={'error' if rs_init_error else 'ok'}")
            result['py_results'] = [py_init_error] if py_init_error else []
            result['rs_results'] = [rs_init_error] if rs_init_error else []
            return result

        # Both initialized successfully, compare get_prev()
        try:
            py_results = []
            for _ in range(iterations):
                try:
                    prev_val = py_cron.get_prev(ret_type or datetime)
                    if isinstance(prev_val, datetime):
                        py_results.append(prev_val.isoformat())
                    else:
                        py_results.append(float(prev_val))
                except Exception as e:
                    py_results.append(f"ERROR: {str(e)}")
                    break

            rs_results = []
            for _ in range(iterations):
                try:
                    prev_val = rs_cron.get_prev(ret_type or datetime)
                    if isinstance(prev_val, datetime):
                        rs_results.append(prev_val.isoformat())
                    else:
                        rs_results.append(float(prev_val))
                except Exception as e:
                    rs_results.append(f"ERROR: {str(e)}")
                    break

            result['py_results'] = py_results
            result['rs_results'] = rs_results

            # Compare results
            if len(py_results) != len(rs_results):
                result['differences'].append(f"Length mismatch: py={len(py_results)}, rs={len(rs_results)}")
            else:
                for i, (py_val, rs_val) in enumerate(zip(py_results, rs_results)):
                    if isinstance(py_val, str) and py_val.startswith("ERROR"):
                        if not (isinstance(rs_val, str) and rs_val.startswith("ERROR")):
                            result['differences'].append(f"Iteration {i}: py error but rs success")
                    elif isinstance(rs_val, str) and rs_val.startswith("ERROR"):
                        result['differences'].append(f"Iteration {i}: rs error but py success")
                    elif isinstance(py_val, float) and isinstance(rs_val, float):
                        if abs(py_val - rs_val) > 0.001:
                            result['differences'].append(f"Iteration {i}: py={py_val}, rs={rs_val}")
                    elif py_val != rs_val:
                        result['differences'].append(f"Iteration {i}: py={py_val}, rs={rs_val}")

            result['passed'] = len(result['differences']) == 0

        except Exception as e:
            result['error'] = f"{type(e).__name__}: {str(e)}"
            result['traceback'] = traceback.format_exc()

        return result

    def compare_match(self, expr: str, dt: datetime, day_or: bool = True) -> Dict[str, Any]:
        """Compare match() between implementations."""
        result = {
            'expr': expr,
            'dt': dt.isoformat(),
            'day_or': day_or,
            'passed': False,
            'error': None,
            'py_result': None,
            'rs_result': None
        }

        try:
            # Python croniter
            try:
                py_result = py_croniter.croniter.match(expr, dt, day_or=day_or)
                result['py_result'] = py_result
            except Exception as e:
                result['py_result'] = f"ERROR: {str(e)}"

            # Rust croniter_rs
            try:
                rs_result = rs_croniter.croniter.match(expr, dt, day_or=day_or)
                result['rs_result'] = rs_result
            except Exception as e:
                result['rs_result'] = f"ERROR: {str(e)}"

            # Compare
            if isinstance(result['py_result'], str) and result['py_result'].startswith("ERROR"):
                result['passed'] = isinstance(result['rs_result'], str) and result['rs_result'].startswith("ERROR")
            elif isinstance(result['rs_result'], str) and result['rs_result'].startswith("ERROR"):
                result['passed'] = False
            else:
                result['passed'] = result['py_result'] == result['rs_result']

        except Exception as e:
            result['error'] = f"{type(e).__name__}: {str(e)}"
            result['traceback'] = traceback.format_exc()

        return result

    def compare_expand(self, expr: str, hash_id: Optional[str] = None) -> Dict[str, Any]:
        """Compare expand() between implementations."""
        result = {
            'expr': expr,
            'hash_id': hash_id,
            'passed': False,
            'error': None,
            'py_result': None,
            'rs_result': None
        }

        try:
            # Python croniter - needs hash_id as bytes
            py_hash_id = hash_id.encode() if isinstance(hash_id, str) else hash_id
            try:
                py_result = py_croniter.croniter.expand(expr, hash_id=py_hash_id)
                result['py_result'] = str(py_result)
            except Exception as e:
                result['py_result'] = f"ERROR: {str(e)}"

            # Rust croniter_rs
            try:
                rs_result = rs_croniter.croniter.expand(expr, hash_id=hash_id)
                result['rs_result'] = str(rs_result)
            except Exception as e:
                result['rs_result'] = f"ERROR: {str(e)}"

            # Compare (both should succeed or both should fail)
            if isinstance(result['py_result'], str) and result['py_result'].startswith("ERROR"):
                result['passed'] = isinstance(result['rs_result'], str) and result['rs_result'].startswith("ERROR")
            elif isinstance(result['rs_result'], str) and result['rs_result'].startswith("ERROR"):
                result['passed'] = False
            else:
                # For expand, we just check both succeed (exact match is complex due to wildcards)
                result['passed'] = True

        except Exception as e:
            result['error'] = f"{type(e).__name__}: {str(e)}"
            result['traceback'] = traceback.format_exc()

        return result

    def run_test_case(self, test_case: Dict[str, Any]) -> Dict[str, Any]:
        """Run a single test case."""
        expr = test_case['expr']
        start = test_case['start']
        iterations = test_case.get('iterations', 10)
        hash_id = test_case.get('hash_id', None)
        description = test_case.get('description', '')

        self.log(f"\n{'='*80}")
        self.log(f"Test: {description}")
        self.log(f"Expression: {expr}")
        self.log(f"Start: {start}")
        self.log(f"Iterations: {iterations}")

        # Test get_next
        next_result = self.compare_get_next(expr, start, iterations, hash_id)
        self.log(f"get_next: {'PASS' if next_result['passed'] else 'FAIL'}")
        if not next_result['passed']:
            self.log(f"  Differences: {next_result['differences']}")
            if next_result.get('error'):
                self.log(f"  Error: {next_result['error']}")

        # Test get_prev
        prev_result = self.compare_get_prev(expr, start, iterations, hash_id)
        self.log(f"get_prev: {'PASS' if prev_result['passed'] else 'FAIL'}")
        if not prev_result['passed']:
            self.log(f"  Differences: {prev_result['differences']}")
            if prev_result.get('error'):
                self.log(f"  Error: {prev_result['error']}")

        # Test match
        match_result = self.compare_match(expr, start)
        self.log(f"match: {'PASS' if match_result['passed'] else 'FAIL'}")
        if not match_result['passed']:
            self.log(f"  py={match_result['py_result']}, rs={match_result['rs_result']}")

        # Test expand
        expand_result = self.compare_expand(expr, hash_id)
        self.log(f"expand: {'PASS' if expand_result['passed'] else 'FAIL'}")

        # Aggregate results
        all_passed = (next_result['passed'] and prev_result['passed'] and
                     match_result['passed'] and expand_result['passed'])

        if all_passed:
            self.results['passed'] += 1
        else:
            self.results['failed'] += 1
            self.results['failures'].append({
                'description': description,
                'expr': expr,
                'start': start.isoformat(),
                'next_result': next_result,
                'prev_result': prev_result,
                'match_result': match_result,
                'expand_result': expand_result
            })

        return {
            'description': description,
            'passed': all_passed,
            'next_result': next_result,
            'prev_result': prev_result,
            'match_result': match_result,
            'expand_result': expand_result
        }

    def run_test_suite(self, test_cases: List[Dict[str, Any]], suite_name: str = "Test Suite"):
        """Run a suite of test cases."""
        print(f"\n{'='*80}")
        print(f"Running {suite_name}")
        print(f"{'='*80}")

        self.log(f"\n{'='*80}")
        self.log(f"Test Suite: {suite_name}")
        self.log(f"Total test cases: {len(test_cases)}")
        self.log(f"{'='*80}")

        start_time = time.time()

        for i, test_case in enumerate(test_cases):
            try:
                self.run_test_case(test_case)
                if (i + 1) % 10 == 0:
                    print(f"  Progress: {i+1}/{len(test_cases)} tests completed")
            except Exception as e:
                self.results['errors'] += 1
                self.log(f"\nERROR in test case: {test_case.get('description', 'Unknown')}")
                self.log(f"  {type(e).__name__}: {str(e)}")
                self.log(traceback.format_exc())

        elapsed = time.time() - start_time

        print(f"\n{suite_name} Results:")
        print(f"  Passed: {self.results['passed']}")
        print(f"  Failed: {self.results['failed']}")
        print(f"  Errors: {self.results['errors']}")
        print(f"  Time: {elapsed:.2f}s")

        self.log(f"\n{suite_name} Summary:")
        self.log(f"  Passed: {self.results['passed']}")
        self.log(f"  Failed: {self.results['failed']}")
        self.log(f"  Errors: {self.results['errors']}")
        self.log(f"  Time: {elapsed:.2f}s")

    def save_results(self, output_file: str):
        """Save test results to JSON file."""
        with open(output_file, 'w') as f:
            json.dump(self.results, f, indent=2, default=str)
        print(f"\nResults saved to: {output_file}")

    def print_summary(self):
        """Print test summary."""
        total = self.results['passed'] + self.results['failed'] + self.results['errors']
        print(f"\n{'='*80}")
        print("FINAL SUMMARY")
        print(f"{'='*80}")
        print(f"Total tests: {total}")
        print(f"Passed: {self.results['passed']} ({100*self.results['passed']/total if total > 0 else 0:.1f}%)")
        print(f"Failed: {self.results['failed']} ({100*self.results['failed']/total if total > 0 else 0:.1f}%)")
        print(f"Errors: {self.results['errors']} ({100*self.results['errors']/total if total > 0 else 0:.1f}%)")
        print(f"\nDetailed log: {self.log_file}")

        if self.results['failures']:
            print(f"\nFirst 5 failures:")
            for i, failure in enumerate(self.results['failures'][:5]):
                print(f"\n{i+1}. {failure['description']}")
                print(f"   Expression: {failure['expr']}")
                if failure['next_result']['differences']:
                    print(f"   get_next differences: {failure['next_result']['differences'][:3]}")
                if failure['prev_result']['differences']:
                    print(f"   get_prev differences: {failure['prev_result']['differences'][:3]}")


if __name__ == '__main__':
    print("Differential Tester - Standalone Test")
    tester = DifferentialTester()

    # Simple test
    test_cases = [
        {
            'expr': '*/5 * * * *',
            'start': datetime(2020, 1, 1, 0, 0, 0),
            'iterations': 5,
            'description': 'Simple every 5 minutes'
        }
    ]

    tester.run_test_suite(test_cases, "Smoke Test")
    tester.print_summary()
