<?php

namespace Tests\Tenant;

use App\Events\ScheduledTaskFinished;
use App\Jobs\RunScheduledTaskJob;
use App\ScheduledTasks\ScheduledTaskRunner;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Mtc\MercuryDataModels\ScheduledTask;
use Mtc\MercuryDataModels\ScheduledTaskAction;
use Mtc\MercuryDataModels\ScheduledTaskRun;
use Tests\Support\FakeTaskAction;
use Tests\TenantTestCase;

class ScheduledTaskEngineTest extends TenantTestCase
{
    #[\Override]
    protected function setUp(): void
    {
        parent::setUp();
        config()->set('scheduled_tasks.actions.fake', FakeTaskAction::class);
        FakeTaskAction::$executed = [];
    }

    #[\Override]
    protected function tearDown(): void
    {
        Carbon::setTestNow();
        parent::tearDown();
    }

    public function testMaterialiseCreatesTheDayQueueAndIsIdempotent(): void
    {
        Carbon::setTestNow('2026-07-08 00:05:00');

        $task = ScheduledTask::factory()->create(['schedule_rules' => [['frequency' => 'daily', 'time' => '08:00'], ['frequency' => 'daily', 'time' => '17:30']]]);
        ScheduledTask::factory()->create(['enabled' => false]);

        Artisan::call('scheduled-tasks:materialise');
        $this->assertEquals(2, ScheduledTaskRun::query()->count());

        // Re-running must not duplicate the queue
        Artisan::call('scheduled-tasks:materialise');
        $this->assertEquals(2, ScheduledTaskRun::query()->count());

        $this->assertEquals(
            '2026-07-08 08:00:00',
            $task->fresh()->next_due_at->format('Y-m-d H:i:s')
        );
    }

