<?php

namespace Tests\Feature;

use App\Agent\Answers;
use App\Agent\Choices;
use App\Agent\MarketingAgent;
use App\Agent\ToolRegistry;
use App\Agent\Tools\Campaign\ChooseAdCopy;
use App\Agent\Workspace;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Choosing the wording instead of being handed it.
 *
 * The only way to change one headline was to ask for the whole set again, which
 * replaced every variation including the ones already approved. These pin the
 * way out of that: alternatives offered as a question, the click applied without
 * a model turn, the buyer's own wording accepted, and a request for different
 * options kept separate from a choice.
 */
class ChooseAdCopyTest extends TestCase
{
    use RefreshDatabase;

    protected User $user;

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

        MarketingAgent::fake(['Noted.']);

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

        $workspace = app(Workspace::class);
        $workspace->bindTo('conv-copy-0001');
        $workspace->ownedBy($this->user);
    }

    private function campaign(string $platform = 'meta', int $ads = 3): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => $this->user->id,
            'name' => 'Test',
            'platform' => $platform,
            'status' => 'draft',
        ]);

        for ($i = 1; $i <= $ads; $i++) {
            $campaign->ads()->create([
                'headline' => "Headline {$i}",
                'primary_text' => "Body {$i}.",
                'cta' => 'LEARN_MORE',
            ]);
        }

        app(Workspace::class)->focusOn($campaign->fresh());

        return $campaign->fresh();
    }

    private function runTool(array $arguments = []): array
    {
        $tool = collect(app(ToolRegistry::class)->resolve())
            ->first(fn ($t) => $t->name() === 'campaign__choose_ad_copy');

        $this->assertNotNull($tool, 'choose_ad_copy is not registered.');

        return json_decode((string) $tool->handle(new Request($arguments)), true);
    }

    private function offered(string $field): ?array
    {
        return collect(app(Choices::class)->all())->firstWhere('field', $field);
    }

    private function headlines(Campaign $campaign): array
    {
        return $campaign->ads()->orderBy('id')->pluck('headline')->all();
    }

    // ------------------------------------------------------------- offering

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

        $result = $this->runTool(['ad' => 1, 'headlines' => ['Shorter', 'Punchier', 'Plainer']]);

        $this->assertTrue($result['ok']);

        $offered = $this->offered('ad_headline');
        $this->assertNotNull($offered);
        $this->assertStringContainsString('variation 1', $offered['question']);
        $this->assertContains('Shorter', array_column($offered['options'], 'label'));

        // Nothing moved just by asking.
        $this->assertSame(['Headline 1', 'Headline 2', 'Headline 3'], $this->headlines($campaign));
    }

    /** Typing your own line is the edit path, so the question has to allow it. */
    public function test_the_buyers_own_wording_is_allowed(): void
    {
        $this->campaign();

        $this->runTool(['ad' => 1, 'headlines' => ['Shorter']]);

        $this->assertTrue($this->offered('ad_headline')['allow_other']);
    }

    /** The platform's own word for the body, so the question reads like the platform. */
    public function test_the_body_question_uses_the_platforms_word(): void
    {
        $this->campaign('google');

        $this->runTool(['ad' => 1, 'primary_texts' => ['One', 'Two']]);

        $this->assertStringContainsString('description', $this->offered('ad_primary_text')['question']);
    }

    public function test_both_questions_can_be_offered_at_once(): void
    {
        $this->campaign();

        $this->runTool(['ad' => 1, 'headlines' => ['A', 'B'], 'primary_texts' => ['C', 'D']]);

        $this->assertNotNull($this->offered('ad_headline'));
        $this->assertNotNull($this->offered('ad_primary_text'));
    }

    public function test_offering_nothing_is_refused_rather_than_shown_empty(): void
    {
        $this->campaign();

        $result = $this->runTool(['ad' => 1]);

        $this->assertFalse($result['ok']);
        $this->assertNull($this->offered('ad_headline'));
    }

    // -------------------------------------------------------------- applying

    /**
     * The click lands on the variation it was offered for.
     *
     * This is the whole reason the index travels in the value: two questions can
     * be on screen for different variations at once, and the apply path is given
     * nothing but the field name and the value.
     */
    public function test_the_chosen_line_lands_on_that_variation_only(): void
    {
        $campaign = $this->campaign();

        app(Answers::class)->applyAll([['field' => 'ad_headline', 'value' => '2::Punchier']]);

        $this->assertSame(['Headline 1', 'Punchier', 'Headline 3'], $this->headlines($campaign));
    }

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

        app(Answers::class)->applyAll([['field' => 'ad_primary_text', 'value' => '3::New body.']]);

        $this->assertSame('New body.', $campaign->ads()->orderBy('id')->get()[2]->primary_text);
    }

    /** Typed wording arrives with no index and belongs to the first variation. */
    public function test_wording_typed_by_the_buyer_is_accepted(): void
    {
        $campaign = $this->campaign();

        app(Answers::class)->applyAll([['field' => 'ad_headline', 'value' => 'My own line']]);

        $this->assertSame('My own line', $this->headlines($campaign)[0]);
    }

    /** A headline that happens to start with digits is not an index. */
    public function test_a_typed_line_is_not_mistaken_for_an_index(): void
    {
        $campaign = $this->campaign();

        app(Answers::class)->applyAll([['field' => 'ad_headline', 'value' => '50% off everything']]);

        $this->assertSame('50% off everything', $this->headlines($campaign)[0]);
    }

    // ----------------------------------------------------------- regenerate

    /**
     * Asking for different options is not a value to store.
     *
     * Refused on purpose, so the sentence reaches the model and it writes a
     * fresh set. Storing it would put the literal marker in the ad.
     */
    public function test_regenerate_is_refused_rather_than_stored(): void
    {
        $campaign = $this->campaign();

        $result = $this->runTool(['headline' => '1::'.ChooseAdCopy::REGENERATE]);

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('different options', $result['error']);
        $this->assertSame('Headline 1', $this->headlines($campaign)[0]);
    }

    /** And it is offered, because asking in prose is what replaced whole sets. */
    public function test_regenerate_is_offered_beside_the_alternatives(): void
    {
        $this->campaign();

        $this->runTool(['ad' => 1, 'headlines' => ['A', 'B']]);

        $values = array_column($this->offered('ad_headline')['options'], 'value');

        $this->assertContains('1::'.ChooseAdCopy::REGENERATE, $values);
    }

    // ---------------------------------------------------------------- guards

    public function test_a_variation_that_does_not_exist_is_refused(): void
    {
        $this->campaign(ads: 2);

        $result = $this->runTool(['ad' => 9, 'headlines' => ['A']]);

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('no variation 9', $result['error']);
    }

    public function test_it_asks_for_copy_first_when_there_is_none(): void
    {
        $this->campaign(ads: 0);

        $result = $this->runTool(['ad' => 1, 'headlines' => ['A']]);

        $this->assertFalse($result['ok']);
        $this->assertStringContainsString('write_ad_copy', $result['error']);
    }

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

        $this->runTool(['headline' => '1::   ']);

        $this->assertSame('Headline 1', $this->headlines($campaign)[0]);
    }

    /** What is there now is marked, so the buyer can see what they would change. */
    public function test_the_current_line_is_marked(): void
    {
        $this->campaign();

        $this->runTool(['ad' => 1, 'headlines' => ['Headline 1', 'Something else']]);

        $current = collect($this->offered('ad_headline')['options'])->firstWhere('label', 'Headline 1');

        $this->assertSame('Current', $current['badge'] ?? null);
    }
}
