<?php

namespace Tests\Unit\Framework\Scheduler;

use Mockery;
use Openguru\OpenCartFramework\Cache\CacheInterface;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Scheduler\Job;
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
use Psr\Log\LoggerInterface;
use Tests\TestCase;

class SchedulerServiceTest extends TestCase
{
    private SchedulerService $scheduler;
    private $cacheMock;
    private $settingsMock;
    private $loggerMock;

    protected function setUp(): void
    {
        parent::setUp();

        $this->cacheMock = Mockery::mock(CacheInterface::class);
        $this->settingsMock = Mockery::mock(Settings::class);
        $this->loggerMock = Mockery::mock(LoggerInterface::class);

        $this->scheduler = new SchedulerService(
            $this->loggerMock,
            $this->cacheMock,
            $this->app,
            $this->settingsMock
        );
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    public function testDisabledModeSkipsExecution()
    {
        // Arrange
        $this->settingsMock->shouldReceive('get')
            ->with('cron.mode', 'disabled')
            ->andReturn('disabled');

        // Act
        $result = $this->scheduler->run();

        // Assert
        $this->assertEquals([['name' => 'Global', 'reason' => 'Scheduler is disabled']], $result->skipped);
        $this->assertEmpty($result->executed);
        $this->assertEmpty($result->failed);
    }

    public function testGlobalLockPreventsExecution()
    {
        // Arrange
        $this->settingsMock->shouldReceive('get')
            ->with('cron.mode', 'disabled')
            ->andReturn('system');

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.global_lock')
            ->andReturn('1'); // Lock is active

        // Act
        $result = $this->scheduler->run();

        // Assert
        $this->assertEquals([['name' => 'Global', 'reason' => 'Global scheduler lock active']], $result->skipped);
        $this->assertEmpty($result->executed);
        $this->assertEmpty($result->failed);
    }

    public function testJobLockPreventsExecution()
    {
        // Arrange
        $this->settingsMock->shouldReceive('get')
            ->with('cron.mode', 'disabled')
            ->andReturn('system');

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.global_lock')
            ->andReturn(null); // No global lock

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_lock', 1, 300)
            ->once();

        $this->cacheMock->shouldReceive('delete')
            ->with('scheduler.global_lock')
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_last_run', Mockery::type('int'))
            ->once();

        // Mock registry and job
        $registryMock = Mockery::mock(ScheduleJobRegistry::class);
        $jobMock = Mockery::mock(Job::class);

        $jobMock->shouldReceive('getName')->andReturn('TestJob');
        $jobMock->shouldReceive('getId')->andReturn('test_job_id');
        $jobMock->shouldReceive('isDue')->andReturn(true);

        // Job has not run recently (getLastRun returns null)
        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.last_run.test_job_id')
            ->andReturn(null);

        // Job is locked
        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.lock.test_job_id')
            ->andReturn('1');

        $registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);

        // Logger should not be called for this test
        $this->loggerMock->shouldReceive('error')->never();

        // Inject registry for testing
        $this->scheduler->setRegistry($registryMock);

        // Act
        $result = $this->scheduler->run();

        // Assert
        $this->assertEquals([['name' => 'TestJob', 'reason' => 'Job is locked (running)']], $result->skipped);
    }

    public function testJobRecentlyExecutedPreventsExecution()
    {
        // Arrange
        $this->settingsMock->shouldReceive('get')
            ->with('cron.mode', 'disabled')
            ->andReturn('system');

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.global_lock')
            ->andReturn(null);

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_lock', 1, 300)
            ->once();

        $this->cacheMock->shouldReceive('delete')
            ->with('scheduler.global_lock')
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_last_run', Mockery::type('int'))
            ->once();

        // Mock registry and job
        $registryMock = Mockery::mock(ScheduleJobRegistry::class);
        $jobMock = Mockery::mock(Job::class);

        $jobMock->shouldReceive('getName')->andReturn('TestJob');
        $jobMock->shouldReceive('getId')->andReturn('test_job_id');
        $jobMock->shouldReceive('isDue')->andReturn(true);

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.lock.test_job_id')
            ->andReturn(null); // Not locked

        // Job was recently executed (same minute)
        $recentTime = time();
        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.last_run.test_job_id')
            ->andReturn($recentTime);

        $registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);

        // Inject registry for testing
        $this->scheduler->setRegistry($registryMock);

        // Act
        $result = $this->scheduler->run();

