<?php

namespace Tests\Feature;

use App\Models\CronLog;
use App\Support\CronLogger;
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Support\Facades\Config;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Output\NullOutput;
use Tests\DatabaseTestCase;

class CronLoggingTest extends DatabaseTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        // Logging is disabled during tests by default (phpunit.xml); turn it on here.
        Config::set('cron_logs.enabled', true);
    }

    public function testTopLevelCommandIsLoggedAsSystemRow(): void
    {
        event(new CommandStarting('clear:audit', new ArrayInput([]), new NullOutput()));

        $row = CronLog::query()->sole();
        $this->assertSame('clear:audit', $row->command);
        $this->assertNull($row->tenant_id);
        $this->assertSame(CronLog::STATUS_RUNNING, $row->status);
        $this->assertNotNull($row->started_at);

        event(new CommandFinished('clear:audit', new ArrayInput([]), new NullOutput(), 0));

        $row->refresh();
        $this->assertSame(CronLog::STATUS_COMPLETED, $row->status);
        $this->assertSame(0, $row->exit_code);
        $this->assertNotNull($row->finished_at);
        $this->assertNotNull($row->duration_ms);
    }

    public function testNonZeroExitCodeIsRecordedAsFailed(): void
    {
        event(new CommandStarting('clear:audit', new ArrayInput([]), new NullOutput()));
        event(new CommandFinished('clear:audit', new ArrayInput([]), new NullOutput(), 1));

        $row = CronLog::query()->sole();
        $this->assertSame(CronLog::STATUS_FAILED, $row->status);
        $this->assertSame(1, $row->exit_code);
    }

    public function testTenantsRunProducesWrapperAndPerTenantChildRows(): void
    {
        // Mirror tenants:run: Symfony binds the input to the command definition
        // before CommandStarting fires, so getArguments() exposes "commandname".
        $definition = new InputDefinition([new InputArgument('commandname', InputArgument::REQUIRED)]);
        $input = new ArrayInput(['commandname' => 'reservations:ingest'], $definition);

        event(new CommandStarting('tenants:run', $input, new NullOutput()));

        $wrapper = CronLog::query()->sole();
        $this->assertSame('tenants:run reservations:ingest', $wrapper->command);
        $this->assertNull($wrapper->tenant_id);

        // Simulate one tenant slice of the fan-out.
        event(new TenancyInitialized(tenancy()));
        event(new TenancyEnded(tenancy()));

        $child = CronLog::query()->whereNotNull('tenant_id')->sole();
        $this->assertSame('reservations:ingest', $child->command);
        $this->assertSame(tenancy()->tenant->getTenantKey(), $child->tenant_id);
        $this->assertSame($wrapper->id, $child->parent_id);
        $this->assertNotNull($child->duration_ms);

        event(new CommandFinished('tenants:run', $input, new NullOutput(), 0));

        $this->assertSame(2, CronLog::query()->count());
    }

    public function testNegativeElapsedTimeIsClampedToZero(): void
    {
        // Start a tenants:run wrapper so the logger is active and has a top-level id.
        $definition = new InputDefinition([new InputArgument('commandname', InputArgument::REQUIRED)]);
        $input = new ArrayInput(['commandname' => 'reservations:ingest'], $definition);
        event(new CommandStarting('tenants:run', $input, new NullOutput()));

        // Inject a tenant frame whose start is in the future, simulating the wall
        // clock stepping backwards mid-run (e.g. an NTP correction). Without the
        // clamp this produces a negative duration_ms and the unsigned column insert
        // fails with SQLSTATE[22003]. The logger is a singleton, so this mutates the
        // same instance the TenancyEnded listener resolves.
        $logger = app(CronLogger::class);
        $stack = new \ReflectionProperty($logger, 'tenantStack');
        $stack->setAccessible(true);
        $stack->setValue($logger, [[
            'tenant_id' => 'tenant-a',
            'start' => microtime(true) + 5.0,
            'started_at' => now(),
        ]]);

        event(new TenancyEnded(tenancy()));

        $child = CronLog::query()->whereNotNull('tenant_id')->sole();
        $this->assertSame('tenant-a', $child->tenant_id);
        $this->assertSame(0, $child->duration_ms);
    }

    public function testIgnoredCommandsAreNotLogged(): void
    {
        event(new CommandStarting('schedule:run', new ArrayInput([]), new NullOutput()));
        event(new CommandStarting('horizon:work', new ArrayInput([]), new NullOutput()));

        $this->assertSame(0, CronLog::query()->count());
    }

    public function testNothingIsLoggedWhenDisabled(): void
    {
        Config::set('cron_logs.enabled', false);

        event(new CommandStarting('clear:audit', new ArrayInput([]), new NullOutput()));

        $this->assertSame(0, CronLog::query()->count());
    }

    public function testPruneKeepsLatestNPerCommandAndTenant(): void
    {
        Config::set('cron_logs.enabled', false); // don't log the prune run itself

        $this->seedRows('reservations:ingest', null, 5);
        $this->seedRows('reservations:ingest', 'tenant-a', 4);
        $this->seedRows('clear:audit', null, 2);

        $this->artisan('cron-logs:prune', ['--keep' => 2])->assertExitCode(0);

        $this->assertSame(2, CronLog::query()->ofCommand('reservations:ingest')->system()->count());
        $this->assertSame(2, CronLog::query()->ofCommand('reservations:ingest')->forTenant('tenant-a')->count());
        $this->assertSame(2, CronLog::query()->ofCommand('clear:audit')->count());

        // The newest rows survive (highest ids).
        $survivingSystem = CronLog::query()->ofCommand('reservations:ingest')->system()->orderBy('id')->pluck('id');
        $this->assertCount(2, $survivingSystem);
    }

    private function seedRows(string $command, ?string $tenantId, int $count): void
    {
        for ($i = 0; $i < $count; $i++) {
            CronLog::create([
                'command' => $command,
                'tenant_id' => $tenantId,
                'status' => CronLog::STATUS_COMPLETED,
                'started_at' => now(),
                'finished_at' => now(),
                'duration_ms' => 10,
            ]);
        }
    }
}
