<?php

namespace Tests\Feature;

use App\Campaigns\Platforms\LinkedInRules;
use App\Campaigns\PublishGate;
use App\Models\Campaign;
use App\Models\User;
use App\Services\LinkedIn\LinkedInException;
use App\Services\LinkedIn\LinkedInRestApi;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * Three ways the LinkedIn path lied about what had happened.
 *
 * Two of them turned a success into a failure or a failure into a success,
 * which is the pair of mistakes an error path exists to avoid. The third
 * refused to publish for a reason nobody could act on.
 */
class LinkedInFailsLoudlyRatherThanQuietlyTest extends TestCase
{
    use RefreshDatabase;

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

        config([
            'platforms.linkedin.token' => 'test-token',
            'platforms.linkedin.publish_enabled' => false,
            'platforms.guardrails.allowed_ad_accounts' => ['556447014'],
        ]);
    }

    /**
     * A missing file is refused instead of uploaded as nothing.
     *
     * file_get_contents() on a path that is not there emits a warning and
     * returns false, and withBody(false) sends an empty body. LinkedIn accepted
     * it, handed back an asset URN, and the creative was built around a zero
     * byte image. The publish succeeded and the ad had no picture in it.
     */
    public function test_an_image_that_is_not_on_disk_is_refused(): void
    {
        Http::fake(['https://api.linkedin.com/*' => Http::response(
            ['value' => ['uploadUrl' => 'https://upload.linkedin.com/x', 'image' => 'urn:li:image:1']], 200
        )]);

        $this->expectException(LinkedInException::class);

        app(LinkedInRestApi::class)->uploadImage('556447014', '/var/tmp/does-not-exist.png', 'Hero');
    }

    /**
     * A successful response is not an error because it has words in it.
     *
     * The check threw whenever the body carried a `message` key, and `message`
     * is ordinary content on LinkedIn rather than a sign of failure: a post has
     * one, and so does an InMail creative. Reading back a post you had just
     * written raised your own ad copy as the exception, and the caller treated
     * that as a publish failure and rolled the campaign back.
     *
     * A 200 rather than a 201 is deliberate. A 201 returns from the header
     * above this check and never reaches it, so a test written against 201
     * passes whether the bug is present or not, which is how this one was
     * first written.
     */
    public function test_a_successful_response_carrying_a_message_key_is_not_an_error(): void
    {
        Http::fake(['https://api.linkedin.com/*' => Http::response(
            ['id' => 'urn:li:share:99', 'message' => 'Block intrusive ads for good.'], 200
        )]);

        $creative = app(LinkedInRestApi::class)->createCreative('556447014', [
            'campaign' => 'urn:li:sponsoredCampaign:1',
            'message' => 'Block intrusive ads for good.',
        ]);

        $this->assertSame('Block intrusive ads for good.', $creative['message'] ?? null);
    }

    /** A real failure still throws, and still says what LinkedIn said. */
    public function test_a_failed_response_is_still_an_error(): void
    {
        Http::fake(['https://api.linkedin.com/*' => Http::response(
            ['message' => 'Invalid ad account', 'status' => 400], 400
        )]);

        $this->expectException(LinkedInException::class);
        $this->expectExceptionMessageMatches('/Invalid ad account/');

        app(LinkedInRestApi::class)->createCampaign('556447014', ['name' => 'Q4']);
    }

    /** And an error on a 200, which LinkedIn does send, is still trusted. */
    public function test_an_error_key_on_a_successful_status_is_still_an_error(): void
    {
        Http::fake(['https://api.linkedin.com/*' => Http::response(
            ['error' => ['message' => 'Permission denied']], 200
        )]);

        $this->expectException(LinkedInException::class);

        app(LinkedInRestApi::class)->createCampaign('556447014', ['name' => 'Q4']);
    }

    /**
     * Lead generation can reach publish.
     *
     * requiresPixel() was true for LEAD_GENERATION, PublishGate blocks a blank
     * pixel_id, steps() has no pixel step, and choose_pixel speaks only to
     * Meta. The four together meant every LinkedIn lead-gen campaign was
     * refused at the last moment for a field the product could not fill, and
     * the buyer was left with a finished campaign and no way to send it.
     */
    public function test_a_linkedin_lead_gen_campaign_is_not_blocked_on_a_pixel(): void
    {
        $rules = new LinkedInRules;

        $this->assertFalse($rules->requiresPixel('LEAD_GENERATION'));
        $this->assertFalse($rules->requiresPixel('WEBSITE_CONVERSIONS'));
    }

    /** The gate agrees, which is the thing that actually stopped the publish. */
    public function test_the_gate_does_not_hold_a_lead_gen_campaign_for_a_pixel(): void
    {
        $campaign = Campaign::create([
            'user_id' => User::factory()->create()->id,
            'name' => 'Leads',
            'platform' => 'linkedin',
            'status' => 'draft',
            'objective' => 'LEAD_GENERATION',
            'pixel_id' => null,
        ]);

        $problems = json_encode(app(PublishGate::class)->check($campaign));

        $this->assertStringNotContainsString('pixel', $problems);
    }
}
