<?php

namespace Tests\Feature;

use App\Agent\Choices;
use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Campaigns\LaunchFlow;
use App\Campaigns\PendingChanges;
use App\Campaigns\Platforms\Platforms;
use App\Campaigns\TrackedUrl;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Tracking parameters on the destination URL.
 *
 * There were none. No column, no default, no way to set one, so every ad
 * published sent traffic to a bare landing page and nothing downstream could
 * tell the click came from Meta, from which campaign, or from which ad.
 *
 * The part worth covering carefully is the joining: a landing page usually
 * already carries parameters of its own, and a second question mark breaks the
 * whole query string quietly, on a live ad, with no error anywhere.
 */
class TrackingTest extends TestCase
{
    use RefreshDatabase;

    protected Campaign $campaign;

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

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

        $workspace = app(Workspace::class);
        $workspace->bindTo('conv-utm-0001');

        $this->campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
            'objective' => 'OUTCOME_TRAFFIC',
            'landing_url' => 'https://example.com/offer',
        ]);

        $workspace->focusOn($this->campaign);
    }

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

        $this->assertNotNull($tool, "Tool {$name} is not registered.");

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

    // ------------------------------------------------------------ the joining

    public function test_parameters_are_appended_to_a_plain_url(): void
    {
        $this->assertSame(
            'https://example.com/offer?utm_source=facebook',
            TrackedUrl::build('https://example.com/offer', 'utm_source=facebook'),
        );
    }

    /** The case that breaks a live ad silently. */
    public function test_a_url_that_already_has_a_query_string_keeps_it(): void
    {
        $this->assertSame(
            'https://example.com/offer?ref=partner&utm_source=facebook',
            TrackedUrl::build('https://example.com/offer?ref=partner', 'utm_source=facebook'),
        );
    }

    /** A fragment has to stay last or the browser drops the query after it. */
    public function test_a_fragment_stays_at_the_end(): void
    {
        $this->assertSame(
            'https://example.com/offer?utm_source=facebook#pricing',
            TrackedUrl::build('https://example.com/offer#pricing', 'utm_source=facebook'),
        );
    }

    public function test_a_stray_leading_separator_is_not_doubled(): void
    {
        $this->assertSame(
            'https://example.com/offer?utm_source=facebook',
            TrackedUrl::build('https://example.com/offer', '?utm_source=facebook'),
        );
    }

    public function test_no_tracking_leaves_the_url_alone(): void
    {
        $this->assertSame(
            'https://example.com/offer',
            TrackedUrl::build('https://example.com/offer', ''),
        );
    }

    // ------------------------------------------------------------- the choice

    public function test_the_tagging_is_offered_with_a_suggestion_and_a_way_to_decline(): void
    {
        $this->runTool('campaign__set_tracking');

        $offered = collect(app(Choices::class)->all())->firstWhere('field', 'utm');

        $this->assertNotNull($offered, 'The tagging should be offered.');

        $labels = array_column($offered['options'], 'label');

        $this->assertContains('Standard tagging', $labels);
        $this->assertContains('No tracking', $labels);
        $this->assertSame('Suggested', $offered['options'][0]['badge']);

        // Nothing is set by being shown the question.
        $this->assertNull($this->campaign->fresh()->utm);
    }

    /**
     * "No tracking" has to be an answer rather than a silence, or the flow
     * asks again every turn and the campaign can never finish.
     */
    /**
     * Turning tracking off is still an answer, and still sticks.
     *
     * The progress assertions went with the step. Tracking is no longer a
     * question: it is set from the platform's own macros when the campaign is
     * created, because every campaign should carry it and asking cost a turn to
     * arrive at the value that was going to be used anyway. What remains is that
     * somebody who says no gets no tracking, and that an empty string is that
     * answer rather than nobody having been asked.
     */
    public function test_declining_tracking_is_recorded_as_an_answer(): void
    {
        $result = $this->runTool('campaign__set_tracking', ['utm' => '']);

        $this->assertTrue($result['ok']);
        $this->assertSame('', $this->campaign->fresh()->utm);
    }

    /**
     * Tracking is not one of the questions any more.
     *
     * It was asked on every platform, and the answer was the standard tagging
     * essentially always. Shyam, 23 Sep: "no need to ask for utm params, they
     * should always be there, just inform the user and ask if any changes are
     * needed." StartCampaign sets it; set_tracking still changes it.
     */
    public function test_tracking_is_not_a_step_any_more(): void
    {
        $keys = array_column(app(LaunchFlow::class)->progress($this->campaign), 'key');

        $this->assertNotContains('utm', $keys, 'Tracking is being asked for again.');
    }

    /**
     * And not on any platform, not just the one this test happens to use.
     *
     * The step was taken out of Meta, Google and the unsupported default, and
     * LinkedIn arrived afterwards carrying one, so the question came back for
     * exactly the platform nobody was testing. Asked of every configured
     * platform so a fifth cannot reintroduce it quietly.
     */
    public function test_no_platform_asks_for_tracking(): void
    {
        $platforms = app(Platforms::class);

        foreach ($platforms->keys() as $key) {
            $keys = array_column($platforms->for($key)->steps(), 'key');

            $this->assertNotContains('utm', $keys, "{$key} still asks how clicks should be tagged.");
        }
    }

    /** Every platform can set it without being asked, which is why. */
    public function test_every_platform_offers_a_default_tagging(): void
    {
        $platforms = app(Platforms::class);

        foreach ($platforms->keys() as $key) {
            $options = $platforms->for($key)->trackingOptions();

            $this->assertNotEmpty($options, "{$key} has no tagging to set at creation.");
            $this->assertArrayHasKey('value', $options[0]);
        }
    }

    /** And a new campaign already carries it. */
    public function test_a_new_campaign_is_created_with_tracking_already_on(): void
    {
        $created = $this->runTool('campaign__start_campaign', ['platform' => 'meta']);

        $campaign = Campaign::findOrFail($created['campaign_id']);

        $this->assertNotEmpty($campaign->utm, 'A campaign was created with no click tracking.');
        $this->assertStringContainsString('utm_source=', (string) $campaign->utm);
    }

    public function test_something_that_is_not_a_query_string_is_refused(): void
    {
        $result = $this->runTool('campaign__set_tracking', ['utm' => 'facebook paid social']);

        $this->assertFalse($result['ok']);
        $this->assertNull($this->campaign->fresh()->utm);
    }

    public function test_metas_macros_are_accepted(): void
    {
        $result = $this->runTool('campaign__set_tracking', [
            'utm' => 'utm_source=facebook&utm_campaign={{campaign.name}}',
        ]);

        $this->assertTrue($result['ok']);
        $this->assertSame('utm_source=facebook&utm_campaign={{campaign.name}}', $this->campaign->fresh()->utm);
    }

    /** A summary showing {{campaign.name}} tells nobody whether it is right. */
    public function test_the_example_url_has_the_macros_filled_in(): void
    {
        $this->campaign->update(['utm' => 'utm_campaign={{campaign.name}}&utm_content={{ad.name}}']);

        $example = TrackedUrl::asExample($this->campaign->fresh());

        $this->assertStringContainsString('utm_campaign=Winter Sale', $example);
        $this->assertStringContainsString('utm_content=Winter Sale - ad 1', $example);
        $this->assertStringNotContainsString('{{', $example);
    }

    /**
     * The URL is part of the creative, and Meta treats a creative as immutable,
     * so this is a rebuild rather than an edit. Saying "updated" would be a lie
     * that only shows up in the reporting weeks later.
     */
    public function test_changing_the_tracking_after_publish_needs_a_new_ad(): void
    {
        $this->campaign->update([
            'utm' => 'utm_source=facebook',
            'external_campaign_id' => '120000000000001',
        ]);

        // The baseline has to be taken after the values are on the model, which
        // is what the publisher does once Meta has accepted them.
        $this->campaign->update([
            'published_state' => app(PendingChanges::class)->snapshot($this->campaign->fresh()),
        ]);

        $this->campaign->update(['utm' => 'utm_source=meta']);

        $problems = app(PendingChanges::class)->requiringRebuild($this->campaign->fresh());

        $this->assertNotEmpty(array_filter($problems, fn ($p) => str_contains($p, 'click tracking')));
    }
}
