"""
Tests for django_cronjob_utils management command.
"""
from datetime import date
from io import StringIO
from unittest.mock import patch
from django.test import TestCase
from django.core.management import call_command
from django.core.management.base import CommandError
from django_cronjob_utils.registry import TaskRegistry, ExecutionPattern
from django_cronjob_utils.base import CronTask
from django_cronjob_utils.models import CronExecution
from django_cronjob_utils.exceptions import TaskNotFoundError, ConcurrentExecutionError


class TestTask(CronTask):
    """Test task for management command tests."""
    
    task_name = 'test-task'
    task_code = 'A001'
    
    def execute(self, date: date) -> dict:
        return {'error': False, 'message': 'Success'}


class FailingTask(CronTask):
    """Task that fails."""
    
    task_name = 'failing-task'
    task_code = 'A002'
    
    def execute(self, date: date) -> dict:
        return {'error': True, 'message': 'Task failed', 'error_code': 'FAIL'}


class SkippedTask(CronTask):
    """Task that gets skipped."""
    
    task_name = 'skipped-task'
    task_code = 'A003'
    execution_pattern = ExecutionPattern.STANDARD
    
    def execute(self, date: date) -> dict:
        return {'error': False, 'message': 'Success'}


class ManagementCommandTests(TestCase):
    """Tests for run_cron_task management command."""
    
    def setUp(self):
        """Set up test fixtures."""
        TaskRegistry._tasks.clear()
        TaskRegistry._codes.clear()
        TaskRegistry._configs.clear()
        
        TaskRegistry.register('test-task', 'A001', TestTask)
        TaskRegistry.register('failing-task', 'A002', FailingTask)
        TaskRegistry.register('skipped-task', 'A003', SkippedTask)
    
    def test_run_task_success(self):
        """Test running a successful task."""
        out = StringIO()
        call_command(
            'run_cron_task',
            'test-task',
            '2024-01-15',
            stdout=out
        )
        
        output = out.getvalue()
        self.assertIn('completed successfully', output)
        self.assertIn('Success', output)
        
        # Check execution record
        execution = CronExecution.objects.get(
            task_code='A001',
            execution_date=date(2024, 1, 15)
        )
        self.assertTrue(execution.completed)
        self.assertTrue(execution.success)
    
    def test_run_task_failure(self):
        """Test running a failing task."""
        out = StringIO()
        err = StringIO()
        
        exit_code = call_command(
            'run_cron_task',
            'failing-task',
            '2024-01-15',
            stdout=out,
            stderr=err
        )
        
        self.assertEqual(exit_code, 1)
        output = out.getvalue()
        self.assertIn('failed', output)
        self.assertIn('Task failed', output)
        
        # Check execution record
        execution = CronExecution.objects.get(
            task_code='A002',
            execution_date=date(2024, 1, 15)
        )
        self.assertTrue(execution.completed)
        self.assertFalse(execution.success)
    
    def test_run_task_not_found(self):
        """Test running a non-existent task."""
        out = StringIO()
        
        exit_code = call_command(
            'run_cron_task',
            'non-existent-task',
            '2024-01-15',
            stdout=out
        )
        
        self.assertEqual(exit_code, 1)
        output = out.getvalue()
        self.assertIn('not found', output)
        self.assertIn('non-existent-task', output)
    
    def test_run_task_invalid_date_format(self):
        """Test running task with invalid date format."""
        out = StringIO()
        
        with self.assertRaises(CommandError) as cm:
            call_command(
                'run_cron_task',
                'test-task',
                'invalid-date',
                stdout=out
            )
        
        self.assertIn('Invalid date format', str(cm.exception))
        self.assertIn('invalid-date', str(cm.exception))
    
    def test_run_task_skipped(self):
        """Test running a task that gets skipped."""
        # Create previous execution
        CronExecution.objects.create(
            task_code='A003',
            task_name='skipped-task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=True
        )
        
        out = StringIO()
        exit_code = call_command(
            'run_cron_task',
            'skipped-task',
            '2024-01-15',
            stdout=out
        )
        
        self.assertEqual(exit_code, 0)
        output = out.getvalue()
        self.assertIn('skipped', output)
        self.assertIn('Already executed', output)
    
    def test_run_task_force_option(self):
        """Test running task with --force option."""
        # Create previous execution
        CronExecution.objects.create(
            task_code='A001',
            task_name='test-task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=True
        )
        
        out = StringIO()
        call_command(
            'run_cron_task',
            'test-task',
            '2024-01-15',
            '--force',
            stdout=out
        )
        
        output = out.getvalue()
        self.assertIn('completed successfully', output)
        
        # Should have two executions now
        executions = CronExecution.objects.filter(
            task_code='A001',
            execution_date=date(2024, 1, 15)
        )
        self.assertEqual(executions.count(), 2)
    
    def test_run_task_rerun_option(self):
        """Test running task with --rerun option."""
        # Create previous successful execution
        CronExecution.objects.create(
            task_code='A001',
            task_name='test-task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=True
        )
        
        out = StringIO()
        call_command(
            'run_cron_task',
            'test-task',
            '2024-01-15',
            '--rerun',
            stdout=out
        )
        
        output = out.getvalue()
        self.assertIn('completed successfully', output)
        
        # Should have two executions now
        executions = CronExecution.objects.filter(
            task_code='A001',
            execution_date=date(2024, 1, 15)
        )
        self.assertEqual(executions.count(), 2)
    
    def test_run_task_concurrent_execution(self):
        """Test running task when concurrent execution exists."""
        # Create running execution
        CronExecution.objects.create(
            task_code='A001',
            task_name='test-task',
            execution_date=date(2024, 1, 15),
            completed=False
        )
        
        out = StringIO()
        exit_code = call_command(
            'run_cron_task',
            'test-task',
            '2024-01-15',
            stdout=out
        )
        
        self.assertEqual(exit_code, 2)
        output = out.getvalue()
        self.assertIn('already running', output.lower())
    
    def test_run_task_exception_handling(self):
        """Test command handles unexpected exceptions."""
        # Create a task that raises exception
        class ExceptionTask(CronTask):
            task_name = 'exception-task'
            task_code = 'A004'
            
            def execute(self, date: date) -> dict:
                raise RuntimeError("Unexpected error")
        
        TaskRegistry.register('exception-task', 'A004', ExceptionTask)
        
        out = StringIO()
        exit_code = call_command(
            'run_cron_task',
            'exception-task',
            '2024-01-15',
            stdout=out
        )
        
        self.assertEqual(exit_code, 1)
        output = out.getvalue()
        self.assertIn('Unexpected error', output)
        
        # Check execution record was created
        execution = CronExecution.objects.get(
            task_code='A004',
            execution_date=date(2024, 1, 15)
        )
        self.assertTrue(execution.completed)
        self.assertFalse(execution.success)
    
    def test_run_task_different_dates(self):
        """Test running task for different dates."""
        dates = ['2024-01-15', '2024-01-16', '2024-01-17']
        
        for date_str in dates:
            out = StringIO()
            call_command(
                'run_cron_task',
                'test-task',
                date_str,
                stdout=out
            )
            
            output = out.getvalue()
            self.assertIn('completed successfully', output)
        
        # Should have three executions
        executions = CronExecution.objects.filter(task_code='A001')
        self.assertEqual(executions.count(), 3)
    
    def test_run_task_edge_case_future_date(self):
        """Test running task with future date (edge case)."""
        out = StringIO()
        call_command(
            'run_cron_task',
            'test-task',
            '2030-12-31',
            stdout=out
        )
        
        output = out.getvalue()
        self.assertIn('completed successfully', output)
        
        execution = CronExecution.objects.get(
            task_code='A001',
            execution_date=date(2030, 12, 31)
        )
        self.assertTrue(execution.completed)
    
    def test_run_task_edge_case_past_date(self):
        """Test running task with past date (edge case)."""
        out = StringIO()
        call_command(
            'run_cron_task',
            'test-task',
            '2020-01-01',
            stdout=out
        )
        
        output = out.getvalue()
        self.assertIn('completed successfully', output)
        
        execution = CronExecution.objects.get(
            task_code='A001',
            execution_date=date(2020, 1, 1)
        )
        self.assertTrue(execution.completed)
    
    def test_run_task_missing_arguments(self):
        """Test command with missing arguments."""
        out = StringIO()
        
        with self.assertRaises(CommandError):
            call_command('run_cron_task', stdout=out)
    
    def test_run_task_help(self):
        """Test command help output."""
        # Note: Django's call_command() doesn't route --help through self.stdout.
        # Instead, argparse prints help directly to sys.stdout before execute() is called.
        # So we test the help by checking that the command's add_arguments() method
        # defines the expected arguments.
        from django_cronjob_utils.management.commands.run_cron_task import Command
        import argparse
        
        # Create a mock parser and call add_arguments
        parser = argparse.ArgumentParser()
        cmd = Command()
        cmd.add_arguments(parser)
        
        # Get the help string from the parser
        help_output = parser.format_help()
        
        # Verify expected arguments are in the help
        self.assertIn('task_name', help_output)
        self.assertIn('date', help_output)
        self.assertIn('--force', help_output)
        self.assertIn('--rerun', help_output)
