<?php

namespace Tests\Feature;

use App\Agent\ToolRegistry;
use App\Agent\Tools\Campaign\ChooseObjective;
use App\Agent\Workspace;
use App\Campaigns\Platforms\PlatformRules;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * One intent, asked once, spelled differently per platform.
 *
 * The same answer is OUTCOME_TRAFFIC on Meta, WEBSITE_TRAFFIC on Google and
 * WEBSITE_VISITS on LinkedIn, and a three-platform brief asked for it three
 * times and made the buyer translate their own intent into each platform's
 * enum. Seen on arb-dev on 25 Sep: three objectives set across three turns, all
 * of them traffic.
 *
 * Every platform already had objectiveFor() to turn an outcome into its own
 * word, and briefs carried an outcome column. Nothing wrote the column, so the
 * mapping had nothing to map and the question came back each time.
 */
class TheObjectiveIsAskedOnceForTheBriefTest extends TestCase
{
    use RefreshDatabase;

    protected Brief $brief;

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

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

        app(Workspace::class)->bindTo('conv-shared-objective');

        $this->brief = Brief::create([
            'conversation_id' => 'conv-shared-objective',
            'user_id' => auth()->id(),
        ]);
    }

    private function campaign(string $platform): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'brief_id' => $this->brief->id,
            'name' => ucfirst($platform).' campaign',
            'platform' => $platform,
            'status' => 'draft',
        ]);

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

        return $campaign;
    }

    private function choose(array $arguments = []): array
    {
        $result = collect(app(ToolRegistry::class)->resolve())
            ->first(fn ($t) => $t->name() === 'campaign__choose_objective')
            ->handle(new Request($arguments));

        return json_decode((string) $result, true);
    }

    /** Picking on one platform records the intent, not the platform's enum. */
    public function test_choosing_an_objective_records_the_outcome_on_the_brief(): void
    {
        $this->campaign('meta');

        $this->choose(['objective' => 'OUTCOME_TRAFFIC']);

        $this->assertSame('TRAFFIC', $this->brief->refresh()->outcome);
    }

    /** The defect: the second platform asked the same question again. */
    public function test_the_next_platform_is_not_asked_again(): void
    {
        $this->campaign('meta');
        $this->choose(['objective' => 'OUTCOME_TRAFFIC']);

        $google = $this->campaign('google');
        $result = $this->choose();

        $this->assertTrue($result['from_brief'] ?? false, 'Google was asked to choose all over again.');
        $this->assertNotNull($google->refresh()->objective);
    }

    /** And it gets its own word for it, not Meta's. */
    public function test_each_platform_gets_its_own_vocabulary(): void
    {
        $this->campaign('meta');
        $this->choose(['objective' => 'OUTCOME_TRAFFIC']);

        $google = $this->campaign('google');
        $this->choose();

        $linkedin = $this->campaign('linkedin');
        $this->choose();

        $this->assertSame('WEBSITE_TRAFFIC', $google->refresh()->objective);
        $this->assertSame('WEBSITE_VISITS', $linkedin->refresh()->objective);
    }

    /** The first platform still asks, because nothing has been decided yet. */
    public function test_the_first_platform_is_still_asked(): void
    {
        $this->campaign('meta');

        $result = $this->choose();

        $this->assertArrayNotHasKey('from_brief', $result);
        $this->assertNotEmpty($result['offered']);
    }

    /**
     * A platform that cannot serve the outcome asks rather than guessing.
     *
     * Handing it the nearest match is how a lead generation brief becomes a
     * traffic campaign with nobody saying so.
     */
    public function test_a_platform_that_cannot_serve_the_outcome_still_asks(): void
    {
        $this->brief->update(['outcome' => 'JOB_APPLY']);

        $this->campaign('meta');
        $result = $this->choose();

        $this->assertArrayNotHasKey('from_brief', $result);
        $this->assertNotEmpty($result['offered']);
    }

    /**
     * The brief keeps what was decided first.
     *
     * A later platform whose vocabulary differs must not redefine the campaign
     * that came before it.
     */
    public function test_a_later_choice_does_not_redefine_the_brief(): void
    {
        $this->campaign('meta');
        $this->choose(['objective' => 'OUTCOME_TRAFFIC']);

        $this->campaign('linkedin');
        $this->choose(['objective' => 'LEAD_GENERATION']);

        $this->assertSame('TRAFFIC', $this->brief->refresh()->outcome);
    }

    /** Overriding on one platform is still allowed, and still recorded there. */
    public function test_a_platform_can_still_be_given_its_own_objective(): void
    {
        $this->campaign('meta');
        $this->choose(['objective' => 'OUTCOME_TRAFFIC']);

        $linkedin = $this->campaign('linkedin');
        $this->choose(['objective' => 'LEAD_GENERATION']);

        $this->assertSame('LEAD_GENERATION', $linkedin->refresh()->objective);
    }

    /**
     * A mapping that points outside the platform's own list is not trusted.
     *
     * objectives() and objectiveFor() are maintained separately on each rules
     * class, so they can drift: a map updated for a new objective that the
     * supported list has not gained yet would otherwise be written straight
     * onto the campaign and refused much later by the platform.
     *
     * Mocked because no real rules class disagrees with itself today, which is
     * exactly why this would go unnoticed if one started to.
     */
    public function test_an_outcome_mapping_outside_the_supported_list_is_ignored(): void
    {
        $this->brief->update(['outcome' => 'TRAFFIC']);

        $rules = $this->createMock(PlatformRules::class);
        $rules->method('objectiveFor')->willReturn('AN_OBJECTIVE_IT_DOES_NOT_LIST');

        // The registry hands back a NamespacedTool wrapper, so the tool itself
        // is resolved rather than unwrapped.
        $tool = app(ChooseObjective::class);

        $method = new \ReflectionMethod($tool, 'sharedObjective');
        $method->setAccessible(true);

        $this->assertNull($method->invoke($tool, ['WEBSITE_TRAFFIC'], $rules));
    }

    /** An objective the platform does not sell is still refused. */
    public function test_an_unsupported_objective_is_still_refused(): void
    {
        $this->campaign('google');

        $result = $this->choose(['objective' => 'OUTCOME_TRAFFIC']);

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