<?php

declare(strict_types=1);

use App\Models\Tenant;
use App\Models\User;
use App\Models\UserRecoveryCode;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use PragmaRX\Google2FA\Google2FA;

uses(DatabaseTransactions::class);

beforeEach(function () {
    $this->tenant = Tenant::factory()->create();
    $this->user = User::factory()->create([
        'tenant_id' => $this->tenant->id,
        'email' => 'setup-2fa@example.ru',
        'password_hash' => Hash::make('master-password-9876'),
        'totp_enabled' => false,
        'totp_secret' => null,
    ]);
    $this->actingAs($this->user);
});

test('POST /api/2fa/init возвращает secret + qr_url и кладёт secret в session', function () {
    $r = $this->postJson('/api/2fa/init');

    $r->assertOk();
    expect($r->json('secret'))->not->toBeEmpty();
    expect($r->json('qr_url'))->toContain('otpauth://totp/');
    expect($r->json('qr_url'))->toContain(rawurlencode('Лидерра'));
});

test('POST /api/2fa/init 422 если 2FA уже включена', function () {
    $this->user->forceFill(['totp_enabled' => true, 'totp_secret' => 'EXISTSECRET'])->save();

    $r = $this->postJson('/api/2fa/init');

    $r->assertStatus(422);
});

test('POST /api/2fa/confirm с правильным кодом активирует 2FA + возвращает 8 recovery-кодов', function () {
    $this->postJson('/api/2fa/init')->assertOk();
    $secret = session('auth.pending_totp_secret');
    expect($secret)->not->toBeEmpty();

    $google2fa = new Google2FA;
    $code = $google2fa->getCurrentOtp($secret);

    $r = $this->postJson('/api/2fa/confirm', ['code' => $code]);

    $r->assertOk();
    expect($r->json('recovery_codes'))->toHaveCount(8);
    foreach ($r->json('recovery_codes') as $plainCode) {
        // Формат XXXX-XXXX (8 hex + дефис посередине = 9 chars).
        expect($plainCode)->toMatch('/^[a-z0-9]{4}-[a-z0-9]{4}$/');
    }

    // Запись в users применилась.
    $this->user->refresh();
    expect($this->user->totp_enabled)->toBeTrue();
    // totp_secret через encrypted-cast возвращает plain.
    expect($this->user->totp_secret)->toBe($secret);

    // Ровно 8 неиспользованных recovery-кодов в БД.
    expect(UserRecoveryCode::query()->where('user_id', $this->user->id)->count())->toBe(8);
});

test('POST /api/2fa/confirm с неверным кодом → 422 и 2FA остаётся выключенной', function () {
    $this->postJson('/api/2fa/init')->assertOk();

    $r = $this->postJson('/api/2fa/confirm', ['code' => '000000']);

    $r->assertStatus(422);
    $this->user->refresh();
    expect($this->user->totp_enabled)->toBeFalse();
});

test('POST /api/2fa/confirm без init → 422', function () {
    $r = $this->postJson('/api/2fa/confirm', ['code' => '123456']);

    $r->assertStatus(422);
    expect($r->json('message'))->toContain('Не начат setup');
});

test('POST /api/2fa/disable с правильным паролем выключает 2FA + удаляет recovery codes', function () {
    // Включаем 2FA через wizard.
    $this->postJson('/api/2fa/init')->assertOk();
    $secret = session('auth.pending_totp_secret');
    $code = (new Google2FA)->getCurrentOtp($secret);
    $this->postJson('/api/2fa/confirm', ['code' => $code])->assertOk();

    expect(UserRecoveryCode::query()->where('user_id', $this->user->id)->count())->toBe(8);

    $r = $this->postJson('/api/2fa/disable', ['password' => 'master-password-9876']);

    $r->assertOk();
    $this->user->refresh();
    expect($this->user->totp_enabled)->toBeFalse();
    expect($this->user->totp_secret)->toBeNull();
    expect(UserRecoveryCode::query()->where('user_id', $this->user->id)->count())->toBe(0);
});

test('POST /api/2fa/disable с неверным паролем → 422', function () {
    $this->user->forceFill(['totp_enabled' => true, 'totp_secret' => 'XXX'])->save();

    $r = $this->postJson('/api/2fa/disable', ['password' => 'wrong-password']);

    $r->assertStatus(422);
});

test('POST /api/2fa/regenerate-recovery-codes возвращает новые 8 кодов', function () {
    // Сначала setup 2FA.
    $this->postJson('/api/2fa/init')->assertOk();
    $secret = session('auth.pending_totp_secret');
    $code = (new Google2FA)->getCurrentOtp($secret);
    $confirm = $this->postJson('/api/2fa/confirm', ['code' => $code])->assertOk();

    $oldCodes = $confirm->json('recovery_codes');
    $oldHashes = DB::table('user_recovery_codes')
        ->where('user_id', $this->user->id)
        ->pluck('code_hash')
        ->toArray();

    $r = $this->postJson('/api/2fa/regenerate-recovery-codes', ['password' => 'master-password-9876']);

    $r->assertOk();
    expect($r->json('recovery_codes'))->toHaveCount(8);

    // Старые хеши удалены, новые отличаются.
    $newHashes = DB::table('user_recovery_codes')
        ->where('user_id', $this->user->id)
        ->pluck('code_hash')
        ->toArray();
    expect($newHashes)->toHaveCount(8);
    expect(array_intersect($oldHashes, $newHashes))->toBe([]);
    expect($r->json('recovery_codes'))->not->toBe($oldCodes);
});

test('POST /api/2fa/regenerate-recovery-codes 422 если 2FA не включена', function () {
    $r = $this->postJson('/api/2fa/regenerate-recovery-codes', ['password' => 'master-password-9876']);

    $r->assertStatus(422);
});

test('Все 4 эндпоинта /api/2fa/* требуют auth:sanctum (401 без login)', function () {
    auth()->logout();

    foreach (['init', 'confirm', 'disable', 'regenerate-recovery-codes'] as $endpoint) {
        $this->postJson('/api/2fa/'.$endpoint, ['code' => '000000', 'password' => 'x'])
            ->assertStatus(401);
    }
});