        // Assert
        $this->assertEquals([['name' => 'TestJob', 'reason' => 'Already ran recently']], $result->skipped);
    }

    public function testJobNotDuePreventsExecution()
    {
        // Arrange
        $this->settingsMock->shouldReceive('get')
            ->with('cron.mode', 'disabled')
            ->andReturn('system');

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.global_lock')
            ->andReturn(null);

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_lock', 1, 300)
            ->once();

        $this->cacheMock->shouldReceive('delete')
            ->with('scheduler.global_lock')
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_last_run', Mockery::type('int'))
            ->once();

        // Mock registry and job
        $registryMock = Mockery::mock(ScheduleJobRegistry::class);
        $jobMock = Mockery::mock(Job::class);

        $jobMock->shouldReceive('getName')->andReturn('TestJob');
        $jobMock->shouldReceive('getId')->andReturn('test_job_id');
        $jobMock->shouldReceive('isDue')->andReturn(false); // Not due

        $registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);

        // Inject registry for testing
        $this->scheduler->setRegistry($registryMock);

        // Act
        $result = $this->scheduler->run();

        // Assert
        $this->assertEquals([['name' => 'TestJob', 'reason' => 'Not due']], $result->skipped);
    }

    public function testSuccessfulJobExecution()
    {
        // Arrange
        $this->settingsMock->shouldReceive('get')
            ->with('cron.mode', 'disabled')
            ->andReturn('system');

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.global_lock')
            ->andReturn(null);

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_lock', 1, 300)
            ->once();

        $this->cacheMock->shouldReceive('delete')
            ->with('scheduler.global_lock')
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_last_run', Mockery::type('int'))
            ->once();

        // Mock registry and job
        $registryMock = Mockery::mock(ScheduleJobRegistry::class);
        $jobMock = Mockery::mock(Job::class);

        $jobMock->shouldReceive('getName')->andReturn('TestJob');
        $jobMock->shouldReceive('getId')->andReturn('test_job_id');
        $jobMock->shouldReceive('isDue')->andReturn(true);
        $jobMock->shouldReceive('run')->once();

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.lock.test_job_id')
            ->andReturn(null);

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.last_run.test_job_id')
            ->andReturn(null); // Never ran before

        // Lock and unlock operations
        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.lock.test_job_id', 1, 1800)
            ->once();

        $this->cacheMock->shouldReceive('delete')
            ->with('scheduler.lock.test_job_id')
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.last_run.test_job_id', Mockery::type('int'))
            ->once();

        $registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);

        $this->loggerMock->shouldReceive('debug')
            ->with('Job executed: TestJob', Mockery::type('array'))
            ->once();

        // Разрешаем любые другие вызовы логгера (на случай неожиданных вызовов)
        $this->loggerMock->shouldReceive('error')
            ->zeroOrMoreTimes()
            ->andReturn(true);

        // Inject registry for testing
        $this->scheduler->setRegistry($registryMock);

        // Act
        $result = $this->scheduler->run();

        // Assert
        $this->assertCount(1, $result->executed);
        $this->assertEquals('TestJob', $result->executed[0]['name']);
        $this->assertEmpty($result->failed);
        $this->assertEmpty($result->skipped);
    }

    public function testJobExecutionFailure()
    {
        // Arrange
        $this->settingsMock->shouldReceive('get')
            ->with('cron.mode', 'disabled')
            ->andReturn('system');

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.global_lock')
            ->andReturn(null);

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_lock', 1, 300)
            ->once();

        $this->cacheMock->shouldReceive('delete')
            ->with('scheduler.global_lock')
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.global_last_run', Mockery::type('int'))
            ->once();

        // Mock registry and job
        $registryMock = Mockery::mock(ScheduleJobRegistry::class);
        $jobMock = Mockery::mock(Job::class);

        $jobMock->shouldReceive('getName')->andReturn('TestJob');
        $jobMock->shouldReceive('getId')->andReturn('test_job_id');
        $jobMock->shouldReceive('isDue')->andReturn(true);
        $jobMock->shouldReceive('run')->andThrow(new \Exception('Job failed'));

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.lock.test_job_id')
            ->andReturn(null);

        $this->cacheMock->shouldReceive('get')
            ->with('scheduler.last_run.test_job_id')
            ->andReturn(null);

        // Lock and unlock operations
        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.lock.test_job_id', 1, 1800)
            ->once();

        $this->cacheMock->shouldReceive('delete')
            ->with('scheduler.lock.test_job_id')
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.last_failure.test_job_id', Mockery::type('int'))
            ->once();

        $this->cacheMock->shouldReceive('set')
            ->with('scheduler.last_failure_msg.test_job_id', 'Job failed')
            ->once();

        $registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);

        $this->loggerMock->shouldReceive('error')
            ->with('Job failed: TestJob', Mockery::type('array'))
            ->once();

        // Inject registry for testing
        $this->scheduler->setRegistry($registryMock);

        // Act
        $result = $this->scheduler->run();

        // Assert
        $this->assertCount(1, $result->failed);
        $this->assertEquals('TestJob', $result->failed[0]['name']);
        $this->assertEquals('Job failed', $result->failed[0]['error']);
        $this->assertEmpty($result->executed);
        $this->assertEmpty($result->skipped);
    }
}
