<?php

declare(strict_types=1);

use Builder\Core\Foundation\Concerns\HasBuilderAccess;
use Builder\Core\Foundation\Contracts\BuilderAccessible;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Inertia\Testing\AssertableInertia;

final class HealthTestUser extends Authenticatable implements BuilderAccessible
{
    use HasBuilderAccess;

    protected $table = 'users';

    protected $guarded = [];

    public function canAccessBuilder(): bool
    {
        return true;
    }

    /** @return list<string> */
    public function builderPermissions(): array
    {
        return ['read:health'];
    }
}

final class HealthTestUserWithoutPermission extends Authenticatable implements BuilderAccessible
{
    use HasBuilderAccess;

    protected $table = 'users';

    protected $guarded = [];

    public function canAccessBuilder(): bool
    {
        return true;
    }

    /** @return list<string> */
    public function builderPermissions(): array
    {
        return ['read:admin'];
    }
}

beforeEach(function (): void {
    config()->set('auth.providers.users.model', HealthTestUser::class);

    Schema::create('users', function ($table): void {
        $table->id();
        $table->string('name')->nullable();
        $table->string('email')->nullable();
        $table->string('password')->nullable();
        $table->timestamps();
    });
});

it('redirects unauthenticated users to the login page', function (): void {
    $this->get(route('builder.health'))
        ->assertRedirect(route('builder.login'));
});

it('returns 403 for authenticated users without read:health permission', function (): void {
    $user = HealthTestUserWithoutPermission::create(['name' => 'User', 'email' => 'user@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertForbidden();
});

it('renders the health page for users with read:health permission', function (): void {
    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'admin@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->has('environment')
            ->has('php')
            ->has('server')
            ->has('database')
            ->has('cacheAndSession')
            ->has('queue')
            ->has('storage')
            ->has('modules'),
        );
});

it('includes environment data on the health page', function (): void {
    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'admin@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->has('environment.laravel_version')
            ->has('environment.builder_version')
            ->has('environment.app_env')
            ->has('environment.debug_mode')
            ->has('environment.app_timezone')
            ->has('environment.app_locale')
            ->has('environment.config_cached')
            ->has('environment.routes_cached')
            ->has('environment.events_cached'),
        );
});

it('includes php data on the health page', function (): void {
    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'admin@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->has('php.version')
            ->has('php.memory_limit')
            ->has('php.upload_max_size')
            ->has('php.post_max_size')
            ->has('php.max_execution')
            ->has('php.opcache_enabled')
            ->has('php.extensions'),
        );
});

it('includes pending job count when queue driver is database', function (): void {
    Schema::create('jobs', function ($table): void {
        $table->bigIncrements('id');
        $table->string('queue')->index();
        $table->longText('payload');
        $table->unsignedTinyInteger('attempts');
        $table->unsignedInteger('reserved_at')->nullable();
        $table->unsignedInteger('available_at');
        $table->unsignedInteger('created_at');
    });

    config()->set('queue.default', 'database');

    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'admin2@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->where('queue.pending', 0),
        );
});

it('includes failed jobs count when failed_jobs table exists', function (): void {
    Schema::create('failed_jobs', function ($table): void {
        $table->id();
        $table->string('uuid')->unique();
        $table->text('connection');
        $table->text('queue');
        $table->longText('payload');
        $table->longText('exception');
        $table->timestamp('failed_at')->useCurrent();
    });

    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'admin3@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->where('queue.failed', 0),
        );
});

it('resolveDbVersion returns null and catches exception for mysql driver with no connection', function (): void {
    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'mysql-version@example.com', 'password' => 'secret']);

    // Override the driver config for the default sqlite connection so the health
    // controller sees 'mysql' as the driver, then make DB::selectOne throw so the
    // catch block is exercised. The rest of the page still renders normally.
    $connection = config('database.default');
    config()->set("database.connections.{$connection}.driver", 'mysql');

    DB::shouldReceive('selectOne')
        ->andThrow(new RuntimeException('Connection refused'));
    DB::shouldReceive('table')->passthru();
    DB::shouldReceive('getSchemaBuilder')->passthru();

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->has('database'),
        );
});

