<?php

namespace Tests\Unit\Actions;

use App\Actions\FetchArticleDataAction;
use App\Actions\PublishRouteArticleAction;
use App\Enums\NotificationTypeEnum;
use App\Enums\PublishStatusEnum;
use App\Models\Article;
use App\Models\ArticlePublication;
use App\Models\Feed;
use App\Models\Route;
use App\Models\RouteArticle;
use App\Services\Notification\NotificationService;
use App\Services\Publishing\ArticlePublishingService;
use App\Services\Publishing\PublishOutcome;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;

class PublishRouteArticleActionTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @param  array<string, mixed>  $articleAttributes
     */
    private function createRouteArticle(array $articleAttributes, bool $unvalidated = false): RouteArticle
    {
        $feed = Feed::factory()->create();
        /** @var Route $route */
        $route = Route::factory()->active()->create(['feed_id' => $feed->id]);

        $factory = Article::factory();
        if ($unvalidated) {
            $factory = $factory->unvalidated();
        }
        $article = $factory->create(array_merge(['feed_id' => $feed->id], $articleAttributes));

        /** @var RouteArticle $routeArticle */
        $routeArticle = RouteArticle::factory()->forRoute($route)->approved()->create([
            'article_id' => $article->id,
        ]);

        return $routeArticle;
    }

    private function makePublication(): ArticlePublication
    {
        /** @var ArticlePublication $publication */
        $publication = ArticlePublication::factory()->create();

        return $publication;
    }

    public function test_publish_uses_stored_article_data_without_fetching(): void
    {
        $routeArticle = $this->createRouteArticle([
            'title' => 'Stored Title',
            'description' => 'Stored description',
            'image_url' => 'https://cdn.test/stored.jpg',
        ]);

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldNotReceive('execute');

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldReceive('publishRouteArticle')
            ->once()
            ->with(Mockery::any(), [
                'title' => 'Stored Title',
                'description' => 'Stored description',
                'thumbnail' => 'https://cdn.test/stored.jpg',
            ])
            ->andReturn(PublishOutcome::published($this->makePublication()));

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->fresh()->publish_status);
    }

    public function test_publish_body_comes_from_description_not_content(): void
    {
        $routeArticle = $this->createRouteArticle([
            'description' => 'The short description',
            'content' => 'The very much longer full article body text',
        ]);

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldNotReceive('execute');

        $captured = null;
        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldReceive('publishRouteArticle')
            ->once()
            ->andReturnUsing(function ($ra, $data) use (&$captured) {
                $captured = $data;

                return PublishOutcome::published($this->makePublication());
            });

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertSame('The short description', $captured['description']);
        $this->assertArrayNotHasKey('content', $captured);
    }

    public function test_publish_falls_back_to_fetching_when_description_is_blank(): void
    {
        $routeArticle = $this->createRouteArticle([
            'description' => '',
            'image_url' => null,
        ]);

        $fetched = ['title' => 'Fetched Title', 'description' => 'Fetched description'];

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldReceive('execute')->once()->andReturn($fetched);

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldReceive('publishRouteArticle')
            ->once()
            ->with(Mockery::any(), $fetched)
            ->andReturn(PublishOutcome::published($this->makePublication()));

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->fresh()->publish_status);
    }

    public function test_publish_falls_back_to_fetching_when_article_has_no_stored_data(): void
    {
        $routeArticle = $this->createRouteArticle([], unvalidated: true);

        $fetched = [
            'title' => 'Fetched Title',
            'description' => 'Fetched description',
            'thumbnail' => 'https://cdn.test/fetched.jpg',
        ];

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldReceive('execute')->once()->andReturn($fetched);

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldReceive('publishRouteArticle')
            ->once()
            ->with(Mockery::any(), $fetched)
            ->andReturn(PublishOutcome::published($this->makePublication()));

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->fresh()->publish_status);
    }

    public function test_publish_fails_when_fallback_fetch_returns_nothing(): void
    {
        $routeArticle = $this->createRouteArticle(['title' => 'Unreachable Article'], unvalidated: true);

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldReceive('execute')->once()->andReturn([]);

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldNotReceive('publishRouteArticle');

        $outcome = (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertTrue($outcome->failed());
        $this->assertSame(PublishStatusEnum::ERROR, $routeArticle->fresh()->publish_status);
        $this->assertDatabaseCount('article_publications', 0);
        $this->assertDatabaseHas('notifications', [
            'type' => NotificationTypeEnum::PUBLISH_FAILED->value,
        ]);
    }

    public function test_a_failure_stores_the_reason_and_stops_further_attempts(): void
    {
        $routeArticle = $this->createRouteArticle([], unvalidated: true);

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldReceive('execute')->andReturn([]);

        $publishingService = Mockery::mock(ArticlePublishingService::class);

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $routeArticle->refresh();

        $this->assertSame(PublishStatusEnum::ERROR, $routeArticle->publish_status);
        $this->assertNotNull($routeArticle->publish_error);
        $this->assertFalse(
            RouteArticle::query()->dueForPublishing()->whereKey($routeArticle->getKey())->exists()
        );
    }

    public function test_a_successful_publish_clears_an_earlier_failure(): void
    {
        $routeArticle = $this->createRouteArticle([
            'description' => 'Stored description',
        ]);
        $routeArticle->recordPublishFailed('an earlier failure');

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldNotReceive('execute');

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldReceive('publishRouteArticle')
            ->once()
            ->andReturn(PublishOutcome::published($this->makePublication()));

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $routeArticle->refresh();

        $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->publish_status);
        $this->assertNull($routeArticle->publish_error);
    }

    public function test_publish_fails_when_fallback_recovers_only_a_title(): void
    {
        $routeArticle = $this->createRouteArticle([], unvalidated: true);

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldReceive('execute')->once()->andReturn(['title' => 'Recovered Title']);

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldNotReceive('publishRouteArticle');

        $outcome = (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertTrue($outcome->failed());
        $this->assertSame(PublishStatusEnum::ERROR, $routeArticle->fresh()->publish_status);
    }

    public function test_publish_succeeds_when_fallback_returns_a_description_without_a_thumbnail(): void
    {
        $routeArticle = $this->createRouteArticle([], unvalidated: true);

        $fetched = ['title' => 'Recovered Title', 'description' => 'Recovered description'];

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldReceive('execute')->once()->andReturn($fetched);

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldReceive('publishRouteArticle')
            ->once()
            ->with(Mockery::any(), $fetched)
            ->andReturn(PublishOutcome::published($this->makePublication()));

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->fresh()->publish_status);
    }

    public function test_publish_does_not_refetch_for_articles_stored_before_image_url_existed(): void
    {
        $routeArticle = $this->createRouteArticle([
            'title' => 'Legacy Title',
            'description' => 'Legacy description',
            'image_url' => null,
        ]);

        $fetcher = Mockery::mock(FetchArticleDataAction::class);
        $fetcher->shouldNotReceive('execute');

        $publishingService = Mockery::mock(ArticlePublishingService::class);
        $publishingService->shouldReceive('publishRouteArticle')
            ->once()
            ->with(Mockery::any(), [
                'title' => 'Legacy Title',
                'description' => 'Legacy description',
                'thumbnail' => null,
            ])
            ->andReturn(PublishOutcome::published($this->makePublication()));

        (new PublishRouteArticleAction($fetcher, $publishingService, new NotificationService))
            ->execute($routeArticle);

        $this->assertSame(PublishStatusEnum::PUBLISHED, $routeArticle->fresh()->publish_status);
    }
}
