<?php

declare(strict_types=1);

use Builder\Core\Extensions\Settings\Pages\GeneralSettings;
use Builder\Core\Foundation\Definition\Field;
use Builder\Core\Foundation\Definition\FieldBuilder;
use Builder\Core\Foundation\Registry;
use Builder\Core\Foundation\Settings\BuilderSettings;
use Builder\Core\Foundation\Settings\SettingsExtension;
use Builder\Core\Tests\BuilderTestUser;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Inertia\Testing\AssertableInertia;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\PermissionRegistrar;

// Named test helper classes (not inside closures — PHP limitation)

final class TestSubSettings extends BuilderSettings
{
    public ?string $custom_field = null;

    public static function group(): string
    {
        return 'advanced';
    }

    public function getLabel(): string
    {
        return 'Advanced';
    }

    public function getParent(): ?string
    {
        return 'general';
    }

    public function fields(): FieldBuilder
    {
        return FieldBuilder::make()->add(Field::text('custom_field')->label('Custom'));
    }

    public function rules(): array
    {
        return ['custom_field' => ['nullable', 'string']];
    }
}

final class TestProtectedSettings extends BuilderSettings
{
    public static function group(): string
    {
        return 'protected';
    }

    public function getLabel(): string
    {
        return 'Protected';
    }

    public function getPermission(): ?string
    {
        return 'manage-protected';
    }
}

/** Settings page with NO validation rules (covers the $request->except() branch). */
final class TestNoRulesSettings extends BuilderSettings
{
    public ?string $free_text = null;

    public static function group(): string
    {
        return 'no-rules';
    }

    public function getLabel(): string
    {
        return 'No Rules';
    }

    public function rules(): array
    {
        return [];
    }

    public function fields(): FieldBuilder
    {
        return FieldBuilder::make()->add(Field::text('free_text')->label('Free Text'));
    }
}

/** Settings class with an underscore-prefixed property to cover the seedDefaults skip branch. */
final class TestUnderscoreSettings extends BuilderSettings
{
    public ?string $visible_field = null;

    /** @var string This should be skipped by seedDefaults() because it starts with _. */
    public ?string $_internal = null;

    public static function group(): string
    {
        return 'underscore-settings';
    }

    public function getLabel(): string
    {
        return 'Underscore Settings';
    }
}

/** Separate settings class used as an extension target for coverage of extension save path. */
final class TestExtSettings extends BuilderSettings
{
    public ?string $ext_field = null;

    public static function group(): string
    {
        return 'ext-settings';
    }

    public function getLabel(): string
    {
        return 'Ext Settings';
    }
}

/** Extension that contributes ext_field to the general settings page. */
final class TestGeneralExtension extends SettingsExtension
{
    public static function getExtends(): string
    {
        return GeneralSettings::class;
    }

    public static function getSettingsClass(): string
    {
        return TestExtSettings::class;
    }

    public function fields(): FieldBuilder
    {
        return FieldBuilder::make();
    }

    public function rules(): array
    {
        return ['ext_field' => ['nullable', 'string']];
    }
}

// -----------------------------------------------------------------------

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

    Schema::create('settings', function ($table): void {
        $table->id();
        $table->string('group');
        $table->string('name');
        $table->boolean('locked')->default(false);
        $table->json('payload');
        $table->timestamps();
        $table->unique(['group', 'name']);
    });

    config(['auth.providers.users.model' => BuilderTestUser::class]);

    // Seed default rows so Spatie's save() can find them (requires rows to exist).
    (new GeneralSettings)->seedDefaults();
    (new TestSubSettings)->seedDefaults();
    (new TestNoRulesSettings)->seedDefaults();
    (new TestExtSettings)->seedDefaults();
    (new TestUnderscoreSettings)->seedDefaults();

    // Reset scoped instances so each test gets a fresh Settings object.
    app()->forgetInstance(GeneralSettings::class);
    app()->forgetInstance(TestSubSettings::class);
    app()->forgetInstance(TestNoRulesSettings::class);
    app()->forgetInstance(TestExtSettings::class);
    app()->forgetInstance(TestUnderscoreSettings::class);

    app(Registry::class)->settingsPage(GeneralSettings::class);
    app(Registry::class)->settingsPage(TestSubSettings::class);
    app(Registry::class)->settingsPage(TestNoRulesSettings::class);
});

// -----------------------------------------------------------------------
// GET /settings/{page}
// -----------------------------------------------------------------------

