<?php

namespace Tests\Feature;

use App\Enums\DisplayFromFormat;
use App\Exports\AliasesExport;
use App\Imports\AliasesImport;
use App\Models\Alias;
use App\Models\AliasRecipient;
use App\Models\DeletedUsername;
use App\Models\Domain;
use App\Models\Recipient;
use App\Models\Username;
use App\Notifications\CustomVerifyEmail;
use App\Notifications\DefaultRecipientUpdated;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class SettingsTest extends TestCase
{
    use LazilyRefreshDatabase;

    protected $user;

    protected function setUp(): void
    {
        parent::setUp();

        $this->user = $this->createUser('johndoe', null, ['password' => Hash::make('mypassword')]);
        $this->actingAs($this->user);
    }

    #[Test]
    public function user_can_update_default_recipient()
    {
        Notification::fake();

        $currentDefaultRecipient = $this->user->defaultRecipient;

        $newDefaultRecipient = Recipient::factory()->create([
            'user_id' => $this->user->id,
        ]);

        $this->assertNotEquals($currentDefaultRecipient->id, $newDefaultRecipient->id);

        $response = $this->post('/settings/default-recipient', [
            'id' => $newDefaultRecipient->id,
        ]);

        $response->assertStatus(200);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'default_recipient_id' => $newDefaultRecipient->id,
        ]);

        Notification::assertSentTo(
            $currentDefaultRecipient,
            DefaultRecipientUpdated::class
        );
    }

    #[Test]
    public function user_cannot_update_to_unverified_default_recipient()
    {
        $newDefaultRecipient = Recipient::factory()->create([
            'user_id' => $this->user->id,
            'email_verified_at' => null,
        ]);

        $this->assertNotEquals($this->user->default_recipient_id, $newDefaultRecipient->id);

        $response = $this->post('/settings/default-recipient', [
            'id' => $newDefaultRecipient->id,
        ]);

        $response->assertStatus(404);
        $this->assertNotEquals($this->user->default_recipient_id, $newDefaultRecipient->id);
    }

    #[Test]
    public function user_can_edit_default_recipient()
    {
        Notification::fake();

        $currentDefaultRecipient = $this->user->defaultRecipient;

        $response = $this->post('/settings/edit-default-recipient', [
            'email' => 'new@example.com',
            'current' => 'mypassword',
        ]);

        $response->assertStatus(302);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'default_recipient_id' => $currentDefaultRecipient->id,
        ]);

        $this->assertDatabaseHas('recipients', [
            'id' => $currentDefaultRecipient->id,
        ]);

        $this->assertDatabaseHas('recipients', [
            'user_id' => $this->user->id,
            'pending' => true,
        ]);

        $newPendingRecipient = $this->user->pendingRecipients()->first();

        Notification::assertSentTo(
            $newPendingRecipient,
            CustomVerifyEmail::class
        );
    }

    #[Test]
    public function user_must_enter_current_password_to_edit_default_recipient()
    {
        Notification::fake();

        $response = $this->post('/settings/edit-default-recipient', [
            'email' => 'new@example.com',
            'current' => 'wrong-password',
        ]);

        $response
            ->assertStatus(302)
            ->assertSessionHasErrors('current');

        Notification::assertNothingSent(
            CustomVerifyEmail::class
        );
    }

    #[Test]
    public function user_can_update_default_username()
    {
        $currentDefaultUsername = $this->user->defaultUsername;

        $newDefaultUsername = Username::factory()->create([
            'user_id' => $this->user->id,
            'can_login' => false,
        ]);

        $this->assertNotEquals($currentDefaultUsername->id, $newDefaultUsername->id);

        $response = $this->post('/settings/default-username', [
            'id' => $newDefaultUsername->id,
        ]);

        $response->assertStatus(200);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'default_username_id' => $newDefaultUsername->id,
        ]);
        $this->assertDatabaseHas('usernames', [
            'id' => $newDefaultUsername->id,
            'user_id' => $this->user->id,
            'can_login' => true,
        ]);
    }

    #[Test]
    public function user_can_update_default_alias_domain()
    {
        $defaultAliasDomain = $this->user->username.'.anonaddy.me';

        $response = $this->post('/settings/default-alias-domain', [
            'domain' => $defaultAliasDomain,
        ]);

        $response->assertStatus(302);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'default_alias_domain' => $defaultAliasDomain,
        ]);
    }

    #[Test]
    public function user_cannot_update_default_alias_domain_if_invalid()
    {
        $response = $this->post('/settings/default-alias-domain', [
            'domain' => 'invalid.anonaddy.me',
        ]);

        $response->assertStatus(302);
        $response->assertSessionHasErrors(['domain']);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'default_alias_domain' => null,
        ]);
    }

    #[Test]
    public function user_can_update_default_alias_format()
    {
        $defaultAliasFormat = 'random_words';

        $response = $this->post('/settings/default-alias-format', [
            'format' => $defaultAliasFormat,
        ]);

        $response->assertStatus(302);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'default_alias_format' => $defaultAliasFormat,
        ]);
    }

    #[Test]
    public function user_cannot_update_default_alias_format_if_invalid()
    {
        $response = $this->post('/settings/default-alias-format', [
            'format' => 'invalid_format',
        ]);

        $response->assertStatus(302);
        $response->assertSessionHasErrors(['format']);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'default_alias_format' => null,
        ]);
    }

    #[Test]
    public function user_can_update_display_from_format()
    {
        $displayFromFormat = DisplayFromFormat::DEFAULT->value;

        $response = $this->post('/settings/display-from-format', [
            'format' => $displayFromFormat,
        ]);

        $response->assertStatus(302);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'display_from_format' => $displayFromFormat,
        ]);
    }

    #[Test]
    public function user_cannot_update_display_from_format_if_invalid()
    {
        $response = $this->post('/settings/display-from-format', [
            'format' => 10,
        ]);

        $response->assertStatus(302);
        $response->assertSessionHasErrors(['format']);
        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
            'display_from_format' => 0,
        ]);
    }

    #[Test]
    public function user_can_update_reply_from_name()
    {
        $this->assertNull($this->user->from_name);

        $response = $this->post('/settings/from-name', [
            'from_name' => 'John Doe',
        ]);

        $response->assertStatus(302);
        $this->assertEquals('John Doe', $this->user->from_name);
    }

    #[Test]
    public function user_can_update_reply_from_name_to_empty()
    {
        $this->assertNull($this->user->from_name);

        $response = $this->post('/settings/from-name', [
            'from_name' => '',
        ]);

        $response->assertStatus(302);
        $this->assertEquals(null, $this->user->from_name);
    }

    #[Test]
    public function user_can_update_email_subject()
    {
        $this->assertNull($this->user->email_subject);

        $response = $this->post('/settings/email-subject', [
            'email_subject' => 'The subject',
        ]);

        $response->assertStatus(302);
        $this->assertEquals('The subject', $this->user->email_subject);
    }

    #[Test]
    public function user_can_update_email_subject_to_empty()
    {
        $this->assertNull($this->user->email_subject);

        $response = $this->post('/settings/email-subject', [
            'email_subject' => '',
        ]);

        $response->assertStatus(302);
        $this->assertEquals(null, $this->user->email_subject);
    }

    #[Test]
    public function user_can_update_email_banner_location()
    {
        $this->assertEquals('top', $this->user->banner_location);

        $response = $this->post('/settings/banner-location', [
            'banner_location' => 'bottom',
        ]);

        $response->assertStatus(302);
        $this->assertEquals('bottom', $this->user->banner_location);
    }

    #[Test]
    public function user_cannot_update_email_banner_location_to_incorrect_value()
    {
        $this->assertEquals('top', $this->user->banner_location);

        $response = $this->post('/settings/banner-location', [
            'banner_location' => 'side',
        ]);

        $response->assertStatus(302);

        $response->assertSessionHasErrors(['banner_location']);
        $this->assertEquals('top', $this->user->banner_location);
    }

    #[Test]
    public function user_can_enable_use_reply_to()
    {
        $this->assertFalse($this->user->use_reply_to);

        $response = $this->post('/settings/use-reply-to/', [
            'use_reply_to' => true,
        ]);

        $response->assertStatus(302);
        $this->assertTrue($this->user->use_reply_to);
    }

    #[Test]
    public function user_can_disable_use_reply_to()
    {
        $this->user->update(['use_reply_to' => true]);

        $this->assertTrue($this->user->use_reply_to);

        $response = $this->post('/settings/use-reply-to/', [
            'use_reply_to' => false,
        ]);

        $response->assertStatus(302);
        $this->assertFalse($this->user->use_reply_to);
    }

    #[Test]
    public function user_can_enable_store_failed_deliveries()
    {
        $this->user->update(['store_failed_deliveries' => false]);

        $response = $this->post('/settings/store-failed-deliveries/', [
            'store_failed_deliveries' => true,
        ]);

        $response->assertStatus(302);
        $this->assertTrue($this->user->store_failed_deliveries);
    }

    #[Test]
    public function user_can_disable_store_failed_deliveries()
    {
        $this->assertTrue($this->user->store_failed_deliveries);

        $response = $this->post('/settings/store-failed-deliveries/', [
            'store_failed_deliveries' => false,
        ]);

        $response->assertStatus(302);
        $this->assertFalse($this->user->store_failed_deliveries);
    }

    #[Test]
    public function user_can_enable_save_alias_last_used()
    {
        $this->user->update(['save_alias_last_used' => false]);

        $this->assertFalse($this->user->save_alias_last_used);

        $response = $this->post('/settings/save-alias-last-used/', [
            'save_alias_last_used' => true,
        ]);

        $response->assertStatus(302);
        $this->assertTrue($this->user->save_alias_last_used);
    }

    #[Test]
    public function user_can_disable_save_alias_last_used()
    {
        $this->assertTrue($this->user->save_alias_last_used);

        $response = $this->post('/settings/save-alias-last-used/', [
            'save_alias_last_used' => false,
        ]);

        $response->assertStatus(302);
        $this->assertFalse($this->user->save_alias_last_used);
    }

    #[Test]
    public function user_can_generate_new_backup_code()
    {
        $this->user->update([
            'two_factor_backup_code' => bcrypt(Str::random(40)),
        ]);

        $currentBackupCode = $this->user->two_factor_backup_code;

        $response = $this->post('/settings/2fa/new-backup-code/', [
            'current' => 'mypassword',
        ]);

        $response
            ->assertStatus(302)
            ->assertSessionHas('regeneratedBackupCode');

        $this->assertNotEquals($currentBackupCode, $this->user->two_factor_backup_code);
    }

    #[Test]
    public function user_must_enter_current_password_to_generate_new_backup_code()
    {
        $this->user->update([
            'two_factor_backup_code' => bcrypt(Str::random(40)),
        ]);

        $currentBackupCode = $this->user->two_factor_backup_code;

        $response = $this->post('/settings/2fa/new-backup-code/', [
            'current' => 'wrong-password',
        ]);

        $response
            ->assertStatus(302)
            ->assertSessionHasErrors('current')
            ->assertSessionMissing('regeneratedBackupCode');

        $this->assertEquals($currentBackupCode, $this->user->two_factor_backup_code);
    }

    #[Test]
    public function user_can_enable_webauthn_key()
    {
        $key = $this->user->webauthnKeys()->create([
            'name' => 'key',
            'enabled' => false,
            'credentialId' => 'xyz',
            'type' => 'public-key',
            'transports' => [],
            'attestationType' => 'none',
            'trustPath' => '{"type":"Webauthn\\TrustPath\\EmptyTrustPath"}',
            'aaguid' => '00000000-0000-0000-0000-000000000000',
            'credentialPublicKey' => 'xyz',
            'counter' => 0,
        ]);

        $this->assertFalse($key->enabled);

        $response = $this->post('/webauthn/enabled-keys/', [
            'id' => $key->id,
        ]);

        $response->assertStatus(201);
        $this->assertTrue($this->user->webauthnKeys[0]->enabled);
    }

    #[Test]
    public function user_can_disable_webauthn_key()
    {
        $key = $this->user->webauthnKeys()->create([
            'name' => 'key',
            'enabled' => true,
            'credentialId' => 'xyz',
            'type' => 'public-key',
            'transports' => [],
            'attestationType' => 'none',
            'trustPath' => '{"type":"Webauthn\\TrustPath\\EmptyTrustPath"}',
            'aaguid' => '00000000-0000-0000-0000-000000000000',
            'credentialPublicKey' => 'xyz',
            'counter' => 0,
        ]);

        $this->assertTrue($key->enabled);

        $response = $this->post('/webauthn/enabled-keys/'.$key->id, [
            'current' => 'mypassword',
        ]);

        $response->assertStatus(204);
        $this->assertFalse($this->user->webauthnKeys[0]->enabled);
    }

    #[Test]
    public function user_must_enter_correct_password_to_disable_webauthn_key()
    {
        $key = $this->user->webauthnKeys()->create([
            'name' => 'key',
            'enabled' => true,
            'credentialId' => 'xyz',
            'type' => 'public-key',
            'transports' => [],
            'attestationType' => 'none',
            'trustPath' => '{"type":"Webauthn\\TrustPath\\EmptyTrustPath"}',
            'aaguid' => '00000000-0000-0000-0000-000000000000',
            'credentialPublicKey' => 'xyz',
            'counter' => 0,
        ]);

        $this->assertTrue($key->enabled);

        $response = $this->post('/webauthn/enabled-keys/'.$key->id, [
            'current' => 'wrong-password',
        ]);

        $response
            ->assertStatus(302)
            ->assertSessionHasErrors('current');
        $this->assertTrue($this->user->webauthnKeys[0]->enabled);
    }

    #[Test]
    public function user_can_delete_webauthn_key()
    {
        $key = $this->user->webauthnKeys()->create([
            'name' => 'key',
            'enabled' => true,
            'credentialId' => 'xyz',
            'type' => 'public-key',
            'transports' => [],
            'attestationType' => 'none',
            'trustPath' => '{"type":"Webauthn\\TrustPath\\EmptyTrustPath"}',
            'aaguid' => '00000000-0000-0000-0000-000000000000',
            'credentialPublicKey' => 'xyz',
            'counter' => 0,
        ]);

        $response = $this->post('/webauthn/keys/'.$key->id, [
            'current' => 'mypassword',
        ]);

        $response->assertStatus(302);

        $this->assertDatabaseMissing('webauthn_keys', [
            'id' => $key->id,
            'user_id' => $this->user->id,
        ]);
    }

    #[Test]
    public function user_must_enter_correct_password_to_delete_webauthn_key()
    {
        $key = $this->user->webauthnKeys()->create([
            'name' => 'key',
            'enabled' => true,
            'credentialId' => 'xyz',
            'type' => 'public-key',
            'transports' => [],
            'attestationType' => 'none',
            'trustPath' => '{"type":"Webauthn\\TrustPath\\EmptyTrustPath"}',
            'aaguid' => '00000000-0000-0000-0000-000000000000',
            'credentialPublicKey' => 'xyz',
            'counter' => 0,
        ]);

        $response = $this->post('/webauthn/keys/'.$key->id, [
            'current' => 'wrong-password',
        ]);

        $response
            ->assertStatus(302)
            ->assertSessionHasErrors('current');

        $this->assertDatabaseHas('webauthn_keys', [
            'id' => $key->id,
            'user_id' => $this->user->id,
        ]);
    }

    #[Test]
    public function user_can_delete_account()
    {
        $this->assertNotNull($this->user->id);

        if (! Hash::check('mypassword', $this->user->password)) {
            $this->fail('Password does not match');
        }

        $alias = Alias::factory()->create([
            'user_id' => $this->user->id,
        ]);

        $uuidAlias = Alias::factory()->create([
            'user_id' => $this->user->id,
            'domain' => 'anonaddy.me',
            'extension' => 'ext',
            'description' => 'Alias',
            'emails_forwarded' => 10,
            'emails_blocked' => 1,
            'emails_replied' => 2,
            'emails_sent' => 3,
        ]);
        $uuidAlias->update(['local_part' => $uuidAlias->id]);

        $recipient = Recipient::factory()->create([
            'user_id' => $this->user->id,
        ]);

        AliasRecipient::create([
            'alias' => $alias,
            'recipient' => $recipient,
        ]);

        $domain = Domain::factory()->create([
            'user_id' => $this->user->id,
        ]);

        $aliasWithCustomDomain = Alias::factory()->create([
            'user_id' => $this->user->id,
            'aliasable_id' => $domain->id,
            'aliasable_type' => 'App\Models\Domain',
        ]);

        $username = Username::factory()->create([
            'user_id' => $this->user->id,
        ]);

        $aliasWithUsername = Alias::factory()->create([
            'user_id' => $this->user->id,
            'aliasable_id' => $username->id,
            'aliasable_type' => 'App\Models\Username',
        ]);

        $response = $this->post('/settings/account', [
            'password' => 'mypassword',
        ]);

        $response->assertRedirect('/login');

        $this->assertDatabaseMissing('users', [
            'id' => $this->user->id,
            'username' => $this->user->username,
        ]);

        $this->assertDatabaseMissing('alias_recipients', [
            'alias_id' => $alias->id,
            'recipient_id' => $recipient->username,
        ]);

        $this->assertDatabaseMissing('aliases', [
            'id' => $alias->id,
            'user_id' => $this->user->id,
        ]);

        $this->assertDatabaseHas('aliases', [
            'id' => $uuidAlias->id,
            'user_id' => $this->user->id,
            'extension' => null,
            'description' => null,
            'emails_forwarded' => 0,
            'emails_blocked' => 0,
            'emails_replied' => 0,
            'emails_sent' => 0,
            'deleted_at' => now(),
        ]);

        $this->assertDatabaseMissing('aliases', [
            'id' => $aliasWithCustomDomain->id,
            'user_id' => $this->user->id,
            'aliasable_id' => $domain->id,
            'aliasable_type' => 'App\Models\Domain',
        ]);

        $this->assertDatabaseMissing('aliases', [
            'id' => $aliasWithUsername->id,
            'user_id' => $this->user->id,
            'aliasable_id' => $username->id,
            'aliasable_type' => 'App\Models\Username',
        ]);

        $this->assertDatabaseMissing('recipients', [
            'id' => $recipient->id,
            'user_id' => $this->user->id,
        ]);

        $this->assertDatabaseMissing('domains', [
            'id' => $domain->id,
            'user_id' => $this->user->id,
        ]);

        $this->assertDatabaseMissing('usernames', [
            'id' => $username->id,
            'user_id' => $this->user->id,
        ]);

        $this->assertEquals(DeletedUsername::first()->username, $this->user->username);
        $this->assertEquals(DeletedUsername::skip(1)->first()->username, $username->username);
    }

    #[Test]
    public function user_must_enter_correct_password_to_delete_account()
    {
        $this->assertNotNull($this->user->id);

        if (! Hash::check('mypassword', $this->user->password)) {
            $this->fail('Password does not match');
        }

        $response = $this->post('/settings/account', [
            'password' => 'wrongpassword',
        ]);

        $response->assertStatus(302);

        $response->assertSessionHasErrors(['password']);
        $this->assertNull(DeletedUsername::first());

        $this->assertDatabaseHas('users', [
            'id' => $this->user->id,
        ]);

        $this->assertDatabaseHas('usernames', [
            'username' => $this->user->username,
        ]);
    }

    #[Test]
    public function user_can_import_aliases_for_custom_domains()
    {
        Excel::fake();

        Domain::factory()->create([
            'user_id' => $this->user->id,
            'domain' => 'example.com',
        ]);

        $response = $this->actingAs($this->user)
            ->post('/settings/aliases/import', [
                'aliases_import' => new UploadedFile(base_path('tests/files/import-aliases-template.csv'), 'import-aliases-template.csv', 'csv', null, true),
            ]);

        $response->assertStatus(302);

        $response->assertSessionDoesntHaveErrors(['aliases_import']);

        Excel::assertQueued('import-aliases-template.csv', function (AliasesImport $import) {
            return $import->getDomains()->first()->domain === 'example.com' && $import->getRecipientIds()[0] === $this->user->default_recipient_id;
        });
    }

    #[Test]
    public function user_can_download_aliases_export()
    {
        Excel::fake();

        Alias::factory()->count(3)->create([
            'user_id' => $this->user->id,
        ]);

        Alias::factory()->create([
            'user_id' => $this->user->id,
            'deleted_at' => now(),
            'active' => false,
        ]);

        Alias::factory()->create();

        $this->actingAs($this->user)
            ->get('/settings/aliases/export');

        Excel::assertDownloaded('aliases-'.now()->toDateString().'.csv', function (AliasesExport $export) {
            $this->assertCount(4, $export->collection());

            return $export->collection()->contains(function ($alias) {
                return $alias['user_id'] === $this->user->id;
            });
        });
    }
}
