<?php

namespace Tests\Unit\Modules\Lemmy\Services;

use App\Enums\PlatformEnum;
use App\Exceptions\PlatformAuthException;
use App\Models\Article;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Modules\Lemmy\Services\LemmyApiService;
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Modules\Lemmy\Services\ThumbnailUploader;
use App\Services\Auth\LemmyAuthService;
use App\Services\Log\LogSaver;
use Exception;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;

class LemmyPublisherTest extends TestCase
{
    use RefreshDatabase;

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

    /**
     * Swaps in a mocked API and an uploader that echoes its input back, so existing
     * expectations can keep asserting on the thumbnail they passed in.
     */
    private function injectMocks(LemmyPublisher $publisher, LemmyApiService $api, ?ThumbnailUploader $uploader = null): void
    {
        if (! $uploader instanceof ThumbnailUploader) {
            $passthrough = Mockery::mock(ThumbnailUploader::class);
            $passthrough->shouldReceive('upload')->andReturnUsing(fn (?string $url): ?string => $url);
            $uploader = $passthrough;
        }

        $reflection = new \ReflectionClass($publisher);

        $apiProperty = $reflection->getProperty('api');
        $apiProperty->setAccessible(true);
        $apiProperty->setValue($publisher, $api);

        $uploaderProperty = $reflection->getProperty('thumbnailUploader');
        $uploaderProperty->setAccessible(true);
        $uploaderProperty->setValue($publisher, $uploader);
    }

    public function test_constructor_initializes_api_service(): void
    {
        $account = PlatformAccount::factory()->make([
            'instance_url' => 'https://lemmy.world',
        ]);

        $publisher = new LemmyPublisher($account);

        $reflection = new \ReflectionClass($publisher);

        $apiProperty = $reflection->getProperty('api');
        $apiProperty->setAccessible(true);
        $this->assertInstanceOf(LemmyApiService::class, $apiProperty->getValue($publisher));

        $accountProperty = $reflection->getProperty('account');
        $accountProperty->setAccessible(true);
        $this->assertSame($account, $accountProperty->getValue($publisher));
    }

    public function test_publish_to_channel_with_all_data(): void
    {
        $account = PlatformAccount::factory()->make([
            'instance_url' => 'https://lemmy.world',
        ]);

        $article = Article::factory()->make([
            'url' => 'https://example.com/article',
        ]);

        $channel = PlatformChannel::factory()->make([
            'channel_id' => 42,
        ]);

        $extractedData = [
            'title' => 'Test Article',
            'description' => 'Test Description',
            'thumbnail' => 'https://example.com/thumb.jpg',
            'language_id' => 5,
        ];

        // Mock LemmyAuthService via service container
        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')
            ->once()
            ->with($account)
            ->andReturn('test-token');

        $this->app->instance(LemmyAuthService::class, $authMock);

        // Mock LemmyApiService
        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')
            ->once()
            ->with(
                'test-token',
                'Test Article',
                'Test Description',
                42,
                'https://example.com/article',
                'https://example.com/thumb.jpg',
                5
            )
            ->andReturn(['post_view' => ['post' => ['id' => 999]]]);

        // Create publisher and inject mocked API using reflection
        $publisher = new LemmyPublisher($account);

        $this->injectMocks($publisher, $apiMock);

        $result = $publisher->publishToChannel($article, $extractedData, $channel);

        $this->assertEquals(['post_view' => ['post' => ['id' => 999]]], $result);
    }

    public function test_publish_to_channel_with_minimal_data(): void
    {
        $account = PlatformAccount::factory()->make([
            'instance_url' => 'https://lemmy.world',
        ]);

        $article = Article::factory()->make([
            'url' => 'https://example.com/article',
        ]);

        $channel = PlatformChannel::factory()->make([
            'channel_id' => 24,
        ]);

        $extractedData = [];

        // Mock LemmyAuthService
        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')
            ->once()
            ->with($account)
            ->andReturn('minimal-token');

        $this->app->instance(LemmyAuthService::class, $authMock);

        // Mock LemmyApiService
        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')
            ->once()
            ->with(
                'minimal-token',
                'Untitled',
                '',
                24,
                'https://example.com/article',
                null,
                null
            )
            ->andReturn(['post_view' => ['post' => ['id' => 777]]]);

        // Create publisher and inject mocked API using reflection
        $publisher = new LemmyPublisher($account);

        $this->injectMocks($publisher, $apiMock);

        $result = $publisher->publishToChannel($article, $extractedData, $channel);

        $this->assertEquals(['post_view' => ['post' => ['id' => 777]]], $result);
    }

