<?php

namespace Tests\Feature;

use App\Campaigns\PublishGate;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;

/**
 * A campaign reads its shared answers from the brief when it has none itself.
 *
 * This is the seam the whole multi-platform flow rests on: ask once, then let
 * each platform's campaign hold only what is genuinely its own. Resolution sits
 * on the model rather than at each reader, because the publish gate, the flow,
 * the panel, the publisher and the pending-changes comparison all read these
 * fields, and a fallback applied in some and not others would let the gate
 * approve a value the publisher never sends.
 *
 * What these pin: the campaign wins when it has an answer, the brief answers
 * when it does not, casts survive either way, and a campaign with no brief is
 * completely unaffected.
 */
class CampaignResolvesFromBriefTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

    protected Brief $brief;

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

        $this->user = User::factory()->create();

        $this->brief = Brief::create([
            'user_id' => $this->user->id,
            'conversation_id' => 'conv-resolve-0001',
            'landing_url' => 'https://example.com/shoes',
            'locations' => ['countries' => ['US', 'CA']],
            'budget' => 50,
            'budget_mode' => 'daily',
            'currency' => 'USD',
            'starts_at' => Carbon::parse('2026-10-01T00:00:00Z'),
            'ends_at' => Carbon::parse('2026-10-31T00:00:00Z'),
        ]);
    }

    private function campaign(array $attributes = []): Campaign
    {
        return Campaign::create([
            'user_id' => $this->user->id,
            'brief_id' => $this->brief->id,
            'name' => 'Meta campaign',
            'platform' => 'meta',
            'status' => 'draft',
            ...$attributes,
        ]);
    }

    public function test_the_brief_answers_what_the_campaign_does_not(): void
    {
        $campaign = $this->campaign()->fresh();

        $this->assertSame('https://example.com/shoes', $campaign->landing_url);
        $this->assertSame(['countries' => ['US', 'CA']], $campaign->locations);
        $this->assertEquals(50, $campaign->budget);
        $this->assertSame('daily', $campaign->budget_mode);
        $this->assertSame('USD', $campaign->currency);
    }

    /**
     * A platform-specific answer always beats the shared one.
     *
     * This is how one budget becomes each platform's share: the brief holds the
     * total the user named, the campaign holds what this platform gets.
     */
    public function test_the_campaigns_own_answer_wins(): void
    {
        $campaign = $this->campaign([
            'budget' => 20,
            'landing_url' => 'https://example.com/other',
        ])->fresh();

        $this->assertEquals(20, $campaign->budget);
        $this->assertSame('https://example.com/other', $campaign->landing_url);
    }

    /**
     * Casts have to survive the fallback, on both sides.
     *
     * The first attempt at this used accessors, which are handed the raw
     * database value and replace the cast: locations came back as a JSON string
     * and starts_at as a plain string, which broke 46 tests.
     *
     * The dates used to be the example here and are no longer shared through
     * this mechanism at all: a null means both "never asked" and "answered no
     * end date", and blank() cannot tell them apart, so a campaign set to run
     * continuously would have inherited the brief's end date. The array and
     * decimal casts cover the same ground. See NOT_SHARED_ON_PURPOSE.
     */
    public function test_casts_survive_the_fallback(): void
    {
        $campaign = $this->campaign()->fresh();

        $this->assertIsArray($campaign->locations);
        $this->assertSame(['US', 'CA'], $campaign->locations['countries']);
        $this->assertEquals(50, $campaign->budget);
    }

    /** And a date is not quietly inherited, because null there is ambiguous. */
    public function test_dates_are_not_inherited_through_the_fallback(): void
    {
        $campaign = $this->campaign()->fresh();

        $this->assertNull($campaign->starts_at);
        $this->assertNull($campaign->ends_at);
    }

    /** An empty list is not an answer to "where should this run". */
    public function test_an_empty_value_falls_back_rather_than_counting_as_answered(): void
    {
        $campaign = $this->campaign(['locations' => []])->fresh();

        $this->assertSame(['countries' => ['US', 'CA']], $campaign->locations);
    }

    /** Platform-specific fields have no shared equivalent and must never resolve. */
    public function test_fields_that_are_not_shared_never_fall_back(): void
    {
        $campaign = $this->campaign()->fresh();

        $this->assertNull($campaign->page_id);
        $this->assertNull($campaign->pixel_id);
        $this->assertNull($campaign->ad_account_id);
        $this->assertNull($campaign->objective);
    }

    /** An adopted campaign has no brief at all, and must be untouched by this. */
    public function test_a_campaign_with_no_brief_is_unaffected(): void
    {
        $campaign = Campaign::create([
            'user_id' => $this->user->id,
            'name' => 'Imported campaign',
            'platform' => 'meta',
            'status' => 'published',
            'budget' => 40,
        ])->fresh();

        $this->assertEquals(40, $campaign->budget);
        $this->assertNull($campaign->landing_url);
        $this->assertNull($campaign->locations);
    }

    /** Resolution is a read. Writing still writes to the campaign. */
    public function test_writes_land_on_the_campaign_not_the_brief(): void
    {
        $campaign = $this->campaign();

        $campaign->update(['budget' => 15]);

        $this->assertEquals(15, $campaign->fresh()->budget);
        $this->assertEquals(50, $this->brief->fresh()->budget, 'The brief was changed by a campaign write.');
        $this->assertEquals(15, $campaign->fresh()->getRawOriginal('budget'));
    }

    /** What the campaign itself holds stays visible, which the publish path needs. */
    public function test_the_raw_value_still_reports_only_the_campaigns_own_answer(): void
    {
        $campaign = $this->campaign()->fresh();

        $this->assertNull($campaign->getRawOriginal('landing_url'));
        $this->assertSame('https://example.com/shoes', $campaign->landing_url);
    }

    /**
     * The gate is the reason this lives on the model.
     *
     * If the gate read the campaign's own field it would refuse a campaign the
     * publisher would have been able to send perfectly well.
     */
    public function test_the_publish_gate_sees_the_resolved_values(): void
    {
        $campaign = $this->campaign()->fresh();

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

        $this->assertNotContains('target country not set', $problems);
        $this->assertNotContains('landing page not set', $problems);
    }
}
