<?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']);
    }

    /**
     * One question on screen, not two.
     *
     * "Create a campaign across multiple platforms" put the platform chooser up
     * and the model asked for the landing page URL in the same breath, so the
     * buyer was shown two questions with buttons under only one. They answered
     * the URL, the chooser went stale, and the platforms had to be asked again.
     */
    public function test_the_models_competing_question_is_dropped(): void
    {
        $reply = $this->questionFor(
            $this->controller(),
            'What is the URL of the page the ads should send people to?',
            step: ['ask' => 'Where should this run? Choose one or more platforms below.'],
        );

        $this->assertSame('Where should this run? Choose one or more platforms below.', $reply);
    }

    /** An instruction to pick competes just as much as a question mark does. */
    public function test_a_choose_instruction_is_dropped_too(): void
    {
        $reply = $this->questionFor(
            $this->controller(),
            'The budget is set to daily at the ad-set level. Choose an amount shown, or type any amount you prefer.',
            step: ['ask' => 'How should the budget work, and how much?'],
        );

        $this->assertStringStartsWith('The budget is set to daily at the ad-set level.', $reply);
        $this->assertStringNotContainsString('Choose an amount shown', $reply);
        $this->assertStringEndsWith('How should the budget work, and how much?', $reply);
    }

    /**
     * What the model explained is still kept.
     *
     * This is the whole reason the words are preserved: a buyer told only
     * "choose a daily amount" three turns running never learns why the last
     * three answers were refused.
     */
    public function test_an_explanation_survives_with_the_question_removed(): void
    {
        $reply = $this->questionFor(
            $this->controller(),
            'USD 30 was refused because the ceiling is USD 20. What would you like instead?',
            step: ['ask' => 'How should the budget work, and how much?'],
        );

        $this->assertStringContainsString('USD 30 was refused because the ceiling is USD 20.', $reply);
        $this->assertStringNotContainsString('What would you like instead?', $reply);
    }

    /**
     * The reply keeps the shape it was written in.
     *
     * The stripper split the reply into sentences and joined the survivors back
     * with single spaces, which flattened every markdown list that passed
     * through it: the three ad variations arrived as one run-on paragraph. It
     * happened on every turn with an outstanding step, whether or not a
     * question was actually dropped, so nothing about the output looked like it
     * had been edited.
     */
    public function test_a_markdown_list_is_not_flattened(): void
    {
        $written = "Three Meta variations are drafted:\n\n"
            ."- **Plan Mode:** Review before changes.\n"
            ."- **Model choice:** Choose your model.\n"
            ."- **Multi-agent:** Explore multi-agent.\n\n"
            .'You can edit, replace, or cut these to one. Which would you like?';

        $reply = $this->questionFor($this->controller(), $written, step: ['ask' => 'Approve publishing?']);

        $this->assertStringContainsString(
            "- **Plan Mode:** Review before changes.\n- **Model choice:** Choose your model.",
            $reply,
            'the list was flattened into a paragraph',
        );
        $this->assertStringNotContainsString('Which would you like?', $reply);
        $this->assertStringEndsWith('Approve publishing?', $reply);
    }

    /**
     * An imperative asking for input competes too.
     *
     * The first version of this caught "Which would you prefer?" and "Choose
     * one of these" and missed "Paste the URL of the page the ads should send
     * people to.", which is how the opening turn of a multi-platform campaign
     * actually asks: a full stop, no question mark. Two of Kapil's four
     * conversations on 28 Sep were abandoned at exactly that screen, where the
     * URL has to be typed and the platforms have to be clicked and a buyer can
     * only do one of them.
     */
    public function test_an_imperative_request_for_input_is_dropped(): void
    {
        $reply = $this->questionFor(
            $this->controller(),
            'What are you promoting? Paste the URL of the page the ads should send people to.',
            step: ['ask' => 'Where should this run? Choose one or more platforms below.'],
        );

        $this->assertSame('Where should this run? Choose one or more platforms below.', $reply);
    }

    /** A statement that merely begins with a verb is not a question. */
    public function test_an_ordinary_sentence_is_not_mistaken_for_a_request(): void
    {
        $reply = $this->questionFor(
            $this->controller(),
            'Typing a custom amount is allowed here. The ceiling is USD 20.',
            step: ['ask' => 'How much per day?'],
        );

        $this->assertStringContainsString('The ceiling is USD 20.', $reply);
        $this->assertStringContainsString('Typing a custom amount is allowed here.', $reply);
    }
}
