<?php

namespace Tests\Feature;

use App\Agent\Answers;
use App\Agent\Choices;
use App\Agent\MarketingAgent;
use App\Agent\Workspace;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

/**
 * Clicking an option records the answer, without the model in the way.
 *
 * Every option already carries the value the platform accepts, and it never
 * left the browser: clicking put a sentence in the composer and the model had
 * to turn that back into an argument. Where the sentence contained the value
 * that worked. Where it did not, the model sometimes called the tool with no
 * argument, which re-offered the same question, so the click looked dead.
 *
 * Reproduced in the browser on 16 Sep on two steps. "Bid for highest volume"
 * left bidding empty and re-asked; the identical second click set it. "Use the
 * standard UTM tagging" did the same. The steps that worked first time all send
 * a phrase with the value inside it, like "Use ad account act_1418821929380936".
 */
class ClickedAnswerTest extends TestCase
{
    use RefreshDatabase;

    private Campaign $campaign;

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

        config([
            'platforms.guardrails.allowed_ad_accounts' => ['act_777'],
            'platforms.guardrails.budget_ceiling_currency' => 'USD',
            'platforms.guardrails.max_daily_budget' => 100,
        ]);

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

        DB::table('agent_conversations')->insert([
            'id' => 'conv-clicked-001',
            'participant_type' => $user->getMorphClass(),
            'participant_id' => $user->getKey(),
            'title' => 'Test',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        $workspace = app(Workspace::class);
        $workspace->bindTo('conv-clicked-001');
        $workspace->ownedBy($user);

        $this->campaign = Campaign::create([
            'user_id' => $user->id,
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
            'objective' => 'OUTCOME_TRAFFIC',
            'currency' => 'USD',
            'ad_account_id' => 'act_777',
        ]);

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

    private function click(string $field, string|array $value): bool
    {
        return app(Answers::class)->apply($field, $value);
    }

    // --------------------------------------------- the two that were broken

    /** The click that silently did nothing until it was clicked twice. */
    public function test_choosing_a_bid_strategy_records_it(): void
    {
        $this->assertTrue($this->click('bid_strategy', 'LOWEST_COST_WITHOUT_CAP'));

        $this->assertSame('LOWEST_COST_WITHOUT_CAP', $this->campaign->fresh()->bid_strategy);
    }

    public function test_choosing_the_standard_tagging_records_it(): void
    {
        $this->assertTrue($this->click('utm', 'utm_source=facebook&utm_medium=paid_social'));

        $this->assertSame('utm_source=facebook&utm_medium=paid_social', $this->campaign->fresh()->utm);
    }

    // ------------------------------------------------------- answers of none

    /**
     * "No tracking" is an answer and has to survive as one.
     *
     * An empty string is how the tool tells "asked, and there is none" from
     * "never asked", so it cannot be dropped on the way through.
     */
    public function test_choosing_no_tracking_is_recorded_rather_than_ignored(): void
    {
        $this->assertTrue($this->click('utm', ''));

        $this->assertSame('', $this->campaign->fresh()->utm);
    }

    // ------------------------------------------------------- shapes

    /**
     * Countries first, because that is how the flow asks.
     *
     * Countries, placements and devices are offered as one group, and the
     * targeting tool will not save anything until it has a country: without one
     * it re-offers instead. Answering placements alone is not a state the
     * product can reach.
     */
    public function test_a_multi_select_arrives_as_a_list(): void
    {
        $this->click('countries', ['US']);

        $this->assertTrue($this->click('placements', ['facebook', 'instagram']));

        $this->assertSame(['facebook', 'instagram'], $this->campaign->fresh()->placements);
    }

    /** Devices are offered as one option carrying both, so they arrive comma joined. */
    public function test_all_devices_arrives_as_both(): void
    {
        $this->click('countries', ['US']);
        $this->assertTrue($this->click('devices', 'mobile,desktop'));

        $this->assertSame(['mobile', 'desktop'], $this->campaign->fresh()->audience['devices']);
    }

    /** Level and mode are one click, crossed into a single option. */
    public function test_the_budget_strategy_option_sets_both_level_and_mode(): void
    {
        $this->assertTrue($this->click('budget_strategy', 'campaign:lifetime'));

        $campaign = $this->campaign->fresh();

        $this->assertSame('campaign', $campaign->budget_level);
        $this->assertSame('lifetime', $campaign->budget_mode);
    }

    public function test_an_amount_arrives_as_a_number(): void
    {
        $this->assertTrue($this->click('budget_amount', '20'));

        $this->assertSame('20.00', $this->campaign->fresh()->budget);
    }

    // --------------------------------------------------------- falling back

    /**
     * Anything unrecognised leaves the turn exactly as it was.
     *
     * The message still reaches the model, which can still do the work, so a
     * question this does not know about degrades to the old behaviour rather
     * than failing.
     */
    public function test_an_unknown_question_is_left_to_the_model(): void
    {
        $this->assertFalse($this->click('something_we_never_ask', 'whatever'));
    }

    /** A tool that refuses is not reported as having answered. */
    public function test_a_refused_answer_is_not_reported_as_applied(): void
    {
        Http::fake(['*' => Http::response(['error' => ['message' => 'nope']], 400)]);

        $this->assertFalse($this->click('ad_account_id', 'act_999'));
    }

    // --------------------------------------------------- through the request

    /**
     * The whole path, as the browser walks it.
     *
     * The value is posted with the message and applied before the turn, so a
     * model that says nothing useful cannot lose the answer. Everything above
     * exercises the applier; this is the wiring that carries it there.
     */
    public function test_a_posted_answer_is_applied_even_when_the_model_does_nothing(): void
    {
        MarketingAgent::fake(['Right.']);

        $this->post(route('chat.send'), [
            'conversation' => 'conv-clicked-001',
            'message' => 'Bid for highest volume',
            'answers' => [
                ['field' => 'bid_strategy', 'value' => 'LOWEST_COST_WITHOUT_CAP'],
            ],
        ])->assertOk();

        $this->assertSame('LOWEST_COST_WITHOUT_CAP', $this->campaign->fresh()->bid_strategy);
    }

    /** Two questions answered together arrive together. */
    public function test_several_answers_post_at_once(): void
    {
        MarketingAgent::fake(['Right.']);

        $this->post(route('chat.send'), [
            'conversation' => 'conv-clicked-001',
            'message' => 'Run on facebook. Target both mobile and desktop',
            'answers' => [
                ['field' => 'countries', 'value' => ['US']],
                ['field' => 'placements', 'value' => ['facebook']],
                ['field' => 'devices', 'value' => 'mobile,desktop'],
            ],
        ])->assertOk();

        $campaign = $this->campaign->fresh();

        // Asserted before reading into them, so a regression reads as "targeting
        // was never applied" rather than as an array offset on null.
        $this->assertNotNull($campaign->locations, 'targeting was not applied');
        $this->assertNotNull($campaign->audience, 'devices were not applied');

        $this->assertSame(['US'], $campaign->locations['countries']);
        $this->assertSame(['facebook'], $campaign->placements);
        $this->assertSame(['mobile', 'desktop'], $campaign->audience['devices']);
    }

    // ------------------------------------------------------- the whole set

    /**
     * Every question this product puts on screen can be answered without the
     * model reading it back out of prose.
     *
     * Walked over the offers rather than a list written here, so a new question
     * cannot quietly reintroduce the guesswork.
     */
    public function test_every_offered_question_can_be_applied(): void
    {
        $answers = app(Answers::class);
        $choices = app(Choices::class);

        $choices->offer('bid_strategy', 'q', [['value' => 'x', 'label' => 'x']]);

        foreach ($choices->all() as $offer) {
            $this->assertTrue(
                $answers->handles($offer['field']),
                "question [{$offer['field']}] cannot be applied without the model",
            );
        }

        // The full set the flow offers today, so a missing one is a failure
        // here rather than a click that does nothing in the browser.
        foreach ([
            'platform', 'ad_account_id', 'objective', 'special_ad_categories',
            'page_id', 'pixel_id', 'budget_amount', 'budget_strategy', 'bid_strategy',
            'ends_at', 'countries', 'placements', 'devices', 'instagram_account_id',
            'utm', 'creative_ids',
        ] as $field) {
            $this->assertTrue($answers->handles($field), "question [{$field}] is not handled");
        }
    }
}
