<?php

namespace Tests\Unit\Jobs;

use App\Enums\PlatformEnum;
use App\Jobs\SyncChannelPostsJob;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\PlatformInstance;
use App\Modules\Lemmy\Services\LemmyApiService;
use App\Services\Log\LogSaver;
use Exception;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Queue;
use Mockery;
use Tests\TestCase;

class SyncChannelPostsJobTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        Queue::fake();
        Cache::flush();
    }

    public function test_constructor_sets_correct_queue(): void
    {
        $channel = PlatformChannel::factory()->make();
        $job = new SyncChannelPostsJob($channel);

        $this->assertEquals('sync', $job->queue);
    }

    public function test_job_implements_should_queue(): void
    {
        $channel = PlatformChannel::factory()->make();
        $job = new SyncChannelPostsJob($channel);

        $this->assertInstanceOf(ShouldQueue::class, $job);
    }

    public function test_job_implements_should_be_unique(): void
    {
        $channel = PlatformChannel::factory()->make();
        $job = new SyncChannelPostsJob($channel);

        $this->assertInstanceOf(ShouldBeUnique::class, $job);
    }

    public function test_job_uses_queueable_trait(): void
    {
        $channel = PlatformChannel::factory()->make();
        $job = new SyncChannelPostsJob($channel);

        $this->assertContains(
            Queueable::class,
            class_uses($job)
        );
    }

    public function test_dispatch_for_all_active_channels_dispatches_jobs(): void
    {
        // Arrange
        $platformInstance = PlatformInstance::factory()->create([
            'platform' => PlatformEnum::LEMMY,
        ]);

        $account = PlatformAccount::factory()->create([
            'instance_url' => $platformInstance->url,
            'is_active' => true,
        ]);

        $channel = PlatformChannel::factory()->create([
            'platform_instance_id' => $platformInstance->id,
            'is_active' => true,
        ]);

        // Attach account to channel with active status
        $channel->platformAccounts()->attach($account->id, [
            'is_active' => true,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        // Mock LogSaver to avoid strict expectations
        $logSaverMock = Mockery::mock(LogSaver::class);
        $logSaverMock->shouldReceive('info')->zeroOrMoreTimes();

        $this->app->instance(LogSaver::class, $logSaverMock);

        // Act
        SyncChannelPostsJob::dispatchForAllActiveChannels();

        // Assert - At least one job should be dispatched
        Queue::assertPushed(SyncChannelPostsJob::class);
    }

    public function test_handle_logs_start_message(): void
    {
        // Arrange
        $platformInstance = PlatformInstance::factory()->create([
            'platform' => PlatformEnum::LEMMY,
            'url' => 'https://lemmy.example.com',
        ]);

        $channel = PlatformChannel::factory()->create([
            'platform_instance_id' => $platformInstance->id,
            'name' => 'testcommunity',
        ]);

        // Mock LogSaver - only test that logging methods are called
        $logSaverMock = Mockery::mock(LogSaver::class);
        $logSaverMock->shouldReceive('info')->atLeast()->once();
        $logSaverMock->shouldReceive('error')->zeroOrMoreTimes();

        $job = new SyncChannelPostsJob($channel);

        // Act - This will fail due to no active account, but we test the logging
        try {
            $job->handle($logSaverMock);
        } catch (Exception $e) {
            // Expected to fail, we're testing that logging is called
        }

        // Assert - Test completes if no exceptions during setup
        $this->assertTrue(true);
    }

    public function test_sync_passes_the_stored_community_id(): void
    {
        [$channel, $account] = $this->makeSyncableChannel(42);

        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('login')
            ->once()
            ->with($account->username, $account->password)
            ->andReturn('token');
        $apiMock->shouldReceive('syncChannelPosts')
            ->once()
            ->with('token', Mockery::on(fn ($arg) => $arg->is($channel)), 42);

        $logSaverMock = Mockery::mock(LogSaver::class);
        $logSaverMock->shouldReceive('info')->zeroOrMoreTimes();
        $logSaverMock->shouldReceive('error')->zeroOrMoreTimes();

        $this->makeJobWithApi($channel, $apiMock)->handle($logSaverMock);

        // The behaviour under test is the mocked call sequence above; assert explicitly
        // so PHPUnit does not flag the test as risky for performing no assertions.
        $this->addToAssertionCount(1);
    }

    public function test_sync_uses_channel_id_without_a_lookup(): void
    {
        [$channel, $account] = $this->makeSyncableChannel(42);

        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('login')
            ->once()
            ->with($account->username, $account->password)
            ->andReturn('token');
        $apiMock->shouldReceive('syncChannelPosts')
            ->once()
            ->with('token', Mockery::on(fn ($arg) => $arg->is($channel)), 42);

        $logSaverMock = Mockery::mock(LogSaver::class);
        $logSaverMock->shouldReceive('info')->zeroOrMoreTimes();
        $logSaverMock->shouldReceive('error')->zeroOrMoreTimes();

        $this->makeJobWithApi($channel, $apiMock)->handle($logSaverMock);

        // The mocked call sequence above is the assertion; assert explicitly so PHPUnit
        // does not flag the test as risky.
        $this->addToAssertionCount(1);
    }

    /**
     * @return array{0: PlatformChannel, 1: PlatformAccount}
     */
    private function makeSyncableChannel(int $channelId): array
    {
        $platformInstance = PlatformInstance::factory()->create([
            'platform' => PlatformEnum::LEMMY,
            'url' => 'https://lemmy.example.com',
        ]);

        $account = PlatformAccount::factory()->create([
            'instance_url' => $platformInstance->url,
            'is_active' => true,
        ]);

        $channel = PlatformChannel::factory()->create([
            'platform_instance_id' => $platformInstance->id,
            'name' => 'tech_news',
            'channel_id' => $channelId,
            'is_active' => true,
        ]);

        $channel->platformAccounts()->attach($account->id, [
            'is_active' => true,
            'priority' => 1,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return [$channel->fresh(['platformInstance']), $account];
    }

    private function makeJobWithApi(PlatformChannel $channel, LemmyApiService $api): SyncChannelPostsJob
    {
        return new class($channel, $api) extends SyncChannelPostsJob
        {
            public function __construct(PlatformChannel $channel, private readonly LemmyApiService $api)
            {
                parent::__construct($channel);
            }

            protected function makeApiService(string $instanceUrl): LemmyApiService
            {
                return $this->api;
            }
        };
    }

    public function test_job_can_be_serialized(): void
    {
        $platformInstance = PlatformInstance::factory()->create();
        $channel = PlatformChannel::factory()->create([
            'platform_instance_id' => $platformInstance->id,
            'name' => 'Test Channel',
        ]);
        $job = new SyncChannelPostsJob($channel);

        $serialized = serialize($job);
        $unserialized = unserialize($serialized);

        $this->assertInstanceOf(SyncChannelPostsJob::class, $unserialized);
        $this->assertEquals($job->queue, $unserialized->queue);
        // Note: Cannot test channel property directly as it's private
        // but serialization/unserialization working proves the job structure is intact
    }

    public function test_dispatch_for_all_active_channels_method_exists(): void
    {
        $this->assertTrue(method_exists(SyncChannelPostsJob::class, 'dispatchForAllActiveChannels'));
    }

    public function test_job_has_handle_method(): void
    {
        $channel = PlatformChannel::factory()->make();
        $job = new SyncChannelPostsJob($channel);

        $this->assertTrue(method_exists($job, 'handle'));
    }

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