<?php

declare(strict_types=1);

use Builder\Core\Foundation\Account\AccountExtension;
use Builder\Core\Foundation\Definition\Hook;
use Builder\Core\Foundation\Registry;
use Builder\Core\Tests\BuilderTestUser;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Inertia\Testing\AssertableInertia;
use PragmaRX\Google2FA\Google2FA;

final class SavedHookExtension extends AccountExtension
{
    public static bool $wasCalled = false;

    /** @return list<Hook> */
    public function hooks(): array
    {
        return [
            Hook::on('saved', function (mixed $user, array $data): void {
                self::$wasCalled = true;
            }),
        ];
    }
}

beforeEach(function (): void {
    SavedHookExtension::$wasCalled = false;

    Schema::create('users', function ($table): void {
        $table->id();
        $table->string('name')->nullable();
        $table->string('email')->nullable()->unique();
        $table->string('locale', 10)->nullable();
        $table->string('password')->nullable();
        $table->text('two_factor_secret')->nullable();
        $table->text('two_factor_recovery_codes')->nullable();
        $table->timestamp('two_factor_confirmed_at')->nullable();
        $table->json('user_widget_preferences')->nullable();
        $table->rememberToken();
        $table->timestamps();
    });

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

it('GET /account renders Account/Index with accountDefinition prop', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane Doe',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->get(route('builder.account.show'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Account/Index')
            ->has('accountDefinition')
            ->has('accountDefinition.sections')
            ->has('user')
            ->where('mfaEnabled', false)
            ->where('recoveryCodes', null),
        );
});

it('PUT /account updates name without current_password when email is unchanged', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Old Name',
        'email' => 'old@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'New Name',
            'email' => 'old@example.com',
        ])
        ->assertRedirect();

    $user->refresh();
    expect($user->name)->toBe('New Name')
        ->and($user->email)->toBe('old@example.com');
});

it('PUT /account allows email change when current_password is provided', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Old Name',
        'email' => 'old@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Old Name',
            'email' => 'new@example.com',
            'current_password' => 'secret',
        ])
        ->assertRedirect();

    $user->refresh();
    expect($user->email)->toBe('new@example.com');
});

it('PUT /account rejects email change without current_password', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Owner',
        'email' => 'owner@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Owner',
            'email' => 'attacker@example.com',
        ])
        ->assertSessionHasErrors(['current_password']);

    $user->refresh();
    expect($user->email)->toBe('owner@example.com');
});

it('PUT /account rejects email change with wrong current_password', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Owner',
        'email' => 'owner@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Owner',
            'email' => 'attacker@example.com',
            'current_password' => 'wrong',
        ])
        ->assertSessionHasErrors(['current_password']);

    $user->refresh();
    expect($user->email)->toBe('owner@example.com');
});

it('PUT /account changes password when new password provided with correct current', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('old-password'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Jane',
            'email' => 'jane@example.com',
            'current_password' => 'old-password',
            'password' => 'new-password',
            'password_confirmation' => 'new-password',
        ])
        ->assertRedirect();

    $user->refresh();
    expect(Hash::check('new-password', $user->password))->toBeTrue();
});

it('PUT /account rejects wrong current password', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('real-password'),
    ]);

    $response = $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Jane',
            'email' => 'jane@example.com',
            'current_password' => 'wrong-password',
            'password' => 'new-password',
            'password_confirmation' => 'new-password',
        ]);

    // Should redirect back with errors
    $response->assertSessionHasErrors(['current_password']);
});

it('PUT /account does not change password when password field is empty', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('original'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Jane',
            'email' => 'jane@example.com',
        ])
        ->assertRedirect();

    $user->refresh();
    expect(Hash::check('original', $user->password))->toBeTrue();
});

it('PUT /account returns validation error for missing name', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'email' => 'jane@example.com',
        ])
        ->assertSessionHasErrors(['name']);
});

it('POST /logout invalidates session and redirects to login', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->post(route('builder.logout'))
        ->assertRedirect();

    $this->assertGuest();
});

it('GET /account requires authentication', function (): void {
    $this->get(route('builder.account.show'))
        ->assertRedirect(route('builder.login'));
});