it('resolveDbVersion returns null and catches exception for pgsql driver with no connection', function (): void {
    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'pgsql-version@example.com', 'password' => 'secret']);

    // Same pattern: fake the driver as 'pgsql' and make DB::selectOne throw.
    $connection = config('database.default');
    config()->set("database.connections.{$connection}.driver", 'pgsql');

    DB::shouldReceive('selectOne')
        ->andThrow(new RuntimeException('Connection refused'));
    DB::shouldReceive('table')->passthru();
    DB::shouldReceive('getSchemaBuilder')->passthru();

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->has('database'),
        );
});

it('resolveDirectorySize returns 0 for non-existent path', function (): void {
    $nonExistentPath = sys_get_temp_dir().'/builder_test_nonexistent_'.uniqid();
    app()->useStoragePath($nonExistentPath);

    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'dir-size@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->where('storage.app_size', 0),
        );
});

it('resolvePendingJobs catches exception and returns null when jobs table does not exist', function (): void {
    config()->set('queue.default', 'database');
    // No 'jobs' table created — DB::table('jobs')->count() will throw

    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'pending-catch@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->where('queue.pending', null),
        );
});

it('resolveFailedJobs catches exception and returns null when schema builder throws', function (): void {
    // Mock the schema builder to throw an exception
    DB::shouldReceive('getSchemaBuilder')
        ->andThrow(new RuntimeException('Schema error'));

    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'failed-catch@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->where('queue.failed', null),
        );
});

it('resolveDbName falls back to config when selectOne returns null name for mysql driver', function (): void {
    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'mysql-name@example.com', 'password' => 'secret']);

    $connection = config('database.default');
    config()->set("database.connections.{$connection}.driver", 'mysql');
    config()->set("database.connections.{$connection}.database", 'my_db_fallback');

    // DB::selectOne returns an object with name = null, triggering the ?? fallback (line 162)
    DB::shouldReceive('selectOne')
        ->andReturn((object) ['name' => null, 'version' => null, 'size' => null]);
    DB::shouldReceive('table')->passthru();
    DB::shouldReceive('getSchemaBuilder')->passthru();

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->where('database.name', 'my_db_fallback'),
        );
});

it('resolveDbName falls back to config when selectOne returns null name for pgsql driver', function (): void {
    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'pgsql-name@example.com', 'password' => 'secret']);

    $connection = config('database.default');
    config()->set("database.connections.{$connection}.driver", 'pgsql');
    config()->set("database.connections.{$connection}.database", 'pg_db_fallback');

    // DB::selectOne returns an object with name = null, triggering the ?? fallback (line 164)
    DB::shouldReceive('selectOne')
        ->andReturn((object) ['name' => null, 'version' => null, 'size' => null]);
    DB::shouldReceive('table')->passthru();
    DB::shouldReceive('getSchemaBuilder')->passthru();

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->where('database.name', 'pg_db_fallback'),
        );
});

it('resolveDirectorySize catches Throwable when iterator throws', function (): void {
    // The controller calls resolveDirectorySize(storage_path('app')).
    // We create the storage base dir, then create the 'app' subdirectory with a file,
    // then make 'app' unreadable so RecursiveDirectoryIterator throws.
    $tmpBase = sys_get_temp_dir().'/builder_iter_test_'.uniqid();
    $tmpApp = $tmpBase.'/app';
    mkdir($tmpApp, 0755, true);
    touch($tmpApp.'/file.txt');

    // Make the 'app' subdirectory unreadable so the iterator throws
    chmod($tmpApp, 0000);

    app()->useStoragePath($tmpBase);

    $user = HealthTestUser::create(['name' => 'Admin', 'email' => 'dir-iter-catch@example.com', 'password' => 'secret']);

    $this->actingAs($user)
        ->get(route('builder.health'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Health/Index')
            ->has('storage'),
        );

    // Restore permissions so test cleanup can delete the directory
    chmod($tmpApp, 0755);
    @unlink($tmpApp.'/file.txt');
    @rmdir($tmpApp);
    @rmdir($tmpBase);
});
