<?php

namespace Tests\Feature;

use App\Campaigns\Publisher;
use App\Campaigns\PublishGate;
use App\Models\Asset;
use App\Models\AssetPlatformRef;
use App\Models\Campaign;
use App\Models\Decision;
use App\Models\User;
use App\Services\Meta\CreativeSpec;
use App\Services\Meta\MetaException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use InvalidArgumentException;
use Tests\SeedsVerifiedCountries;
use Tests\TestCase;

/**
 * Publishing a campaign whose creative is a video.
 *
 * It crashed, and it crashed late. `ReviewAssets` stores a video as external_id
 * and an image as external_hash; `AssetPlatformRef::reference()` returns either,
 * so the publish gate saw a video as ready; and the publisher then read the hash
 * specifically, got null, and handed null to a constructor typed string. The
 * TypeError surfaced after the campaign and the ad set already existed on the
 * live account.
 *
 * Underneath that, video was never built: the creative payload knew only
 * image_hash, in both transports.
 */
class VideoCreativeTest extends TestCase
{
    use RefreshDatabase;
    use SeedsVerifiedCountries;

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

        $this->seedVerifiedCountries();

        config([
            'platforms.guardrails.allowed_ad_accounts' => ['act_777'],
            'platforms.guardrails.max_daily_budget' => 100,
            'platforms.guardrails.retry_backoff_ms' => 0,
            'platforms.meta.mcp.enabled' => false,
            'platforms.meta.token' => 'test-token',
            // No sitting through a real transcode.
            'platforms.meta.video_ready_wait_ms' => 0,
            'platforms.meta.video_ready_attempts' => 3,
        ]);
    }

    private function campaign(string $kind = 'video'): Campaign
    {
        $user = User::factory()->create();

        $campaign = Campaign::create([
            'user_id' => $user->id,
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
            'ad_account_id' => 'act_777',
            'page_id' => 'page-1',
            'objective' => 'OUTCOME_TRAFFIC',
            'special_ad_categories' => ['NONE'],
            'landing_url' => 'https://example.com',
            'utm' => '',
            'budget' => 20,
            'currency' => 'USD',
            'bid_strategy' => 'LOWEST_COST_WITHOUT_CAP',
            'locations' => ['countries' => ['US']],
            'placements' => ['facebook'],
        ]);

        $asset = Asset::create([
            'owner_type' => $user->getMorphClass(),
            'owner_id' => $user->id,
            'kind' => $kind,
            'source' => 'generated',
            'disk' => 'public',
            'path' => 'creatives/x.'.($kind === 'video' ? 'mp4' : 'png'),
            'original_name' => 'x.'.($kind === 'video' ? 'mp4' : 'png'),
            'mime' => $kind === 'video' ? 'video/mp4' : 'image/png',
            'bytes' => 10,
            'checksum' => 'sum-'.$kind,
        ]);

        // Exactly what ReviewAssets writes for each kind.
        AssetPlatformRef::create([
            'asset_id' => $asset->id,
            'platform' => 'meta',
            'ad_account_id' => 'act_777',
            ...$kind === 'video' ? ['external_id' => '99887766'] : ['external_hash' => 'HASH123'],
        ]);

        $campaign->ads()->create([
            'headline' => 'Short headline',
            'primary_text' => 'Some body copy.',
            'description' => 'A supporting line.',
            'cta' => 'LEARN_MORE',
            'asset_id' => $asset->id,
        ]);

        return $campaign->fresh();
    }

    /** @param array<string, mixed> $overrides */
    private function fakeMeta(array $overrides = []): void
    {
        Http::fake([
            '*/thumbnails*' => Http::response(['data' => [
                ['uri' => 'https://scontent.example/not-preferred.jpg', 'is_preferred' => false],
                ['uri' => 'https://scontent.example/preferred.jpg', 'is_preferred' => true],
            ]]),
            '*99887766*' => Http::response($overrides['status'] ?? ['status' => ['video_status' => 'ready']]),
            '*/campaigns*' => Http::response(['id' => '120111']),
            '*/adsets*' => Http::response(['id' => '120222']),
            '*/adcreatives*' => Http::response(['id' => '120333']),
            '*/ads*' => Http::response(['id' => '120444']),
            '*' => Http::response(['success' => true]),
        ]);
    }

    private function publish(Campaign $campaign): Decision
    {
        $publisher = app(Publisher::class);

        return $publisher->approve($publisher->propose($campaign, 'test'), 'test');
    }

    // ------------------------------------------------------------- the crash

    public function test_a_video_campaign_publishes_rather_than_crashing(): void
    {
        $campaign = $this->campaign();
        $this->fakeMeta();

        $decision = $this->publish($campaign);

        $this->assertTrue($decision->wasApplied(), 'publishing a video creative should succeed');
        $this->assertSame('120111', $campaign->fresh()->external_campaign_id);
    }

    /** The payload Meta is actually sent for a video. */
    public function test_the_creative_is_sent_as_video_data_with_a_thumbnail(): void
    {
        $this->fakeMeta();
        $this->publish($this->campaign());

        Http::assertSent(function ($request): bool {
            if (! str_contains($request->url(), '/adcreatives')) {
                return false;
            }

            $story = json_decode($request->data()['object_story_spec'], true);

            return isset($story['video_data'])
                && ! isset($story['link_data'])
                && $story['video_data']['video_id'] === '99887766'
                && $story['video_data']['image_url'] === 'https://scontent.example/preferred.jpg';
        });
    }

    /**
     * Meta names the same ideas differently inside video_data.
     *
     * The headline is title rather than name, the description is
     * link_description, and the destination moves into the call to action
     * because video_data has no link field.
     */
    public function test_video_data_uses_metas_own_field_names(): void
    {
        $this->fakeMeta();
        $this->publish($this->campaign());

        Http::assertSent(function ($request): bool {
            if (! str_contains($request->url(), '/adcreatives')) {
                return false;
            }

            $video = json_decode($request->data()['object_story_spec'], true)['video_data'] ?? [];

            return ($video['title'] ?? null) === 'Short headline'
                && ($video['link_description'] ?? null) === 'A supporting line.'
                && ($video['call_to_action']['value']['link'] ?? null) === 'https://example.com';
        });
    }

    /** An image campaign is untouched by any of this. */
    public function test_an_image_campaign_still_sends_link_data(): void
    {
        $this->fakeMeta();
        $this->publish($this->campaign('image'));

        Http::assertSent(function ($request): bool {
            if (! str_contains($request->url(), '/adcreatives')) {
                return false;
            }

            $story = json_decode($request->data()['object_story_spec'], true);

            return isset($story['link_data'])
                && ! isset($story['video_data'])
                && $story['link_data']['image_hash'] === 'HASH123';
        });
    }

    // ------------------------------------------------------------ processing

    /**
     * An ad naming a video Meta has not finished transcoding is rejected.
     *
     * Refused here, with a sentence saying to try again, rather than letting
     * Meta refuse it after a campaign and ad set already exist.
     */
    public function test_a_video_still_processing_stops_the_publish(): void
    {
        $this->fakeMeta(['status' => ['status' => ['video_status' => 'processing']]]);

        $this->expectException(MetaException::class);
        $this->expectExceptionMessage('still being processed');

        $this->publish($this->campaign());
    }

    public function test_a_video_meta_could_not_process_is_reported_as_such(): void
    {
        $this->fakeMeta(['status' => ['status' => ['video_status' => 'error']]]);

        $this->expectException(MetaException::class);
        $this->expectExceptionMessage('could not process');

        $this->publish($this->campaign());
    }

    // --------------------------------------------------------- the spec itself

    public function test_a_creative_must_be_one_kind_or_the_other(): void
    {
        $this->expectException(InvalidArgumentException::class);

        new CreativeSpec(
            adAccountId: 'act_777', pageId: 'page-1', landingUrl: 'https://example.com',
            headline: 'h', primaryText: 'p', callToAction: 'LEARN_MORE', name: 'n',
        );
    }

    public function test_a_video_creative_without_a_thumbnail_is_refused(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('thumbnail');

        new CreativeSpec(
            adAccountId: 'act_777', pageId: 'page-1', landingUrl: 'https://example.com',
            headline: 'h', primaryText: 'p', callToAction: 'LEARN_MORE', name: 'n',
            videoId: '99887766',
        );
    }

    /** The gate was already letting video through; it should keep doing so, now truthfully. */
    public function test_the_gate_allows_a_video_campaign(): void
    {
        $this->assertSame([], app(PublishGate::class)->check($this->campaign()));
    }
}
