"""
Comprehensive error handling testing for rateloader.

Tests error scenarios and recovery across all components.
Validates that the application stays alive and logs errors appropriately.
"""

import os
import sys
import time
import tempfile
import shutil
import unittest
from unittest.mock import Mock, patch, MagicMock
import threading
import json

# Add current directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from core.config import get_config, reload_config
from core.logger import setup_logging, get_logger_manager
from core.oracle import create_oracle_wrapper, OracleWrapper
from core.task_queue import get_task_queue, start_task_queue, stop_task_queue
from core.scheduler import get_scheduler, start_scheduler, stop_scheduler
from core.http_server import get_http_server, start_server, stop_server
from core.tasks import ExportMailerTask, PriceExportTask
from features.export_mailer import ExportMailer
from features.price_export import PriceExport
from features.rate_loader_logic import process_prices, parse_json_data


class TestErrorHandling(unittest.TestCase):
    """Comprehensive error handling tests for rateloader."""

    @classmethod
    def setUpClass(cls):
        """Set up test environment."""
        # Create temporary directory for test files
        cls.test_dir = tempfile.mkdtemp()
        cls.original_dir = os.getcwd()
        os.chdir(cls.test_dir)
        
        # Create log directory
        os.makedirs("log", exist_ok=True)
        
        # Backup original settings.ini if exists
        cls.settings_backup = None
        if os.path.exists("settings.ini"):
            cls.settings_backup = "settings.ini.backup"
            shutil.copy("settings.ini", cls.settings_backup)
        
        # Initialize test configuration
        cls._setup_test_config()
        
        # Initialize logging
        cls.logger_manager = setup_logging()
        
        print(f"Error handling test environment set up in: {cls.test_dir}")

    @classmethod
    def tearDownClass(cls):
        """Clean up test environment."""
        # Restore original directory
        os.chdir(cls.original_dir)
        
        # Clean up test directory
        shutil.rmtree(cls.test_dir, ignore_errors=True)
        
        print(f"Error handling test environment cleaned up: {cls.test_dir}")

    def setUp(self):
        """Set up for each test."""
        # Reload configuration
        reload_config()
        self.config = get_config()

    def tearDown(self):
        """Clean up after each test."""
        # Stop all components
        try:
            stop_server()
            stop_scheduler()
            stop_task_queue()
        except:
            pass
        
        # Wait for threads to stop
        time.sleep(0.5)

    @classmethod
    def _setup_test_config(cls):
        """Set up test configuration."""
        config = get_config()
        
        # Test configuration values
        test_config = {
            "hotel_key": "TESTHOTEL",
            "oracleconnectstring": "test/test@test",
            "nls_lang": "AMERICAN_AMERICA.CL8MSWIN1251",
            "file_encoding": "utf-8",
            "url": "http://test.com/select.sql",
            "block-url": "http://test.com/selectBlocks.sql",
            "sendfrom": "test@example.com",
            "sendto": "recipient@example.com",
            "mailserver": "smtp.test.com",
            "port": "587",
            "pwd": "testpass",
            "ssl": "1",
            "api_url": "http://test.com/api/prices",
            "token": "test-token",
            "months": "1",
            "server_port": "8001",
            "schedule_hours": "9,15,21"
        }
        
        for key, value in test_config.items():
            config.set(key, value)

    def test_config_missing_file_handling(self):
        """Test handling of missing configuration file."""
        # Remove settings.ini
        if os.path.exists("settings.ini"):
            os.remove("settings.ini")
        
        # Try to get configuration (should create new file)
        config = get_config()
        
        # Verify file was created with defaults
        self.assertTrue(os.path.exists("settings.ini"))
        self.assertEqual(config.get("oracleconnectstring"), "v8live/live@v8")
        self.assertEqual(config.get("server_port"), "8000")

    def test_config_missing_keys_handling(self):
        """Test handling of missing configuration keys."""
        # Create incomplete settings.ini
        with open("settings.ini", "w") as f:
            f.write("[settings]\n")
            f.write("oracleconnectstring = test/test@test\n")
            # Missing other keys
        
        # Reload configuration
        reload_config()
        config = get_config()
        
        # Verify missing keys were added with defaults
        self.assertEqual(config.get("server_port"), "8000")
        self.assertEqual(config.get("schedule_hours"), "8,12,15,18,20")

    def test_config_invalid_values_handling(self):
        """Test handling of invalid configuration values."""
        config = get_config()
        
        # Test invalid integer values
        self.assertEqual(config.get_int("invalid_key", fallback=100), 100)
        self.assertEqual(config.get_int("port", fallback=25), 25)  # Valid
        
        # Test invalid boolean values
        self.assertFalse(config.get_bool("invalid_key", fallback=False))
        self.assertTrue(config.get_bool("ssl", fallback=False))  # Valid
        
        # Test invalid float values
        self.assertEqual(config.get_float("invalid_key", fallback=1.5), 1.5)

    def test_oracle_connection_error_handling(self):
        """Test Oracle connection error handling."""
        with patch('cx_Oracle.connect') as mock_connect:
            # Mock connection failure
            mock_connect.side_effect = Exception("Database connection failed")
            
            # Try to connect
            connection = create_oracle_wrapper().connect()
            
            # Should return None on failure
            self.assertIsNone(connection)
            
            # Verify error was logged
            self.assertTrue(os.path.exists("log/rateloader.log"))

    def test_oracle_query_error_handling(self):
        """Test Oracle query error handling."""
        with patch('features.export_mailer.create_oracle_wrapper') as mock_oracle:
            # Mock Oracle wrapper to raise exception
            mock_wrapper = Mock()
            mock_oracle.return_value = mock_wrapper
            mock_wrapper.is_connected.return_value = True
            mock_wrapper.connect.side_effect = Exception("Query failed")
            
            # Create ExportMailer and try to run
            export_mailer = ExportMailer()
            success, main_csv, blocks_csv = export_mailer.run_export()
            
            # Should handle error gracefully
            self.assertFalse(success)
            self.assertIsNone(main_csv)
            self.assertIsNone(blocks_csv)

    def test_oracle_stream_query_error_handling(self):
        """Test Oracle stream query error handling."""
        with patch('core.oracle.cx_Oracle') as mock_cx:
            # Mock cursor to raise exception
            mock_cursor = Mock()
            mock_cursor.execute.side_effect = Exception("Stream query failed")
            mock_connection = Mock()
            mock_connection.cursor.return_value = mock_cursor
            mock_cx.connect.return_value = mock_connection
            
            # Try stream query
            results = list(create_oracle_wrapper().stream_query("SELECT 1 FROM DUAL"))
            
            # Should return empty list on error
            self.assertEqual(results, [])

    def test_task_queue_worker_error_handling(self):
        """Test task queue worker error handling."""
        # Start task queue
        self.assertTrue(start_task_queue())
        
        # Create task that raises exception
        class FailingTask:
            def __init__(self):
                self.name = "FailingTask"
                self.error_message = None
                self.status = "pending"
            
            def execute(self):
                raise Exception("Task execution failed")
            
            def mark_started(self):
                self.status = "running"
            
            def mark_completed(self, success, error_message=None):
                self.status = "completed" if success else "failed"
                self.error_message = error_message
        
        # Enqueue failing task
        failing_task = FailingTask()
        task_queue = get_task_queue()
        
        # Create a mock task that behaves like our failing task
        from core.task_queue import Task
        
        class MockFailingTask(Task):
            def execute(self):
                raise Exception("Task execution failed")
        
        mock_task = MockFailingTask("FailingTask")
        self.assertTrue(task_queue.enqueue(mock_task))
        
        # Wait for task to be processed
        time.sleep(1)
        
        # Verify task queue is still running (worker didn't crash)
        self.assertTrue(task_queue.is_running())
        
        # Verify error was recorded
        status = task_queue.get_status()
        self.assertGreater(status["statistics"]["total_failed"], 0)

    def test_scheduler_error_handling(self):
        """Test scheduler error handling."""
        # Create scheduler
        scheduler = get_scheduler()
        
        # Mock task queue to raise exception
        scheduler.task_queue = Mock()
        scheduler.task_queue.enqueue.side_effect = Exception("Task enqueue failed")
        
        # Try to enqueue scheduled tasks
        scheduler._enqueue_scheduled_tasks()
        
        # Should handle error gracefully (no exception raised)
        self.assertTrue(True)  # If we reach here, error was handled

    def test_http_server_error_handling(self):
        """Test HTTP server error handling."""
        with patch('core.http_server.create_oracle_wrapper') as mock_oracle:
            # Mock Oracle wrapper
            mock_wrapper = Mock()
            mock_oracle.return_value = mock_wrapper
            mock_wrapper.is_connected.return_value = True
            
            # Start HTTP server
            self.assertTrue(start_server())
            time.sleep(0.5)  # Give server time to start
            
            try:
                import requests
                
                # Test invalid endpoint
                response = requests.get("http://localhost:8001/invalid", timeout=5)
                self.assertEqual(response.status_code, 404)
                
                # Test invalid JSON
                response = requests.post(
                    "http://localhost:8001/load",
                    data="invalid json",
                    timeout=5
                )
                self.assertEqual(response.status_code, 400)
                
            except requests.exceptions.ConnectionError:
                self.skipTest("HTTP server not available for testing")

    def test_export_mailer_network_error_handling(self):
        """Test ExportMailer network error handling."""
        with patch('urllib.request.urlretrieve') as mock_download:
            # Mock network error
            mock_download.side_effect = Exception("Network error")
            
            # Create ExportMailer
            export_mailer = ExportMailer()
            
            # Try to download SQL file
            success = export_mailer.download_sql_file("http://test.com/test.sql", "test.sql")
            
            # Should handle error gracefully
            self.assertFalse(success)
            
            # Verify error was logged
            self.assertTrue(os.path.exists("log/hotellabexport.log"))

    def test_export_mailer_email_error_handling(self):
        """Test ExportMailer email error handling."""
        with patch('core.mailer.send_mail') as mock_send_mail:
            # Mock email error
            mock_send_mail.return_value = False
            
            # Create ExportMailer
            export_mailer = ExportMailer()
            
            # Try to send emails
            success = export_mailer.send_emails("test.csv", "blocks.csv", "TEST")
            
            # Should handle email failure gracefully
            self.assertFalse(success)

    def test_price_export_api_error_handling(self):
        """Test PriceExport API error handling."""
        with patch('features.price_export.create_oracle_wrapper') as mock_oracle:
            with patch('requests.post') as mock_post:
                # Mock Oracle wrapper
                mock_wrapper = Mock()
                mock_oracle.return_value = mock_wrapper
                mock_wrapper.is_connected.return_value = True
                mock_wrapper.connect.return_value = True
                mock_wrapper.execute_scalar.return_value = '{"test": "data"}'
                
                # Mock API error
                mock_response = Mock()
                mock_response.status_code = 500
                mock_response.text = "Internal Server Error"
                mock_post.return_value = mock_response
                
                # Create PriceExport
                price_export = PriceExport()
                
                # Try to export prices
                success = price_export.export_prices_for_date("01.01.2023")
                
                # Should handle API error gracefully
                self.assertFalse(success)
                
                # Verify error was logged
                self.assertTrue(os.path.exists("log/hotellabpriceexport.log"))

    def test_price_export_json_error_handling(self):
        """Test PriceExport JSON error handling."""
        with patch('features.price_export.create_oracle_wrapper') as mock_oracle:
            # Mock Oracle wrapper
            mock_wrapper = Mock()
            mock_oracle.return_value = mock_wrapper
            mock_wrapper.is_connected.return_value = True
            mock_wrapper.connect.return_value = True
            mock_wrapper.execute_scalar.return_value = "invalid json"
            
            # Create PriceExport
            price_export = PriceExport()
            
            # Try to export prices
            success = price_export.export_prices_for_date("01.01.2023")
            
            # Should handle JSON error gracefully
            self.assertFalse(success)

    def test_rate_loader_json_parsing_error_handling(self):
        """Test RateLoader JSON parsing error handling."""
        # Test invalid JSON
        invalid_json = "invalid json string"
        
        # Try to parse
        result = parse_json_data(invalid_json)
        
        # Should return None on error
        self.assertIsNone(result)

    def test_rate_loader_missing_data_error_handling(self):
        """Test RateLoader missing data error handling."""
        # Test missing prices key
        invalid_data = {"other_key": "value"}
        
        # Try to process
        success = process_prices(invalid_data)
        
        # Should handle error gracefully
        self.assertFalse(success)

    def test_rate_loader_invalid_price_data_error_handling(self):
        """Test RateLoader invalid price data error handling."""
        # Test invalid price data
        invalid_data = {
            "prices": [
                {
                    "date": "2023-01-01",
                    # Missing required fields
                }
            ]
        }
        
        # Try to process
        success = process_prices(invalid_data)
        
        # Should handle error gracefully
        self.assertFalse(success)

    def test_logging_error_handling(self):
        """Test logging error handling."""
        # Try to log to non-existent logger
        logger_manager = get_logger_manager()
        
        # Should not raise exception
        logger_manager.log_info("non_existent_logger", "Test message")
        logger_manager.log_error("non_existent_logger", "Test error")
        logger_manager.log_exception("non_existent_logger", "Test exception", Exception("Test"))

    def test_file_io_error_handling(self):
        """Test file I/O error handling."""
        # Try to read non-existent file
        try:
            with open("non_existent_file.txt", "r") as f:
                content = f.read()
        except FileNotFoundError:
            # Expected error
            pass
        else:
            self.fail("Should have raised FileNotFoundError")

    def test_memory_error_handling(self):
        """Test memory error handling."""
        # This test simulates memory pressure
        large_list = []
        
        try:
            # Try to allocate large amount of memory
            for i in range(1000000):
                large_list.append("x" * 1000)
        except MemoryError:
            # Should handle memory error gracefully
            pass
        finally:
            # Clean up
            del large_list

    def test_thread_error_handling(self):
        """Test thread error handling."""
        # Create thread that raises exception
        def failing_thread():
            raise Exception("Thread error")
        
        thread = threading.Thread(target=failing_thread)
        thread.start()
        thread.join()
        
        # Exception in thread should not crash main application
        self.assertTrue(True)  # If we reach here, main thread is still running

    def test_gui_error_handling(self):
        """Test GUI error handling."""
        # Mock tkinter to avoid actual GUI
        with patch('tkinter.Tk'):
            with patch('gui.app.get_task_queue') as mock_queue:
                with patch('gui.app.get_scheduler') as mock_scheduler:
                    with patch('gui.app.get_http_server') as mock_server:
                        with patch('gui.app.get_logger_manager') as mock_logger:
                            with patch('gui.app.get_config'):
                                # Mock components to raise exceptions
                                mock_queue.side_effect = Exception("Queue error")
                                
                                try:
                                    from gui.app import RateLoaderGUI
                                    gui = RateLoaderGUI()
                                    
                                    # Should handle error during initialization
                                    # (This might not work perfectly due to Tkinter)
                                    
                                except Exception:
                                    # Expected due to mocked components
                                    pass

    def test_concurrent_error_handling(self):
        """Test concurrent error handling."""
        # Start multiple components that might fail
        errors = []
        
        def failing_component(component_name):
            try:
                if component_name == "oracle":
                    # Simulate Oracle error
                    raise Exception("Oracle connection failed")
                elif component_name == "api":
                    # Simulate API error
                    raise Exception("API request failed")
                elif component_name == "file":
                    # Simulate file error
                    raise Exception("File access denied")
            except Exception as e:
                errors.append(f"{component_name}: {str(e)}")
        
        # Start multiple failing threads
        threads = []
        for component in ["oracle", "api", "file"]:
            thread = threading.Thread(target=failing_component, args=(component,))
            threads.append(thread)
            thread.start()
        
        # Wait for all threads to complete
        for thread in threads:
            thread.join()
        
        # Verify all errors were caught
        self.assertEqual(len(errors), 3)
        for error in errors:
            self.assertIn("failed", error.lower())

    def test_cascading_error_handling(self):
        """Test cascading error handling."""
        # Test that one component failure doesn't cascade to others
        
        # Mock ExportMailer to fail
        with patch('features.export_mailer.ExportMailer.run_export_mailer') as mock_export:
            mock_export.return_value = False
            
            # Create ExportMailerTask
            task = ExportMailerTask()
            
            # Execute task
            success = task.execute()
            
            # Task should fail but not crash
            self.assertFalse(success)
            self.assertIsNotNone(task.error_message)

    def test_recovery_after_error(self):
        """Test recovery after error."""
        # Start task queue
        self.assertTrue(start_task_queue())
        
        # Create failing task
        class FailingThenSucceedingTask:
            def __init__(self):
                self.call_count = 0
                self.name = "FailingThenSucceedingTask"
                self.error_message = None
            
            def execute(self):
                self.call_count += 1
                if self.call_count == 1:
                    raise Exception("First attempt failed")
                return True
        
        # Create mock task
        from core.task_queue import Task
        
        class MockTask(Task):
            def __init__(self):
                super().__init__("MockTask")
                self.call_count = 0
            
            def execute(self):
                self.call_count += 1
                if self.call_count == 1:
                    raise Exception("First attempt failed")
                return True
        
        # First attempt should fail
        task1 = MockTask()
        task_queue = get_task_queue()
        self.assertTrue(task_queue.enqueue(task1))
        time.sleep(1)
        
        # Second attempt should succeed
        task2 = MockTask()
        self.assertTrue(task_queue.enqueue(task2))
        time.sleep(1)
        
        # Verify task queue is still running and can process tasks
        self.assertTrue(task_queue.is_running())

    def test_error_logging_verification(self):
        """Test that errors are properly logged."""
        # Clear log files
        for log_file in ["log/rateloader.log"]:
            if os.path.exists(log_file):
                with open(log_file, 'w') as f:
                    f.write("")
        
        # Generate errors in different components
        with patch('features.export_mailer.create_oracle_wrapper') as mock_oracle:
            mock_wrapper = Mock()
            mock_oracle.return_value = mock_wrapper
            mock_wrapper.is_connected.return_value = False
            
            # ExportMailer error
            export_mailer = ExportMailer()
            export_mailer.run_export()
        
        with patch('features.price_export.create_oracle_wrapper') as mock_oracle:
            mock_wrapper = Mock()
            mock_oracle.return_value = mock_wrapper
            mock_wrapper.is_connected.return_value = False
            
            # PriceExport error
            price_export = PriceExport()
            price_export.export_prices_for_date("01.01.2023")
        
        # Verify errors were logged
        self.assertTrue(os.path.exists("log/rateloader.log"))
        
        # Check log contents for component prefixes
        with open("log/rateloader.log", "r") as f:
            unified_log = f.read()
            self.assertIn("[EXPORT]", unified_log)
            self.assertIn("[PRICE]", unified_log)
            self.assertIn("ERROR", unified_log.upper())

    def test_application_resilience(self):
        """Test overall application resilience to errors."""
        # Start all components
        self.assertTrue(start_task_queue())
        self.assertTrue(start_scheduler())
        self.assertTrue(start_server())
        
        # Generate various errors
        errors_generated = 0
        
        # Task queue errors
        from core.task_queue import Task
        
        class ErrorTask(Task):
            def execute(self):
                nonlocal errors_generated
                errors_generated += 1
                raise Exception(f"Task error {errors_generated}")
        
        # Enqueue multiple failing tasks
        task_queue = get_task_queue()
        for i in range(5):
            task = ErrorTask(f"ErrorTask{i}")
            task_queue.enqueue(task)
        
        # Wait for tasks to be processed
        time.sleep(2)
        
        # Verify application is still running
        self.assertTrue(task_queue.is_running())
        
        # Verify scheduler is still running
        scheduler = get_scheduler()
        self.assertTrue(scheduler.is_running())
        
        # Verify HTTP server is still running
        server = get_http_server()
        self.assertTrue(server.is_running)
        
        # Verify errors were processed
        status = task_queue.get_status()
        self.assertGreater(status["statistics"]["total_failed"], 0)


def run_error_handling_tests():
    """Run all error handling tests."""
    print("Running error handling tests for rateloader...")
    print("=" * 60)
    
    # Create test suite
    suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorHandling)
    
    # Run tests
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)
    
    # Print summary
    print("=" * 60)
    print(f"Tests run: {result.testsRun}")
    print(f"Failures: {len(result.failures)}")
    print(f"Errors: {len(result.errors)}")
    print(f"Skipped: {len(result.skipped)}")
    
    if result.failures:
        print("\nFailures:")
        for test, traceback in result.failures:
            print(f"  - {test}: {traceback}")
    
    if result.errors:
        print("\nErrors:")
        for test, traceback in result.errors:
            print(f"  - {test}: {traceback}")
    
    success = len(result.failures) == 0 and len(result.errors) == 0
    print(f"\nOverall result: {'PASS' if success else 'FAIL'}")
    
    return success


if __name__ == "__main__":
    success = run_error_handling_tests()
    sys.exit(0 if success else 1)