it('PUT /account rejects locale outside config(builder.locales)', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Jane',
            'email' => 'jane@example.com',
            'locale' => 'xx-bogus',
        ])
        ->assertSessionHasErrors(['locale']);

    $user->refresh();
    expect($user->locale)->not->toBe('xx-bogus');
});

it('PUT /account saves locale to database', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Jane',
            'email' => 'jane@example.com',
            'locale' => 'nl',
        ])
        ->assertRedirect();

    $user->refresh();
    expect($user->locale)->toBe('nl');
});

// ── MFA endpoints ─────────────────────────────────────────────────────────────

it('POST /account/mfa/enable returns QR code URL and secret', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $response = $this->actingAs($user)
        ->postJson(route('builder.account.mfa.enable'))
        ->assertOk()
        ->assertJsonStructure(['secret', 'qr_code_url']);

    expect($response->json('secret'))->not->toBeEmpty();
});

it('POST /account/mfa/confirm with valid TOTP enables MFA and shows recovery codes', function (): void {
    $google2fa = new Google2FA;
    $secret = $google2fa->generateSecretKey();

    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $response = $this->actingAs($user)
        ->withSession(['mfa_pending_secret' => $secret])
        ->post(route('builder.account.mfa.confirm'), [
            'code' => $google2fa->getCurrentOtp($secret),
        ]);

    $response->assertRedirect(route('builder.account.show'));
    $user->refresh();
    expect($user->two_factor_confirmed_at)->not->toBeNull();
});

it('POST /account/mfa/confirm with invalid code returns error', function (): void {
    $google2fa = new Google2FA;
    $secret = $google2fa->generateSecretKey();

    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->withSession(['mfa_pending_secret' => $secret])
        ->post(route('builder.account.mfa.confirm'), ['code' => '000000'])
        ->assertSessionHasErrors(['code']);
});

it('POST /account/mfa/confirm without pending secret returns error', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->post(route('builder.account.mfa.confirm'), ['code' => '123456'])
        ->assertSessionHasErrors(['code']);
});

it('POST /account/mfa/recovery-codes regenerates recovery codes when MFA is enabled', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
        'two_factor_secret' => encrypt('FAKESECRET'),
        'two_factor_recovery_codes' => encrypt(json_encode(['OLD-CODE'])),
        'two_factor_confirmed_at' => now(),
    ]);

    $this->actingAs($user)
        ->post(route('builder.account.mfa.recovery-codes'))
        ->assertRedirect(route('builder.account.show'));

    $user->refresh();
    $newCodes = json_decode(decrypt($user->two_factor_recovery_codes), true);
    expect($newCodes)->not->toContain('OLD-CODE')
        ->and($newCodes)->toHaveCount(8);
});

it('POST /account/mfa/recovery-codes returns error when MFA not enabled', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->post(route('builder.account.mfa.recovery-codes'))
        ->assertSessionHasErrors(['mfa']);
});

it('GET /account shows recovery codes when show_recovery_codes is in session', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
        'two_factor_secret' => encrypt('SECRET'),
        'two_factor_recovery_codes' => encrypt(json_encode(['ABCD-1234'])),
        'two_factor_confirmed_at' => now(),
    ]);

    $this->actingAs($user)
        ->withSession(['show_recovery_codes' => true])
        ->get(route('builder.account.show'))
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Account/Index')
            ->where('mfaEnabled', true)
            ->has('recoveryCodes')
        );
});

it('PUT /account executes saved hook from AccountExtension', function (): void {
    app(Registry::class)->accountExtension(new SavedHookExtension);

    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Jane Updated',
            'email' => 'jane@example.com',
        ])
        ->assertRedirect();

    expect(SavedHookExtension::$wasCalled)->toBeTrue();
});

it('PUT /account without locale leaves locale unchanged', function (): void {
    $user = BuilderTestUser::create([
        'name' => 'Jane',
        'email' => 'jane@example.com',
        'password' => Hash::make('secret'),
        'locale' => null,
    ]);

    $this->actingAs($user)
        ->put(route('builder.account.update'), [
            'name' => 'Jane',
            'email' => 'jane@example.com',
        ])
        ->assertRedirect();

    $user->refresh();
    expect($user->locale)->toBeNull();
});