it('GET /settings/{page} renders Settings/Show with settingsDefinition and values', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'admin@example.com',
        'password' => 'secret',
    ]);

    // Pre-seed a setting
    $settings = app(GeneralSettings::class);
    $settings->site_name = 'My Site';
    $settings->save();
    app()->forgetInstance(GeneralSettings::class);

    $this->actingAs($user)
        ->get(route('builder.settings.show', 'general'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Settings/Show')
            ->has('settingsDefinition')
            ->has('settingsDefinition.key')
            ->has('settingsDefinition.sections')
            ->has('values')
            ->where('values.site_name', 'My Site'),
        );
});

it('GET /settings/{page} returns 404 for unknown page key', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'admin@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->get(route('builder.settings.show', 'nonexistent'))
        ->assertNotFound();
});

it('GET /settings/{page} returns 403 when user lacks permission', function (): void {
    // Set up Spatie tables for permission checking.
    Schema::create('permissions', function ($t): void {
        $t->bigIncrements('id');
        $t->string('name');
        $t->string('guard_name');
        $t->timestamps();
        $t->unique(['name', 'guard_name']);
    });
    Schema::create('roles', function ($t): void {
        $t->bigIncrements('id');
        $t->string('name');
        $t->string('guard_name');
        $t->timestamps();
        $t->unique(['name', 'guard_name']);
    });
    Schema::create('model_has_permissions', function ($t): void {
        $t->unsignedBigInteger('permission_id');
        $t->string('model_type');
        $t->unsignedBigInteger('model_id');
        $t->primary(['permission_id', 'model_id', 'model_type']);
    });
    Schema::create('model_has_roles', function ($t): void {
        $t->unsignedBigInteger('role_id');
        $t->string('model_type');
        $t->unsignedBigInteger('model_id');
        $t->primary(['role_id', 'model_id', 'model_type']);
    });
    Schema::create('role_has_permissions', function ($t): void {
        $t->unsignedBigInteger('permission_id');
        $t->unsignedBigInteger('role_id');
        $t->primary(['permission_id', 'role_id']);
    });

    config([
        'permission.models.permission' => Permission::class,
        'permission.table_names.roles' => 'roles',
        'permission.table_names.permissions' => 'permissions',
        'permission.table_names.model_has_permissions' => 'model_has_permissions',
        'permission.table_names.model_has_roles' => 'model_has_roles',
        'permission.table_names.role_has_permissions' => 'role_has_permissions',
        'permission.cache.store' => 'array',
    ]);
    app(PermissionRegistrar::class)->forgetCachedPermissions();

    app(Registry::class)->settingsPage(TestProtectedSettings::class);

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

    $this->actingAs($user)
        ->get(route('builder.settings.show', 'protected'))
        ->assertForbidden();
});

// -----------------------------------------------------------------------
// PUT /settings/{page}
// -----------------------------------------------------------------------

it('PUT /settings/{page} validates and saves settings', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'admin@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'general'), [
            'site_name' => 'My Awesome Site',
            'site_url' => 'https://example.com',
            'admin_email' => 'admin@example.com',
        ])
        ->assertRedirect();

    app()->forgetInstance(GeneralSettings::class);
    expect(app(GeneralSettings::class)->site_name)->toBe('My Awesome Site');
});

it('PUT /settings/{page} returns validation errors for invalid data', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'admin@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'general'), [
            'site_name' => '', // required
        ])
        ->assertSessionHasErrors(['site_name']);
});

it('PUT /settings/{page} preserves unchanged properties on save', function (): void {
    // Pre-seed values
    $settings = app(GeneralSettings::class);
    $settings->site_name = 'Old Name';
    $settings->admin_email = 'old@example.com';
    $settings->save();
    app()->forgetInstance(GeneralSettings::class);

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

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'general'), [
            'site_name' => 'New Name',
            'site_url' => null,
            'admin_email' => 'old@example.com',
        ])
        ->assertRedirect();

    app()->forgetInstance(GeneralSettings::class);
    $saved = app(GeneralSettings::class);

    expect($saved->site_name)->toBe('New Name')
        ->and($saved->admin_email)->toBe('old@example.com');
});

// -----------------------------------------------------------------------
// Sub-page routes
// -----------------------------------------------------------------------

it('GET /settings/{parent}/{page} renders Settings/Show for sub-pages', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'admin@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->get(route('builder.settings.show.sub', ['general', 'advanced']))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Settings/Show')
            ->where('settingsDefinition.key', 'general.advanced'),
        );
});

it('PUT /settings/{parent}/{page} saves sub-page settings', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'admin@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update.sub', ['general', 'advanced']), [
            'custom_field' => 'hello',
        ])
        ->assertRedirect();

    app()->forgetInstance(TestSubSettings::class);
    expect(app(TestSubSettings::class)->custom_field)->toBe('hello');
});

// -----------------------------------------------------------------------
// Authorization
// -----------------------------------------------------------------------

