<?php

namespace Tests\Feature;

use App\Models\User;
use App\Services\BackupService;
use App\Services\SecureFileService;
use App\Services\TotpService;
use Database\Seeders\AuthorizationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use RuntimeException;
use Tests\TestCase;

class SecurityBackupReviewTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed(AuthorizationSeeder::class);
    }

    public function test_two_factor_setup_is_encrypted_and_confirmed_with_totp(): void
    {
        $user = User::factory()->create([
            'status' => 'active',
            'password' => Hash::make('Password123'),
        ]);

        $response = $this->actingAs($user)->post('/security/two-factor/enable', [
            'password' => 'Password123',
        ]);

        $response->assertRedirect()->assertSessionHas('two_factor_setup_secret')->assertSessionHas('two_factor_recovery_codes');
        $secret = (string) session('two_factor_setup_secret');
        $this->assertNotSame('', $secret);
        $this->assertNotSame($secret, (string) DB::table('users')->where('id', $user->id)->value('two_factor_secret'));

        $code = app(TotpService::class)->code($secret);
        $this->actingAs($user)->post('/security/two-factor/confirm', ['code' => $code])->assertRedirect();

        $user->refresh();
        $this->assertNotNull($user->two_factor_confirmed_at);
        $this->assertDatabaseHas('audit_logs', ['action' => 'security.two_factor_enabled', 'auditable_id' => $user->id]);
    }

    public function test_enabled_two_factor_blocks_password_only_login_and_recovery_code_is_one_time(): void
    {
        $totp = app(TotpService::class);
        $secret = $totp->generateSecret();
        $recovery = $totp->generateRecoveryCodes();
        $user = User::factory()->create([
            'status' => 'active',
            'password' => Hash::make('Password123'),
        ]);
        $user->forceFill([
            'two_factor_secret' => $secret,
            'two_factor_recovery_codes' => $recovery['hashes'],
            'two_factor_confirmed_at' => now(),
        ])->save();

        $this->post('/login', ['login' => $user->email, 'password' => 'Password123'])
            ->assertRedirect('/two-factor-challenge')
            ->assertSessionHas('security.two_factor_user_id', $user->id);
        $this->assertGuest();

        $this->post('/two-factor-challenge', ['code' => $recovery['plain'][0]])->assertRedirect('/dashboard');
        $this->assertAuthenticatedAs($user);
        $this->assertDatabaseHas('login_attempts', ['user_id' => $user->id, 'status' => 'recovery_success']);

        $this->post('/logout')->assertRedirect('/login');
        $this->post('/login', ['login' => $user->email, 'password' => 'Password123'])->assertRedirect('/two-factor-challenge');
        $this->post('/two-factor-challenge', ['code' => $recovery['plain'][0]])->assertSessionHasErrors('code');
        $this->assertGuest();
    }

    public function test_login_failures_are_monitored_without_bypassing_generic_authentication_error(): void
    {
        $user = User::factory()->create([
            'status' => 'active',
            'password' => Hash::make('Password123'),
        ]);

        $response = $this->withServerVariables(['REMOTE_ADDR' => '10.20.30.40'])->post('/login', [
            'login' => $user->email,
            'password' => 'wrong-password',
        ]);

        $response->assertSessionHasErrors('login');
        $this->assertGuest();
        $this->assertDatabaseHas('login_attempts', [
            'user_id' => $user->id,
            'status' => 'failed_credentials',
            'ip_address' => '10.20.30.40',
        ]);

        $this->post('/login', [
            'login' => "' OR 1=1 --",
            'password' => 'anything',
        ])->assertSessionHasErrors('login');
        $this->assertGuest();
    }

    public function test_security_headers_and_session_defaults_are_hardened(): void
    {
        $response = $this->get('/login');

        $response->assertOk();
        $response->assertHeader('X-Content-Type-Options', 'nosniff');
        $response->assertHeader('X-Frame-Options', 'DENY');
        $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->assertHeader('Cross-Origin-Opener-Policy', 'same-origin');
        $this->assertStringContainsString("frame-ancestors 'none'", (string) $response->headers->get('Content-Security-Policy'));
        $this->assertTrue((bool) config('session.encrypt'));
        $this->assertSame('strict', config('session.same_site'));
        $this->assertTrue((bool) config('session.http_only'));
    }

    public function test_private_file_service_rejects_executable_content_and_disables_download_caching(): void
    {
        Storage::fake('local');
        $files = app(SecureFileService::class);

        try {
            $files->store(UploadedFile::fake()->createWithContent('payload.php', '<?php echo 1;'), 'security-tests');
            $this->fail('Executable upload was not rejected.');
        } catch (ValidationException $exception) {
            $this->assertArrayHasKey('file', $exception->errors());
        }

        $path = $files->store(UploadedFile::fake()->create('identity.pdf', 8, 'application/pdf'), 'security-tests');
        Storage::disk('local')->assertExists($path);
        $download = $files->download($path, 'identity.pdf');
        $this->assertSame('nosniff', $download->headers->get('X-Content-Type-Options'));
        $this->assertStringContainsString('no-store', (string) $download->headers->get('Cache-Control'));
    }

    public function test_backup_captures_database_and_every_private_file_with_tamper_detection(): void
    {
        $backupRoot = storage_path('framework/testing/step25-backups');
        $privateRoot = storage_path('framework/testing/step25-private');
        File::deleteDirectory($backupRoot);
        File::deleteDirectory($privateRoot);
        File::ensureDirectoryExists($privateRoot.'/customers/1/kyc');
        File::ensureDirectoryExists($privateRoot.'/loans/1/contracts');
        File::ensureDirectoryExists($privateRoot.'/collateral/1');
        File::ensureDirectoryExists($privateRoot.'/expenses/1');
        File::put($privateRoot.'/customers/1/kyc/id.pdf', 'customer-document');
        File::put($privateRoot.'/loans/1/contracts/agreement.pdf', 'contract-document');
        File::put($privateRoot.'/collateral/1/photo.jpg', 'collateral-file');
        File::put($privateRoot.'/expenses/1/receipt.pdf', 'financial-document');

        config([
            'backups.path' => $backupRoot,
            'backups.private_root' => $privateRoot,
            'backups.keep_days' => 30,
        ]);
        $user = User::factory()->create(['status' => 'active']);
        $backups = app(BackupService::class);

        try {
            $created = $backups->create();
            $this->assertDirectoryExists($created['path']);
            $this->assertSame(4, count($created['manifest']['private_files']));
            $this->assertSame('sqlite', $created['manifest']['database']['driver']);
            $this->assertStringContainsString($user->email, File::get($created['path'].'/database.sql'));
            $verified = $backups->verify($created['path']);
            $this->assertSame(4, $verified['private_files']);

            File::put($created['path'].'/private/customers/1/kyc/id.pdf', 'tampered');
            $this->expectException(RuntimeException::class);
            $backups->verify($created['path']);
        } finally {
            File::deleteDirectory($backupRoot);
            File::deleteDirectory($privateRoot);
        }
    }

    public function test_password_reset_is_non_enumerating_and_revokes_database_sessions(): void
    {
        $missing = $this->post('/forgot-password', ['email' => 'missing@example.test']);
        $missing->assertSessionHas('status', 'If an active account exists for that email, a password reset link has been sent.');

        $user = User::factory()->create([
            'status' => 'active',
            'password' => Hash::make('OldPassword123'),
        ]);
        DB::table('sessions')->insert([
            'id' => 'step25-old-session',
            'user_id' => $user->id,
            'ip_address' => '127.0.0.1',
            'user_agent' => 'test',
            'payload' => 'payload',
            'last_activity' => time(),
        ]);
        $token = Password::broker()->createToken($user);

        $this->post('/reset-password', [
            'token' => $token,
            'email' => $user->email,
            'password' => 'NewPassword123',
            'password_confirmation' => 'NewPassword123',
        ])->assertRedirect('/login');

        $this->assertDatabaseMissing('sessions', ['id' => 'step25-old-session']);
        $this->assertTrue(Hash::check('NewPassword123', (string) $user->fresh()->password));
    }

    public function test_security_routes_backup_commands_and_user_navigation_are_registered(): void
    {
        $user = User::factory()->create(['status' => 'active']);
        $user->givePermissionTo('dashboard.view');
        $this->actingAs($user)->get('/security')->assertOk()->assertSee('Recent sign-in activity');
        $this->actingAs($user)->get('/dashboard')->assertOk()->assertSee('Security');
        $this->get('/two-factor-challenge')->assertRedirect('/dashboard');

        $commands = Artisan::all();
        $this->assertArrayHasKey('security:backup', $commands);
        $this->assertArrayHasKey('security:backup-verify', $commands);
    }
}