    public function test_publish_to_channel_without_thumbnail(): void
    {
        $account = PlatformAccount::factory()->make([
            'instance_url' => 'https://lemmy.world',
        ]);

        $article = Article::factory()->make([
            'url' => 'https://example.com/article',
        ]);

        $channel = PlatformChannel::factory()->make([
            'channel_id' => 33,
        ]);

        $extractedData = [
            'title' => 'No Thumbnail Article',
            'description' => 'Article without thumbnail',
            'language_id' => 2,
        ];

        // Mock LemmyAuthService
        $authMock = Mockery::mock(LemmyAuthService::class);
        $this->app->instance(LemmyAuthService::class, $authMock);
        $authMock->shouldReceive('getToken')
            ->once()
            ->with($account)
            ->andReturn('no-thumb-token');

        // Mock LemmyApiService
        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')
            ->once()
            ->with(
                'no-thumb-token',
                'No Thumbnail Article',
                'Article without thumbnail',
                33,
                'https://example.com/article',
                null,
                2
            )
            ->andReturn(['post_view' => ['post' => ['id' => 555]]]);

        // Create publisher and inject mocked API using reflection
        $publisher = new LemmyPublisher($account);

        $this->injectMocks($publisher, $apiMock);

        $result = $publisher->publishToChannel($article, $extractedData, $channel);

        $this->assertEquals(['post_view' => ['post' => ['id' => 555]]], $result);
    }

    public function test_publish_to_channel_throws_platform_auth_exception(): void
    {
        $account = PlatformAccount::factory()->make([
            'instance_url' => 'https://lemmy.world',
        ]);

        $article = Article::factory()->make();
        $channel = PlatformChannel::factory()->make();
        $extractedData = [];

        // Mock LemmyAuthService to throw exception
        $authMock = Mockery::mock(LemmyAuthService::class);
        $this->app->instance(LemmyAuthService::class, $authMock);
        $authMock->shouldReceive('getToken')
            ->once()
            ->with($account)
            ->andThrow(new PlatformAuthException(PlatformEnum::LEMMY, 'Auth failed'));

        $publisher = new LemmyPublisher($account);

        $this->expectException(PlatformAuthException::class);
        $this->expectExceptionMessage('Auth failed');

        $publisher->publishToChannel($article, $extractedData, $channel);
    }

    public function test_publish_to_channel_throws_api_exception(): void
    {
        $account = PlatformAccount::factory()->make([
            'instance_url' => 'https://lemmy.world',
        ]);

        $article = Article::factory()->make([
            'url' => 'https://example.com/article',
        ]);

        $channel = PlatformChannel::factory()->make([
            'channel_id' => 42,
        ]);

        $extractedData = [
            'title' => 'Test Article',
        ];

        // Mock LemmyAuthService via service container
        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')
            ->once()
            ->with($account)
            ->andReturn('test-token');

        $this->app->instance(LemmyAuthService::class, $authMock);

        // Mock LemmyApiService to throw exception
        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')
            ->once()
            ->andThrow(new Exception('API Error'));

        // Create publisher and inject mocked API using reflection
        $publisher = new LemmyPublisher($account);

        $this->injectMocks($publisher, $apiMock);

        $this->expectException(Exception::class);
        $this->expectExceptionMessage('API Error');

        $publisher->publishToChannel($article, $extractedData, $channel);
    }

    public function test_publish_to_channel_forwards_resolved_community_id_to_create_post(): void
    {
        $account = PlatformAccount::factory()->make([
            'instance_url' => 'https://lemmy.world',
        ]);

        $article = Article::factory()->make([
            'url' => 'https://example.com/article',
        ]);

        $channel = PlatformChannel::factory()->make([
            'channel_id' => 42,
        ]);

        $extractedData = [
            'title' => 'Test Title',
        ];

        // Mock LemmyAuthService
        $authMock = Mockery::mock(LemmyAuthService::class);
        $this->app->instance(LemmyAuthService::class, $authMock);
        $authMock->shouldReceive('getToken')
            ->once()
            ->andReturn('token');

        // Mock LemmyApiService - should resolve non-numeric channel_id to a community id
        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')
            ->once()
            ->with(
                'token',
                'Test Title',
                '',
                42, // resolved community ID
                'https://example.com/article',
                null,
                null
            )
            ->andReturn(['success' => true]);

        // Create publisher and inject mocked API using reflection
        $publisher = new LemmyPublisher($account);

        $this->injectMocks($publisher, $apiMock);

        $result = $publisher->publishToChannel($article, $extractedData, $channel);

        $this->assertEquals(['success' => true], $result);
    }

