<?php

namespace Tests\Feature;

use App\Agent\ToolRegistry;
use App\Agent\Workspace;
use App\Models\Brief;
use App\Models\Campaign;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Laravel\Ai\Tools\Request;
use Tests\TestCase;

/**
 * Two ways the product made the buyer do its work for it.
 *
 * "start oct 1" came back as "Which year, start time, and time zone should I
 * use for October 1?", which is three questions for a date that had one
 * sensible reading. And a publish blocked on an account permission offered
 * "Retry publishing" underneath it, so the button was pressed, it failed again,
 * and the model had to talk the buyer out of the product's own suggestion.
 *
 * Both were seen on arb-dev on 25 Sep in the same conversation.
 */
class ADateAndADeadEndButtonTest extends TestCase
{
    use RefreshDatabase;

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

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

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

        Brief::create(['conversation_id' => 'conv-dates', 'user_id' => auth()->id()]);
    }

    private function campaign(): Campaign
    {
        $campaign = Campaign::create([
            'user_id' => auth()->id(),
            'name' => 'Meta campaign',
            'platform' => 'meta',
            'status' => 'draft',
        ]);

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

        return $campaign;
    }

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

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

    // ------------------------------------------------------------------ dates

    /** The defect: a bare date was treated as three unanswered questions. */
    public function test_a_date_with_no_year_or_time_is_accepted(): void
    {
        Carbon::setTestNow('2026-09-25 10:00:00');

        $campaign = $this->campaign();

        $result = $this->schedule(['starts_at' => 'oct 1']);

        $this->assertTrue($result['ok'], $result['error'] ?? '');
        $this->assertSame('2026-10-01', $campaign->refresh()->starts_at->toDateString());
    }

    /** It starts at the beginning of the day, not whenever the turn happened. */
    public function test_a_bare_date_starts_at_the_beginning_of_the_day(): void
    {
        Carbon::setTestNow('2026-09-25 14:37:00');

        $campaign = $this->campaign();

        $this->schedule(['starts_at' => '1 October']);

        $this->assertSame('00:00:00', $campaign->refresh()->starts_at->toTimeString());
    }

    /**
     * A bare end date runs to the end of its day, not the start.
     *
     * "Until the 20th" means through the 20th. Midnight would stop the campaign
     * at the start of that day and quietly lose it.
     */
    public function test_a_bare_end_date_covers_the_whole_day(): void
    {
        Carbon::setTestNow('2026-09-25 14:37:00');

        $campaign = $this->campaign();

        $this->schedule(['ends_at' => '20 October']);

        $this->assertSame('2026-10-20 23:59:59', $campaign->refresh()->ends_at->toDateTimeString());
    }

    /**
     * A time that was given is kept rather than flattened to midnight.
     *
     * "oct 1 09:30" rather than "1 October 09:30": PHP's date parser rejects
     * the second outright, which is its own limitation and not something this
     * tool can read either. The unreadable case is covered by
     * ThreeStepsThatCouldNotBeFinishedTest.
     */
    public function test_a_time_that_was_given_is_kept(): void
    {
        Carbon::setTestNow('2026-09-25 14:37:00');

        $campaign = $this->campaign();

        $this->schedule(['starts_at' => 'oct 1 09:30']);

        $this->assertSame('09:30:00', $campaign->refresh()->starts_at->toTimeString());
    }

    /**
     * A day that has already gone means next year's.
     *
     * Carbon reads "oct 1" as this year's, which is in the past for most of the
     * year, and no platform accepts a campaign starting in the past.
     */
    public function test_a_date_that_has_already_passed_rolls_forward(): void
    {
        Carbon::setTestNow('2026-11-20 10:00:00');

        $campaign = $this->campaign();

        $this->schedule(['starts_at' => 'oct 1']);

        $this->assertSame('2027-10-01', $campaign->refresh()->starts_at->toDateString());
    }

    /**
     * "Today" means today, not this date next year.
     *
     * Raised on the PR. isPast() was true for anything earlier in the current
     * day, and Carbon reads "today" and "midnight" as 00:00, so the roll-forward
     * fired and "start today" booked the campaign for the same date in 2027.
     * None of my own tests used a same-day answer, which is why they all passed.
     */
    public function test_today_is_not_pushed_a_year_forward(): void
    {
        Carbon::setTestNow('2026-09-25 14:37:00');

        $campaign = $this->campaign();

        $this->schedule(['starts_at' => 'today']);

        $this->assertSame('2026-09-25', $campaign->refresh()->starts_at->toDateString());
    }

    /** The end date has the same hole, checked before endOfDay() is applied. */
    public function test_an_end_date_of_today_is_not_pushed_a_year_forward(): void
    {
        Carbon::setTestNow('2026-09-25 14:37:00');

        $campaign = $this->campaign();

        $this->schedule(['ends_at' => 'today']);

        $this->assertSame('2026-09-25 23:59:59', $campaign->refresh()->ends_at->toDateTimeString());
    }

    /**
     * A time earlier today is also today.
     *
     * "9am" at 14:37 parses to a moment five hours gone, which isPast() also
     * treated as last year's date.
     */
    public function test_a_time_already_gone_today_stays_today(): void
    {
        Carbon::setTestNow('2026-09-25 14:37:00');

        $campaign = $this->campaign();

        $this->schedule(['starts_at' => '9am']);

        $this->assertSame('2026-09-25 09:00:00', $campaign->refresh()->starts_at->toDateTimeString());
    }

    /**
     * A year the buyer actually typed is left alone.
     *
     * Correcting "1 October 2025" to 2026 would move a campaign a year from
     * where it was put, without saying so. The publish gate refuses a past
     * start with a reason instead.
     */
    public function test_a_year_that_was_given_is_never_rewritten(): void
    {
        Carbon::setTestNow('2026-11-20 10:00:00');

        $campaign = $this->campaign();

        $this->schedule(['starts_at' => '2025-10-01']);

        $this->assertSame('2025-10-01', $campaign->refresh()->starts_at->toDateString());
    }

    /** A full ISO timestamp is unaffected. */
    public function test_a_full_timestamp_is_unaffected(): void
    {
        Carbon::setTestNow('2026-09-25 10:00:00');

        $campaign = $this->campaign();

        $this->schedule(['starts_at' => '2026-10-01T09:30:00Z']);

        $this->assertSame('2026-10-01 09:30:00', $campaign->refresh()->starts_at->toDateTimeString());
    }

    /** The tool says not to ask, because the model was asking. */
    public function test_the_tool_tells_the_model_not_to_ask_for_the_missing_parts(): void
    {
        $description = (string) collect(app(ToolRegistry::class)->resolve())
            ->first(fn ($t) => $t->name() === 'campaign__set_schedule')
            ->description();

        $this->assertStringContainsString('Never ask which year, what time, or which time zone', $description);
    }

    // ------------------------------------------------------------ the button

    /**
     * Retry is not offered for a failure retry cannot clear.
     *
     * Runs chat.js's own quickActionsFor() under node rather than reading the
     * file for strings. The first version of this test asserted that
     * "needsRetryToHelp" appeared somewhere in the script, which stayed true
     * when the flag was deleted from the action, so it passed with the bug back.
     *
     * @return array<string, list<string>>
     */
    private function offeredActions(): array
    {
        $output = shell_exec(sprintf(
            'node %s %s 2>&1',
            escapeshellarg(base_path('tests/js/quick-actions.cjs')),
            escapeshellarg(public_path('assets/js/chat.js')),
        ));

        $decoded = json_decode((string) $output, true);

        if (! is_array($decoded) || isset($decoded['error'])) {
            // Failed, not skipped.
            //
            // markTestSkipped here made three guardrail tests disappear on any
            // machine without node while phpunit still reported "passed": the
            // runner's own summary said 14 tests, 11 passed, and the three that
            // check a dead-end button is not offered had never run. A test that
            // reports green without executing is worse than no test, because it
            // is counted.
            //
            // CI without node is a CI problem to fix, and this is how it gets
            // noticed.
            $this->fail(
                'The chat.js harness could not run, so the quick-action guards were not checked. '
                .'node must be on PATH. Output: '.trim((string) $output)
            );
        }

        return $decoded;
    }

    /** The defect: the button that could not work was recommended anyway. */
    public function test_retry_is_withheld_when_the_blocker_is_a_permission(): void
    {
        $offered = $this->offeredActions();

        foreach (['permission', 'allowList', 'admin'] as $case) {
            $this->assertNotContains('Retry publishing', $offered[$case],
                "a publish blocked by a {$case} problem still offers a retry that cannot clear it");
        }
    }

    /** And it is still offered when retrying genuinely might work. */
    public function test_retry_is_still_offered_for_a_failure_that_might_clear(): void
    {
        $this->assertContains('Retry publishing', $this->offeredActions()['transient']);
    }

    /** "How to fix this" always appears, because that one always can. */
    public function test_the_explanation_is_offered_whatever_the_blocker(): void
    {
        $offered = $this->offeredActions();

        foreach (['permission', 'allowList', 'admin', 'transient'] as $case) {
            $this->assertContains('How to fix this', $offered[$case], "no explanation offered for {$case}");
        }
    }
}
