<?php

namespace Tests\Feature;

use App\Agent\Tools\Campaign\SetTargeting;
use App\Campaigns\Platforms\Platforms;
use App\Campaigns\PublishGate;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\JsonSchema\JsonSchemaTypeFactory;
use Illuminate\JsonSchema\Serializer;
use Tests\TestCase;

/**
 * Meta's shape must not be imposed on other platforms.
 *
 * Found reviewing the multi-platform work on 21 Sep. Until Google existed there
 * was one platform, so anything true of Meta was true full stop, and several
 * checks were written as if that would hold. Each of these is a place where a
 * Google campaign was asked for something it cannot have.
 */
class PlatformShapedCampaignTest extends TestCase
{
    use RefreshDatabase;

    private function campaign(string $platform, array $attributes = []): Campaign
    {
        return Campaign::create([
            'user_id' => User::factory()->create()->id,
            'name' => 'Test',
            'platform' => $platform,
            'status' => 'draft',
            ...$attributes,
        ]);
    }

    /** A Facebook Page is Meta's requirement, not everyone's. */
    public function test_a_page_is_not_demanded_of_a_platform_that_has_none(): void
    {
        $problems = app(PublishGate::class)->check($this->campaign('google'));

        $this->assertNotContains('Page not selected', $problems);
    }

    /** And is still demanded where it genuinely is required. */
    public function test_a_page_is_still_demanded_on_meta(): void
    {
        $problems = app(PublishGate::class)->check($this->campaign('meta'));

        $this->assertContains('Page not selected', $problems);
    }

    /** The rules themselves answer the question, so a new platform inherits it. */
    public function test_only_meta_declares_that_it_publishes_from_a_page(): void
    {
        $platforms = app(Platforms::class);

        $this->assertTrue($platforms->for($this->campaign('meta'))->requiresPage());
        $this->assertFalse($platforms->for($this->campaign('google'))->requiresPage());
    }

    /**
     * The panel describes the campaign in front of it.
     *
     * Rendering the stored view is the only way to catch this: the rows are
     * built in the template, so nothing below it knows they exist.
     */
    public function test_the_panel_hides_fields_the_platform_does_not_have(): void
    {
        $campaign = $this->campaign('google');
        $rules = app(Platforms::class)->for($campaign);

        $html = view('partials.campaign-panel', [
            'campaign' => $campaign,
            'rules' => $rules,
            'progress' => [],
            'nextStep' => null,
            'blocking' => [],
            'sources' => [],
            'pending' => [],
            'needsRebuild' => [],
            'campaigns' => collect([$campaign]),
            'assets' => collect(),
        ])->render();

        // Meta's own concepts, which Google has no equivalent of.
        $this->assertStringNotContainsString('Instagram', $html);
        $this->assertStringNotContainsString('Facebook', $html);

        // Google does have these, under its own names. Showing them as
        // "Placements" and "Pixel" would be describing a Google campaign in
        // Meta's words, which is the same defect one level down.
        $this->assertStringContainsString('Networks', $html);
        $this->assertStringContainsString('Conversion action', $html);
        $this->assertStringNotContainsString('Placements', $html);
        $this->assertStringNotContainsString('>Pixel<', $html);
        // A text ad has no creative, so a count of them is not an outstanding
        // task, it is a line about something that does not exist here.
        $this->assertStringNotContainsString('Creatives:', $html);

        // The shared ones stay, or the panel would say nothing at all.
        $this->assertStringContainsString('Landing page', $html);
        $this->assertStringContainsString('Countries', $html);
    }

    /** Meta's panel is unchanged, which is what makes the hiding safe. */
    public function test_the_panel_still_shows_meta_fields_on_meta(): void
    {
        $campaign = $this->campaign('meta');

        $html = view('partials.campaign-panel', [
            'campaign' => $campaign,
            'rules' => app(Platforms::class)->for($campaign),
            'progress' => [],
            'nextStep' => null,
            'blocking' => [],
            'sources' => [],
            'pending' => [],
            'needsRebuild' => [],
            'campaigns' => collect([$campaign]),
            'assets' => collect(),
        ])->render();

        $this->assertStringContainsString('Instagram', $html);
        $this->assertStringContainsString('Pixel', $html);
        $this->assertStringContainsString('Placements', $html);
        $this->assertStringContainsString('Creatives:', $html);
    }