    public function test_it_passes_the_uploaded_thumbnail_url_to_create_post(): void
    {
        $account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
        $article = Article::factory()->make(['url' => 'https://example.com/article']);
        $channel = PlatformChannel::factory()->make(['channel_id' => 7]);

        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')->andReturn('tok');
        $this->app->instance(LemmyAuthService::class, $authMock);

        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')
            ->once()
            ->with('tok', 'T', '', 7, 'https://example.com/article', 'https://lemmy.world/pictrs/image/x.jpg', null)
            ->andReturn(['ok' => true]);

        $uploader = Mockery::mock(ThumbnailUploader::class);
        $uploader->shouldReceive('upload')
            ->once()
            ->with('https://cdn.example.com/big.jpg', 'tok')
            ->andReturn('https://lemmy.world/pictrs/image/x.jpg');

        $publisher = new LemmyPublisher($account);
        $this->injectMocks($publisher, $apiMock, $uploader);

        $this->assertSame(
            ['ok' => true],
            $publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/big.jpg'], $channel)
        );
    }

    public function test_it_logs_a_warning_when_the_thumbnail_upload_fails(): void
    {
        $account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
        $article = Article::factory()->create();
        $channel = PlatformChannel::factory()->create();

        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')->andReturn('tok');
        $this->app->instance(LemmyAuthService::class, $authMock);

        $logMock = Mockery::mock(LogSaver::class);
        $logMock->shouldReceive('warning')
            ->once()
            ->withArgs(fn (string $message, ?PlatformChannel $c, array $context): bool => str_contains($message, 'Thumbnail upload failed')
                && $context['source'] === 'https://cdn.example.com/big.jpg');
        $this->app->instance(LogSaver::class, $logMock);

        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);

        $uploader = Mockery::mock(ThumbnailUploader::class);
        $uploader->shouldReceive('upload')->once()->andReturn(null);

        $publisher = new LemmyPublisher($account);
        $this->injectMocks($publisher, $apiMock, $uploader);

        $this->assertSame(
            ['ok' => true],
            $publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/big.jpg'], $channel)
        );
    }

    public function test_it_does_not_log_when_there_was_no_thumbnail_to_upload(): void
    {
        $account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
        $article = Article::factory()->make(['url' => 'https://example.com/article']);
        $channel = PlatformChannel::factory()->make(['channel_id' => 7]);

        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')->andReturn('tok');
        $this->app->instance(LemmyAuthService::class, $authMock);

        $logMock = Mockery::mock(LogSaver::class);
        $logMock->shouldNotReceive('warning');
        $this->app->instance(LogSaver::class, $logMock);

        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);

        $uploader = Mockery::mock(ThumbnailUploader::class);
        $uploader->shouldNotReceive('upload');

        $publisher = new LemmyPublisher($account);
        $this->injectMocks($publisher, $apiMock, $uploader);

        $this->assertSame(['ok' => true], $publisher->publishToChannel($article, ['title' => 'T'], $channel));
    }

    public function test_a_stale_token_retry_does_not_re_upload_the_thumbnail(): void
    {
        $account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
        $article = Article::factory()->create();
        $channel = PlatformChannel::factory()->create();

        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')->once()->andReturn('stale');
        $authMock->shouldReceive('refreshToken')->once()->andReturn('fresh');
        $this->app->instance(LemmyAuthService::class, $authMock);

        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')
            ->once()
            ->with('stale', Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(), 'https://lemmy.world/pictrs/image/x.jpg', Mockery::any())
            ->andThrow(new Exception('not_logged_in'));
        $apiMock->shouldReceive('createPost')
            ->once()
            ->with('fresh', Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(), 'https://lemmy.world/pictrs/image/x.jpg', Mockery::any())
            ->andReturn(['ok' => true]);

        $uploader = Mockery::mock(ThumbnailUploader::class);
        $uploader->shouldReceive('upload')->once()->andReturn('https://lemmy.world/pictrs/image/x.jpg');

        $publisher = new LemmyPublisher($account);
        $this->injectMocks($publisher, $apiMock, $uploader);

        $this->assertSame(
            ['ok' => true],
            $publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/b.jpg'], $channel)
        );
    }

    public function test_a_stale_token_retry_logs_the_upload_failure_only_once(): void
    {
        $account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
        $article = Article::factory()->create();
        $channel = PlatformChannel::factory()->create();

        $authMock = Mockery::mock(LemmyAuthService::class);
        $authMock->shouldReceive('getToken')->andReturn('stale');
        $authMock->shouldReceive('refreshToken')->andReturn('fresh');
        $this->app->instance(LemmyAuthService::class, $authMock);

        $logMock = Mockery::mock(LogSaver::class);
        $logMock->shouldReceive('warning')->once();
        $this->app->instance(LogSaver::class, $logMock);

        $apiMock = Mockery::mock(LemmyApiService::class);
        $apiMock->shouldReceive('createPost')->once()->andThrow(new Exception('not_logged_in'));
        $apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);

        $uploader = Mockery::mock(ThumbnailUploader::class);
        $uploader->shouldReceive('upload')->once()->andReturn(null);

        $publisher = new LemmyPublisher($account);
        $this->injectMocks($publisher, $apiMock, $uploader);

        $this->assertSame(
            ['ok' => true],
            $publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/b.jpg'], $channel)
        );
    }
}