    public function testDispatchPicksOnlyDuePendingRuns(): void
    {
        Queue::fake();
        Carbon::setTestNow('2026-07-08 08:00:30');

        $task = ScheduledTask::factory()->create();
        $due = ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::parse('2026-07-08 08:00:00'),
        ]);
        ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::parse('2026-07-08 17:30:00'), // future
        ]);
        ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::parse('2026-07-07 08:00:00'),
            'status' => ScheduledTaskRun::STATUS_COMPLETED, // finished
        ]);

        Artisan::call('scheduled-tasks:dispatch');

        Queue::assertPushed(RunScheduledTaskJob::class, 1);
        Queue::assertPushed(RunScheduledTaskJob::class, fn ($job) => $job->run->id === $due->id);
    }

    public function testOverlapSkipPolicyMarksDueRunSkippedWhileSiblingRuns(): void
    {
        Queue::fake();
        Carbon::setTestNow('2026-07-08 09:00:00');

        $task = ScheduledTask::factory()->create(['overlap_policy' => ScheduledTask::OVERLAP_SKIP]);
        ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::parse('2026-07-08 08:00:00'),
            'status' => ScheduledTaskRun::STATUS_RUNNING,
        ]);
        $due = ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::parse('2026-07-08 08:30:00'),
        ]);

        Artisan::call('scheduled-tasks:dispatch');

        Queue::assertNothingPushed();
        $this->assertEquals(ScheduledTaskRun::STATUS_SKIPPED, $due->fresh()->status);
        $this->assertStringContainsString('still running', $due->fresh()->error);
    }

    public function testOverlapQueuePolicyLeavesDueRunPending(): void
    {
        Queue::fake();
        Carbon::setTestNow('2026-07-08 09:00:00');

        $task = ScheduledTask::factory()->create(['overlap_policy' => ScheduledTask::OVERLAP_QUEUE]);
        ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::parse('2026-07-08 08:00:00'),
            'status' => ScheduledTaskRun::STATUS_RUNNING,
        ]);
        $due = ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::parse('2026-07-08 08:30:00'),
        ]);

        Artisan::call('scheduled-tasks:dispatch');

        Queue::assertNothingPushed();
        $this->assertEquals(ScheduledTaskRun::STATUS_PENDING, $due->fresh()->status);
    }

    public function testRunnerExecutesActionsInOrderAndRecordsResults(): void
    {
        Event::fake([ScheduledTaskFinished::class]);

        $task = ScheduledTask::factory()->create();
        ScheduledTaskAction::factory()->create([
            'scheduled_task_id' => $task->id,
            'order' => 2,
            'action_type' => 'fake',
            'config' => ['marker' => 'second'],
        ]);
        ScheduledTaskAction::factory()->create([
            'scheduled_task_id' => $task->id,
            'order' => 1,
            'action_type' => 'fake',
            'config' => ['marker' => 'first'],
        ]);
        $run = ScheduledTaskRun::factory()->create(['scheduled_task_id' => $task->id]);

        app(ScheduledTaskRunner::class)->execute($run);

        $run->refresh();
        $this->assertEquals(ScheduledTaskRun::STATUS_COMPLETED, $run->status);
        $this->assertEquals(['first', 'second'], FakeTaskAction::$executed);
        $this->assertCount(2, $run->action_results);
        $this->assertEquals('completed', $run->action_results[0]['status']);
        $this->assertNotNull($run->task->fresh()->last_run_at);
        Event::assertDispatched(
            ScheduledTaskFinished::class,
            fn (ScheduledTaskFinished $event) => $event->succeeded()
        );
    }

    public function testRunWhenBranchingOnFailure(): void
    {
        Event::fake([ScheduledTaskFinished::class]);

        $task = ScheduledTask::factory()->create();
        ScheduledTaskAction::factory()->create([
            'scheduled_task_id' => $task->id,
            'order' => 1,
            'action_type' => 'fake',
            'config' => ['marker' => 'breaks', 'fail' => true],
        ]);
        ScheduledTaskAction::factory()->create([
            'scheduled_task_id' => $task->id,
            'order' => 2,
            'action_type' => 'fake',
            'config' => ['marker' => 'normal-step'],
        ]);
        ScheduledTaskAction::factory()->create([
            'scheduled_task_id' => $task->id,
            'order' => 3,
            'action_type' => 'fake',
            'config' => ['marker' => 'failure-branch'],
            'run_when' => ScheduledTaskAction::RUN_WHEN_ON_FAILURE,
        ]);
        $run = ScheduledTaskRun::factory()->create(['scheduled_task_id' => $task->id]);

        app(ScheduledTaskRunner::class)->execute($run);

        $run->refresh();
        // on_success step skipped, on_failure step executed, run failed overall
        $this->assertEquals(['breaks', 'failure-branch'], FakeTaskAction::$executed);
        $this->assertEquals(ScheduledTaskRun::STATUS_FAILED, $run->status);
        $this->assertEquals('skipped', $run->action_results[1]['status']);
        $this->assertEquals('completed', $run->action_results[2]['status']);
        $this->assertEquals('Deliberate failure', $run->error);
        Event::assertDispatched(
            ScheduledTaskFinished::class,
            fn (ScheduledTaskFinished $event) => !$event->succeeded()
        );
    }

    public function testClaimRefusesWhileSiblingIsRunningAndAppliesPolicy(): void
    {
        $task = ScheduledTask::factory()->create(['overlap_policy' => ScheduledTask::OVERLAP_SKIP]);
        ScheduledTaskAction::factory()->create(['scheduled_task_id' => $task->id, 'action_type' => 'fake']);
        ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::now()->subMinutes(10),
            'status' => ScheduledTaskRun::STATUS_RUNNING,
        ]);
        $run = ScheduledTaskRun::factory()->create(['scheduled_task_id' => $task->id]);

        app(ScheduledTaskRunner::class)->execute($run);

        $this->assertEquals(ScheduledTaskRun::STATUS_SKIPPED, $run->fresh()->status);
        $this->assertEmpty(FakeTaskAction::$executed);
    }

    public function testAlreadyClaimedRunIsNotExecutedTwice(): void
    {
        $task = ScheduledTask::factory()->create();
        ScheduledTaskAction::factory()->create(['scheduled_task_id' => $task->id, 'action_type' => 'fake']);
        $run = ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'status' => ScheduledTaskRun::STATUS_COMPLETED,
        ]);

        app(ScheduledTaskRunner::class)->execute($run);

        $this->assertEmpty(FakeTaskAction::$executed);
        $this->assertEquals(ScheduledTaskRun::STATUS_COMPLETED, $run->fresh()->status);
    }

    public function testUnknownActionTypeFailsTheRunSafely(): void
    {
        $task = ScheduledTask::factory()->create();
        ScheduledTaskAction::factory()->create([
            'scheduled_task_id' => $task->id,
            'action_type' => 'not-registered',
        ]);
        $run = ScheduledTaskRun::factory()->create(['scheduled_task_id' => $task->id]);

        app(ScheduledTaskRunner::class)->execute($run);

        $run->refresh();
        $this->assertEquals(ScheduledTaskRun::STATUS_FAILED, $run->status);
        $this->assertStringContainsString('Unknown action type', $run->error);
    }

    public function testPruneDeletesOnlyOldFinishedRuns(): void
    {
        Carbon::setTestNow('2026-07-08 01:40:00');

        $task = ScheduledTask::factory()->create();
        $old = ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'status' => ScheduledTaskRun::STATUS_COMPLETED,
            'scheduled_for' => Carbon::now()->subDays(15),
            'finished_at' => Carbon::now()->subDays(15),
        ]);
        $recent = ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'status' => ScheduledTaskRun::STATUS_FAILED,
            'scheduled_for' => Carbon::now()->subDays(3),
            'finished_at' => Carbon::now()->subDays(3),
        ]);
        $pending = ScheduledTaskRun::factory()->create([
            'scheduled_task_id' => $task->id,
            'scheduled_for' => Carbon::now()->subDays(20),
        ]);

        Artisan::call('scheduled-tasks:prune');

        $this->assertDatabaseMissing('scheduled_task_runs', ['id' => $old->id]);
        $this->assertDatabaseHas('scheduled_task_runs', ['id' => $recent->id]);
        $this->assertDatabaseHas('scheduled_task_runs', ['id' => $pending->id]);
    }
}
