<?php

namespace Tests\Feature;

use App\Campaigns\Publishing\LinkedInPublisher;
use App\Exceptions\PlatformNotImplementedException;
use App\Models\Campaign;
use App\Models\User;
use App\Services\Platforms\LinkedInAdapter;
use App\Services\Platforms\PlatformAdapterFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * Nothing in the adapter scaffold can set a campaign live without a guard.
 *
 * MetaAdapter::publish() posted status ACTIVE straight at the Graph API with a
 * bare Http call, so it reached none of the checks every other Meta write goes
 * through: not the PLATFORM_PAUSE_ALL_WRITES kill switch, not the allowed
 * accounts list, not the hourly call budget, and not the api_calls audit row,
 * because all four live in CallLogger and it never went near it. Its
 * createCampaign() and attachCreative() bypassed the same four.
 *
 * It was unreachable - PlatformAdapterFactory is called by nothing, and Meta
 * really publishes through MetaPublisher -> Meta -> GraphApi -> CallLogger -
 * but it was wired into config/creative.php, so it was one call away from being
 * the path that ran. Deleted rather than patched, because a second Meta
 * implementation with none of the guards is not worth keeping for a factory
 * nobody calls.
 *
 * These tests exist so the gap cannot quietly reopen.
 */
class NoAdapterCanGoLiveUnguardedTest extends TestCase
{
    use RefreshDatabase;

    private function factory(): PlatformAdapterFactory
    {
        return app(PlatformAdapterFactory::class);
    }

    /**
     * The defect: asking the factory for Meta must not hand back a live wire.
     *
     * Failing loudly is the right answer. A caller that wanted to publish to
     * Meta wanted MetaPublisher, and being told so beats being handed something
     * that spends money with no kill switch.
     */
    public function test_the_factory_has_no_meta_adapter_to_hand_out(): void
    {
        $this->expectException(PlatformNotImplementedException::class);

        $this->factory()->forPlatform('meta');
    }

    /** And no configured adapter class is the deleted one, under any key. */
    public function test_no_platform_is_wired_to_the_deleted_adapter(): void
    {
        foreach ((array) config('creative.platform_adapters') as $platform => $class) {
            $this->assertStringNotContainsString(
                'MetaAdapter',
                (string) $class,
                "{$platform} is wired to the unguarded adapter",
            );
        }
    }

    /**
     * The platforms with no integration refuse rather than improvise.
     *
     * UnimplementedPlatformAdapter::activate() throws, which is what Meta's
     * should have done and did not. The method was called publish() until
     * activation became its own act; the interface described it as
     * "publish/activate", which is the conflation that let LinkedIn go live as a
     * side effect of being published.
     */
    public function test_an_unimplemented_platform_refuses_to_start_a_campaign(): void
    {
        $campaign = $this->campaign('google');

        foreach (['google', 'tiktok', 'taboola'] as $platform) {
            try {
                $this->factory()->forPlatform($platform)->activate($campaign);
                $this->fail("{$platform} started a campaign without an implementation");
            } catch (PlatformNotImplementedException) {
                $this->assertTrue(true);
            }
        }
    }

    /**
     * LinkedIn's adapter can start a campaign, and only when asked to.
     *
     * There used to be a config switch consulted here, so whether a campaign went
     * live was a property of the environment. It is now a decision: nothing calls
     * activate() except an approved activation, and publishing does not call it at
     * all - which is what the publisher test below pins.
     *
     * What is asserted here is that it walks the tree. A campaign set ACTIVE over
     * DRAFT creatives serves nothing and reports it as a campaign with no
     * impressions.
     */
    public function test_the_linkedin_adapter_starts_every_level(): void
    {
        config([
            'platforms.linkedin.token' => 'test-token',
            'platforms.guardrails.allowed_ad_accounts' => ['linkedin:*'],
        ]);

        Http::fake(['https://api.linkedin.com/*' => Http::response([], 200)]);

        $campaign = $this->campaign('linkedin');
        $campaign->update([
            'ad_account_id' => '556447014',
            'external_ids' => ['campaign_group_id' => '77', 'campaign_id' => '12345'],
        ]);

        app(LinkedInAdapter::class)->activate($campaign->fresh(), ['999']);

        $sent = collect(Http::recorded())->map(fn (array $pair): string => $pair[0]->url().' '
            .json_encode($pair[0]->data()));

        foreach (['creatives', 'adCampaignGroups', 'adCampaigns'] as $level) {
            $this->assertTrue(
                $sent->contains(fn (string $line): bool => str_contains($line, $level)),
                "{$level} was not started",
            );
        }

        $this->assertTrue(
            $sent->contains(fn (string $line): bool => str_contains($line, 'ACTIVE')),
            'nothing was set live',
        );
    }

    /**
     * And publishing never reaches it.
     *
     * LinkedInPublisher::create() used to call adapter->publish() as its last step,
     * so on LinkedIn alone publishing meant going live, held back only by a switch
     * rewriting statuses further down. Meta and Google have never done this.
     *
     * Asserted by refusing the call rather than by reading the file: my first
     * version of this searched create() for "->activate(" and failed on the comment
     * explaining why it is not there, which is the whole problem with testing
     * source text.
     */
    public function test_publishing_does_not_start_anything(): void
    {
        $adapter = \Mockery::mock(LinkedInAdapter::class);
        $adapter->shouldReceive('createCampaign')->andReturn('urn:li:sponsoredCampaign:1');
        $adapter->shouldReceive('createAd')->andReturn('urn:li:sponsoredCreative:1');

        // The point of the test. Either name is a failure: one is the verb it used
        // to call, the other is the verb that would replace it.
        $adapter->shouldNotReceive('activate');
        $adapter->shouldNotReceive('publish');

        $campaign = $this->campaign('linkedin');
        $campaign->update(['external_campaign_id' => null]);

        $created = [];
        (new LinkedInPublisher($adapter))
            ->create($campaign->fresh(), 'tester', $created);

        $this->assertSame('urn:li:sponsoredCampaign:1', $created['campaign_id']);
    }

    private function campaign(string $platform): Campaign
    {
        return Campaign::create([
            'user_id' => User::factory()->create()->id,
            'name' => 'Winter Sale',
            'platform' => $platform,
            'status' => 'published',
            'external_campaign_id' => '12345',
        ]);
    }
}
