<?php

namespace Tests\Feature;

use App\Agent\Workspace;
use App\Campaigns\LaunchFlow;
use App\Http\Controllers\ChatController;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * The sentence and the buttons describe the same step.
 *
 * Seen in the browser on 16 Sep: the three bid strategy options on screen,
 * under the line "Please write the primary text, headline, description, and
 * call to action for your ad". The controller guarantees the right options
 * appear when the model does not offer them, and nothing kept the prose in step
 * with that. The model only fails to offer when it has gone somewhere else, so
 * in exactly that case its words are about another step.
 */
class QuestionMatchesOptionsTest extends TestCase
{
    use RefreshDatabase;

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

        $this->actingAs(User::factory()->create());
        app(Workspace::class)->bindTo('conv-question-001');
    }

    private function controller(): ChatController
    {
        return app(ChatController::class);
    }

    /** @param array<string, mixed>|null $step */
    private function questionFor(ChatController $controller, string $reply, array $awaiting = [], ?array $step = null, bool $failed = false): string
    {
        $reflection = new \ReflectionClass($controller);

        $property = $reflection->getProperty('steppedIn');
        $property->setAccessible(true);
        $property->setValue($controller, $step);

        $method = $reflection->getMethod('questionFor');
        $method->setAccessible(true);

        return $method->invoke($controller, $reply, $awaiting, $failed);
    }

    /**
     * Every step has a sentence, so the override can never blank the reply.
     *
     * Walked over the whole table rather than the cases known today: a step
     * added without one would silently fall back to the drifting prose.
     */
    public function test_every_step_carries_a_sentence(): void
    {
        $steps = app(LaunchFlow::class)->steps(new Campaign(['platform' => 'meta']));

        foreach ($steps as $step) {
            $this->assertArrayHasKey('ask', $step, "step [{$step['key']}] has no sentence");
            $this->assertNotEmpty($step['ask'], "step [{$step['key']}] has an empty sentence");
        }
    }

    /**
     * The question that belongs to the buttons is the last thing said.
     *
     * This used to assert the model's words were discarded entirely, and that
     * cost something elsewhere: on arb-dev on 25 Sep a buyer was shown "Choose
     * a daily amount" three turns running while three replies explaining why
     * the budget kept being refused were written to the database and never
     * read. Throwing the words away is how the product repeats itself without
     * ever saying what is wrong.
     *
     * So they are kept, and the step's own question follows them. The prose
     * next to the options still describes the step the options belong to,
     * which is what this test is for; it is just no longer the only sentence
     * on screen.
     */
    public function test_the_step_the_controller_offered_supplies_the_sentence(): void
    {
        $bidding = collect(app(LaunchFlow::class)->steps(new Campaign(['platform' => 'meta'])))
            ->firstWhere('key', 'bidding');

        $reply = $this->questionFor(
            $this->controller(),
            'Please write the primary text, headline, description, and call to action for your ad.',
            step: $bidding,
        );

        $this->assertStringEndsWith($bidding['ask'], $reply);
    }

    /** And a model that already asked the right thing is not made to ask twice. */
    public function test_the_question_is_not_repeated_when_the_model_already_asked_it(): void
    {
        $bidding = collect(app(LaunchFlow::class)->steps(new Campaign(['platform' => 'meta'])))
            ->firstWhere('key', 'bidding');

        $reply = $this->questionFor($this->controller(), $bidding['ask'], step: $bidding);

        $this->assertSame($bidding['ask'], $reply);
    }

    /** The model led the way itself, so its words stand. */
    public function test_a_reply_is_left_alone_when_the_controller_did_not_step_in(): void
    {
        $reply = $this->questionFor($this->controller(), 'Please pick your bid strategy.');

        $this->assertSame('Please pick your bid strategy.', $reply);
    }

    /**
     * An approval is a button to press, not a question to answer.
     *
     * Replacing the reply there would talk over the thing the person is
     * actually being asked to decide.
     */
    public function test_a_pending_approval_keeps_its_reply(): void
    {
        $bidding = collect((new \ReflectionClass(LaunchFlow::class))->getConstant('STEPS'))
            ->firstWhere('key', 'bidding');

        $reply = $this->questionFor(
            $this->controller(),
            'Waiting on your approval below before anything is sent to the platform.',
            awaiting: [['id' => 'x', 'tool' => 'publish campaign', 'reason' => null]],
            step: $bidding,
        );

        $this->assertStringContainsString('approval', $reply);
    }

    /**
     * A turn that broke keeps its own account of what broke.
     *
     * The failure text is the only record the person gets of how far the turn
     * got before it died, and replacing it with a question would throw that
     * away. Caught by the existing suite when the first version of this fix
     * overwrote it.
     */
    public function test_a_failed_turn_keeps_its_failure_report(): void
    {
        $bidding = collect((new \ReflectionClass(LaunchFlow::class))->getConstant('STEPS'))
            ->firstWhere('key', 'bidding');

        $reply = $this->questionFor(
            $this->controller(),
            "That step failed: the AI provider is busy.\n\nSome of it was applied before the failure.",
            step: $bidding,
            failed: true,
        );

        $this->assertStringContainsString('applied before the failure', $reply);
        $this->assertStringNotContainsString('How should it bid', $reply);
    }

    /** End to end: a campaign sitting on the bidding step gets the bidding sentence. */
    public function test_the_flow_hands_over_the_sentence_for_the_outstanding_step(): void
    {
        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Winter Sale',
            'platform' => 'meta',
            'status' => 'draft',
            'ad_account_id' => 'act_777',
            'objective' => 'OUTCOME_TRAFFIC',
            'special_ad_categories' => ['NONE'],
            'landing_url' => 'https://example.com',
            'page_id' => 'page-1',
            'budget' => 20,
        ]);

        $next = app(LaunchFlow::class)->next($campaign);

        $this->assertSame('bidding', $next['key']);
        $this->assertSame('How should it bid?', $next['ask']);
    }
}