it('PUT /settings/{page} returns 404 when page key does not exist (authorize aborts)', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'ghost-page@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'nonexistent-page'), [
            'foo' => 'bar',
        ])
        ->assertNotFound();
});

it('PUT /settings/{page} returns 403 when user lacks page permission (authorize check)', function (): void {
    Schema::create('permissions', function ($t): void {
        $t->bigIncrements('id');
        $t->string('name');
        $t->string('guard_name');
        $t->timestamps();
        $t->unique(['name', 'guard_name']);
    });
    Schema::create('roles', function ($t): void {
        $t->bigIncrements('id');
        $t->string('name');
        $t->string('guard_name');
        $t->timestamps();
        $t->unique(['name', 'guard_name']);
    });
    Schema::create('model_has_permissions', function ($t): void {
        $t->unsignedBigInteger('permission_id');
        $t->string('model_type');
        $t->unsignedBigInteger('model_id');
        $t->primary(['permission_id', 'model_id', 'model_type']);
    });
    Schema::create('model_has_roles', function ($t): void {
        $t->unsignedBigInteger('role_id');
        $t->string('model_type');
        $t->unsignedBigInteger('model_id');
        $t->primary(['role_id', 'model_id', 'model_type']);
    });
    Schema::create('role_has_permissions', function ($t): void {
        $t->unsignedBigInteger('permission_id');
        $t->unsignedBigInteger('role_id');
        $t->primary(['permission_id', 'role_id']);
    });

    config([
        'permission.models.permission' => Permission::class,
        'permission.table_names.roles' => 'roles',
        'permission.table_names.permissions' => 'permissions',
        'permission.table_names.model_has_permissions' => 'model_has_permissions',
        'permission.table_names.model_has_roles' => 'model_has_roles',
        'permission.table_names.role_has_permissions' => 'role_has_permissions',
        'permission.cache.store' => 'array',
    ]);
    app(PermissionRegistrar::class)->forgetCachedPermissions();

    app(Registry::class)->settingsPage(TestProtectedSettings::class);

    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'put-forbidden@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'protected'), [
            'foo' => 'bar',
        ])
        ->assertForbidden();
});

it('PUT /settings/{page} saves without rules using request->except()', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'no-rules@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'no-rules'), [
            'free_text' => 'anything goes',
        ])
        ->assertRedirect();

    app()->forgetInstance(TestNoRulesSettings::class);
    expect(app(TestNoRulesSettings::class)->free_text)->toBe('anything goes');
});

it('PUT /settings/{page} drops unknown properties even when rules are empty', function (): void {
    // Settings page with no rules falls back to $request->except() — but
    // saveToClass()->fill() must still only persist known properties.
    // Smuggling an undeclared key in the payload must not create a new property
    // on the settings record.
    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'no-rules-extra@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'no-rules'), [
            'free_text' => 'legit value',
            'arbitrary_smuggled_field' => 'hax',
        ])
        ->assertRedirect();

    app()->forgetInstance(TestNoRulesSettings::class);
    $settings = app(TestNoRulesSettings::class);
    expect($settings->free_text)->toBe('legit value');
    expect(property_exists($settings, 'arbitrary_smuggled_field'))->toBeFalse();
});

it('seedDefaults is idempotent and skips existing properties on repeated calls', function (): void {
    // beforeEach already called seedDefaults(); calling it again should not throw or duplicate rows
    $beforeCount = DB::table('settings')->where('group', 'general')->count();

    (new GeneralSettings)->seedDefaults();

    $afterCount = DB::table('settings')->where('group', 'general')->count();
    expect($afterCount)->toBe($beforeCount);
});

it('PUT /settings/{page} saves extension fields via extendSettings', function (): void {
    app(Registry::class)->extendSettings(TestGeneralExtension::class);

    $user = BuilderTestUser::create([
        'name' => 'Admin',
        'email' => 'ext-settings@example.com',
        'password' => 'secret',
    ]);

    $this->actingAs($user)
        ->put(route('builder.settings.update', 'general'), [
            'site_name' => 'Extended Site',
            'site_url' => null,
            'admin_email' => null,
            'site_logo' => null,
            'ext_field' => 'my extension value',
        ])
        ->assertRedirect();

    app()->forgetInstance(TestExtSettings::class);
    expect(app(TestExtSettings::class)->ext_field)->toBe('my extension value');
});

it('seedDefaults skips properties with underscore prefix', function (): void {
    // beforeEach already called seedDefaults() on TestUnderscoreSettings which has both
    // visible_field (regular) and _internal (underscore-prefixed, should be skipped).
    $rows = DB::table('settings')
        ->where('group', 'underscore-settings')
        ->pluck('name')
        ->all();

    expect($rows)->toContain('visible_field')
        ->and($rows)->not->toContain('_internal');
});
