<?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\CampaignAd;
use App\Models\User;
use App\Services\LinkedIn\LinkedInRestApi;
use App\Services\Platforms\LinkedInAdapter;
use App\Services\Platforms\Support\CallLogger;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Tests\SeedsVerifiedCountries;
use Tests\TestCase;

class LinkedInPublishTest extends TestCase
{
    use RefreshDatabase;
    use SeedsVerifiedCountries;

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

        $this->seedVerifiedCountries();

        config([
            'platforms.guardrails.allowed_ad_accounts' => ['509999999', 'urn:li:sponsoredAccount:509999999'],
            'platforms.guardrails.max_daily_budget' => 50,
            'platforms.guardrails.max_calls_per_hour' => 60,
            'platforms.linkedin.token' => 'mock-token',
            'platforms.linkedin.ad_account_id' => '509999999',
        ]);

        $this->actingAs(User::factory()->create());

        $this->app->scoped(LinkedInRestApi::class, fn (): LinkedInRestApi => new LinkedInRestApi(
            'mock-token',
            '202401',
            app(CallLogger::class),
        ));
    }

    public function test_publishes_linkedin_campaign_end_to_end(): void
    {
        Http::fake([
            'https://api.linkedin.com/rest/adAccounts/*/adCampaignGroups' => Http::response([], 201, [
                'x-restli-id' => 'urn:li:sponsoredCampaignGroup:11111',
            ]),
            'https://api.linkedin.com/rest/adAccounts/*/adCampaigns' => Http::response([], 201, [
                'x-restli-id' => 'urn:li:sponsoredCampaign:22222',
            ]),
            'https://api.linkedin.com/rest/adAccounts/*/creatives' => Http::response([], 201, [
                'x-restli-id' => 'urn:li:sponsoredCreative:33333',
            ]),
            'https://api.linkedin.com/rest/adAccounts/*/adCampaigns/22222' => Http::response([], 200),
        ]);

        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'platform' => 'linkedin',
            'ad_account_id' => '509999999',
            'page_id' => 'urn:li:organization:123456',
            'name' => 'Q4 Enterprise Growth',
            'objective' => 'WEBSITE_VISITS',
            'budget' => 25.00,
            'budget_level' => 'campaign',
            'budget_mode' => 'daily',
            'landing_url' => 'https://example.com/offer',
            'locations' => ['countries' => ['US']],
            'status' => 'draft',
        ]);

        $asset = Asset::create([
            'owner_type' => User::class,
            'owner_id' => auth()->id(),
            'kind' => 'image',
            'source' => 'upload',
            'disk' => 'local',
            'path' => 'assets/test.jpg',
            'original_name' => 'test.jpg',
            'mime' => 'image/jpeg',
            'bytes' => 1024,
            'checksum' => 'fake-checksum',
        ]);

        AssetPlatformRef::create([
            'asset_id' => $asset->id,
            'platform' => 'linkedin',
            'ad_account_id' => '509999999',
            'external_id' => 'urn:li:digitalmediaAsset:123456',
        ]);

        CampaignAd::create([
            'campaign_id' => $campaign->id,
            'asset_id' => $asset->id,
            'headline' => 'Scale Your Marketing with AI',
            'primary_text' => 'Discover how our platform simplifies campaign creation.',
            'description' => 'Drive qualified B2B leads.',
            'cta' => 'LEARN_MORE',
            'status' => 'draft',
        ]);

        $gate = app(PublishGate::class);
        $this->assertSame([], $gate->check($campaign));

        $publisher = app(Publisher::class);
        $proposal = $publisher->propose($campaign, 'test-actor');

        $decision = $publisher->approve($proposal, 'test-actor');

        $this->assertTrue($decision->wasApplied());
        $this->assertSame('published', $campaign->fresh()->status);
        $this->assertSame('urn:li:sponsoredCampaign:22222', $campaign->fresh()->external_campaign_id);

        // Verify structured targetingCriteria and valid objectiveType were sent
        Http::assertSent(function ($request) {
            if (str_contains($request->url(), 'adCampaigns') && ! str_contains($request->url(), '22222')) {
                $targeting = $request['targetingCriteria'] ?? null;
                $hasLocation = isset($targeting['include']['and'][0]['or']['urn:li:adTargetingFacet:locations'])
                    && $targeting['include']['and'][0]['or']['urn:li:adTargetingFacet:locations'] === ['urn:li:geo:103644278'];
                $hasValidObjective = ($request['objectiveType'] ?? null) === 'WEBSITE_VISIT';

                return $hasLocation && $hasValidObjective;
            }

            return false;
        });

        // Verify that no request to set status to ACTIVE was sent to LinkedIn
        Http::assertNotSent(function ($request) {
            return ($request['patch']['$set']['status'] ?? null) === 'ACTIVE';
        });
    }

    /**
     * The transport sends the status it is given.
     *
     * This asserted the opposite: it asked for ACTIVE and checked that PAUSED went
     * out, which pinned a rewrite that has been removed. The rewrite was the defect
     * - a caller that asks for ACTIVE and gets PAUSED has no way to know, and the
     * LinkedIn adapter did exactly that for months while believing it was going
     * live.
     *
     * Nothing goes live by accident any more because nothing asks to: the adapter
     * builds DRAFT, and ACTIVE is only ever sent by an approved activation.
     */
    public function test_the_transport_sends_the_status_it_is_given(): void
    {
        Http::fake([
            'https://api.linkedin.com/rest/adAccounts/*/adCampaigns/22222' => Http::response([], 200),
        ]);

        app(LinkedInRestApi::class)->updateEntity('509999999', 'campaign', '22222', [
            'status' => 'ACTIVE',
        ]);

        Http::assertSent(fn ($request): bool => ($request['patch']['$set']['status'] ?? null) === 'ACTIVE');
    }

    public function test_upload_image_uses_organization_as_owner(): void
    {
        config(['platforms.linkedin.organization_id' => '106065219']);

        Http::fake([
            'https://api.linkedin.com/rest/images?action=initializeUpload' => Http::response([
                'value' => [
                    'uploadUrl' => 'https://api.linkedin.com/mock-upload-url',
                    'image' => 'urn:li:image:ORG_OWNED_123',
                ],
            ], 200),
            'https://api.linkedin.com/mock-upload-url' => Http::response('', 201),
        ]);

        Storage::fake('local');
        Storage::disk('local')->put('test.jpg', 'fake-image-bytes');
        $path = Storage::disk('local')->path('test.jpg');

        $api = app(LinkedInRestApi::class);
        $urn = $api->uploadImage('509999999', $path, 'test.jpg');

        $this->assertSame('urn:li:image:ORG_OWNED_123', $urn);

        Http::assertSent(function ($request) {
            if (str_contains($request->url(), 'images?action=initializeUpload')) {
                $owner = $request['initializeUploadRequest']['owner'] ?? null;
                $mediaAccount = $request['initializeUploadRequest']['mediaLibraryMetadata']['associatedAccount'] ?? null;

                return $owner === 'urn:li:organization:106065219'
                    && $mediaAccount === 'urn:li:sponsoredAccount:509999999';
            }

            return false;
        });
    }

    public function test_dsc_creation_recovers_when_image_ownership_mismatches(): void
    {
        config(['platforms.linkedin.organization_id' => '106065219']);

        Storage::fake('local');
        Storage::disk('local')->put('creatives/sample.jpg', 'img-data');

        $asset = Asset::create([
            'owner_type' => User::class,
            'owner_id' => auth()->id(),
            'kind' => 'image',
            'source' => 'upload',
            'disk' => 'local',
            'path' => 'creatives/sample.jpg',
            'original_name' => 'sample.jpg',
            'mime' => 'image/jpeg',
            'bytes' => 1024,
            'checksum' => 'fake-checksum-2',
        ]);

        // Old reference pointing to image uploaded with wrong owner
        AssetPlatformRef::create([
            'asset_id' => $asset->id,
            'platform' => 'linkedin',
            'ad_account_id' => '509999999',
            'external_id' => 'urn:li:image:OLD_WRONG_OWNER',
        ]);

        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'platform' => 'linkedin',
            'ad_account_id' => '509999999',
            'external_campaign_id' => 'urn:li:sponsoredCampaign:22222',
            'name' => 'Retry DSC Campaign',
            'objective' => 'WEBSITE_VISITS',
            'budget' => 25.00,
            'budget_level' => 'campaign',
            'budget_mode' => 'daily',
            'landing_url' => 'https://example.com/offer',
            'status' => 'draft',
        ]);

        $ad = CampaignAd::create([
            'campaign_id' => $campaign->id,
            'asset_id' => $asset->id,
            'headline' => 'Test DSC Headline',
            'primary_text' => 'Test Primary Text',
            'cta' => 'LEARN_MORE',
            'status' => 'draft',
        ]);

        $postCallCount = 0;
        Http::fake([
            'https://api.linkedin.com/rest/posts' => function ($request) use (&$postCallCount) {
                $postCallCount++;
                if ($postCallCount === 1) {
                    // First call fails with ownership error
                    return Http::response([
                        'message' => 'One or more of the contents is not owned by the author. All contents must be owned by the author',
                        'code' => '400',
                    ], 400);
                }

                // Second call with re-uploaded image succeeds
                return Http::response([], 201, [
                    'x-restli-id' => 'urn:li:share:NEW_RECOVERED_SHARE_999',
                ]);
            },
            'https://api.linkedin.com/rest/images?action=initializeUpload' => Http::response([
                'value' => [
                    'uploadUrl' => 'https://api.linkedin.com/mock-upload-url-2',
                    'image' => 'urn:li:image:REUPLOADED_ORG_OWNED',
                ],
            ], 200),
            'https://api.linkedin.com/mock-upload-url-2' => Http::response('', 201),
            'https://api.linkedin.com/rest/adAccounts/*/creatives' => Http::response([], 201, [
                'x-restli-id' => 'urn:li:sponsoredCreative:44444',
            ]),
        ]);

        $adapter = app(LinkedInAdapter::class);
        $creativeId = $adapter->createAd($campaign, $ad);

        $this->assertSame('urn:li:sponsoredCreative:44444', $creativeId);
        $this->assertSame(2, $postCallCount);

        // Verify AssetPlatformRef was updated to the re-uploaded image URN
        $ref = AssetPlatformRef::where('asset_id', $asset->id)
            ->where('platform', 'linkedin')
            ->first();
        $this->assertSame('urn:li:image:REUPLOADED_ORG_OWNED', $ref->external_id);

        // Verify landing page URL and call to action are sent to rest/posts
        Http::assertSent(function ($request) {
            if (str_contains($request->url(), 'rest/posts')) {
                return isset($request['contentLandingPage'])
                    && str_contains($request['contentLandingPage'], 'https://example.com/offer')
                    && ($request['contentCallToActionLabel'] ?? null) === 'LEARN_MORE';
            }

            return false;
        });
    }
}