    /**
     * The tool must not name Meta's placements to the model.
     *
     * schema() is written once and sent for every campaign, so a hardcoded list
     * is an instruction to offer Messenger on a Google campaign.
     */
    public function test_the_targeting_tool_does_not_name_one_platforms_placements(): void
    {
        $tool = app(SetTargeting::class);
        $schema = $tool->schema(new JsonSchemaTypeFactory);
        $description = Serializer::serialize($schema['placements'])['description'] ?? '';

        foreach (['facebook', 'instagram', 'messenger', 'audience_network', 'threads'] as $placement) {
            $this->assertStringNotContainsString(
                $placement,
                strtolower($description),
                'The placements description names Meta values, which the model will use on any platform.',
            );
        }
    }

    /**
     * Two platforms cannot quietly spend more than the total agreed once.
     *
     * Each share passes the per-campaign floor and ceiling on its own. Only
     * adding them up catches it, and nothing did: a 5.00 total with two 10.00
     * shares reported no problem at all.
     */
    public function test_shares_adding_up_to_more_than_the_brief_total_block_publishing(): void
    {
        $user = User::factory()->create();
        $brief = Brief::create([
            'user_id' => $user->id,
            'conversation_id' => 'c1',
            'budget' => 5,
            'budget_mode' => 'daily',
        ]);

        $meta = $this->campaign('meta', ['brief_id' => $brief->id, 'budget' => 10]);
        $this->campaign('google', ['brief_id' => $brief->id, 'budget' => 10]);

        $problems = app(PublishGate::class)->check($meta->fresh());

        $this->assertNotEmpty(
            array_filter($problems, fn (string $p): bool => str_contains($p, 'over the 5.00')),
            'Two campaigns may spend 20.00 a day against a 5.00 total. Problems: '.implode(' | ', $problems),
        );
    }

    /** Shares that fit the total are not a problem. */
    public function test_shares_within_the_brief_total_do_not_block_publishing(): void
    {
        $user = User::factory()->create();
        $brief = Brief::create([
            'user_id' => $user->id,
            'conversation_id' => 'c2',
            'budget' => 20,
            'budget_mode' => 'daily',
        ]);

        $meta = $this->campaign('meta', ['brief_id' => $brief->id, 'budget' => 10]);
        $this->campaign('google', ['brief_id' => $brief->id, 'budget' => 10]);

        $problems = app(PublishGate::class)->check($meta->fresh());

        $this->assertEmpty(array_filter($problems, fn (string $p): bool => str_contains($p, 'over the')));
    }

    /**
     * An unsplit total is the same overspend wearing a different face.
     *
     * A campaign with no budget of its own reads the brief's total through
     * ResolvesFromBrief, so two of them spend it twice over.
     */
    public function test_an_unsplit_total_blocks_publishing(): void
    {
        $user = User::factory()->create();
        $brief = Brief::create([
            'user_id' => $user->id,
            'conversation_id' => 'c3',
            'budget' => 20,
            'budget_mode' => 'daily',
        ]);

        $meta = $this->campaign('meta', ['brief_id' => $brief->id]);
        $this->campaign('google', ['brief_id' => $brief->id]);

        $problems = app(PublishGate::class)->check($meta->fresh());

        $this->assertNotEmpty(
            array_filter($problems, fn (string $p): bool => str_contains($p, 'has not been split')),
            'Problems: '.implode(' | ', $problems),
        );
    }

    /** A single platform spending the whole total is the normal case. */
    public function test_one_platform_holding_the_whole_total_is_not_an_overspend(): void
    {
        $user = User::factory()->create();
        $brief = Brief::create([
            'user_id' => $user->id,
            'conversation_id' => 'c4',
            'budget' => 20,
            'budget_mode' => 'daily',
        ]);

        $meta = $this->campaign('meta', ['brief_id' => $brief->id]);

        $problems = app(PublishGate::class)->check($meta->fresh());

        $this->assertEmpty(array_filter(
            $problems,
            fn (string $p): bool => str_contains($p, 'over the') || str_contains($p, 'has not been split'),
        ));
    }
}
