<?php

declare(strict_types=1);

namespace Tests\Unit\Service;

use App\Enums\Role;
use App\Events\BeforeOrganizationDeletion;
use App\Exceptions\Api\CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers;
use App\Models\Client;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\Report;
use App\Models\Tag;
use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User;
use App\Service\DeletionService;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\CoversClass;
use Tests\TestCaseWithDatabase;
use TiMacDonald\Log\LogEntry;

#[CoversClass(DeletionService::class)]
class DeletionServiceTest extends TestCaseWithDatabase
{
    private DeletionService $deletionService;

    protected function setUp(): void
    {
        parent::setUp();
        Event::fake([
            BeforeOrganizationDeletion::class,
        ]);
        $this->deletionService = app(DeletionService::class);
    }

    /**
     * Creates an organization with all relations.
     * It is important that every relation has at least two entries, to test for possible lazy loading issues.
     *
     * @return object{
     *     organization: Organization,
     *     clients: Collection<Client>,
     *     projects: Collection<Project>,
     *     projectMembers: Collection<ProjectMember>,
     *     tags: Collection<Tag>,
     *     members: Collection<Member>,
     *     tasks: Collection<Task>,
     *     timeEntries: Collection<TimeEntry>,
     *     owner: User,
     *     reports: Collection<Report>
     * }
     */
    private function createOrganizationWithAllRelations(): object
    {
        $userOwner = User::factory()->create();
        $userEmployee = User::factory()->withProfilePicture()->create();
        $userPlaceholder = User::factory()->placeholder()->create();

        $organization = Organization::factory()->withOwner($userOwner)->create();

        // Create a personal organization for the employee
        $personalOrganizationOfEmployee = Organization::factory()->withOwner($userEmployee)->create();
        $personalOrganizationMember = Member::factory()->forUser($userEmployee)->forOrganization($personalOrganizationOfEmployee)->create();

        // Set the current organizations for the users
        $userOwner->update(['current_team_id' => $organization->id]);
        $userEmployee->update(['current_team_id' => $personalOrganizationOfEmployee->id]);
        $userPlaceholder->update(['current_team_id' => null]);

        $memberOwner = Member::factory()->forUser($userOwner)->forOrganization($organization)->role(Role::Owner)->create();
        $memberEmployee = Member::factory()->forUser($userEmployee)->forOrganization($organization)->role(Role::Employee)->create();
        $memberPlaceholder = Member::factory()->forUser($userPlaceholder)->forOrganization($organization)->role(Role::Placeholder)->create();
        $members = collect([$memberOwner, $memberEmployee, $memberPlaceholder]);

        $clients = Client::factory()->forOrganization($organization)->createMany(2);

        $projectWithClient = Project::factory()->forClient($clients->get(0))->forOrganization($organization)->create();
        $projectWithoutClient = Project::factory()->forOrganization($organization)->create();
        $projects = collect([$projectWithClient, $projectWithoutClient]);

        $projectMemberOwner = ProjectMember::factory()->forMember($memberOwner)->forProject($projectWithClient)->create();
        $projectMemberEmployee = ProjectMember::factory()->forMember($memberEmployee)->forProject($projectWithClient)->create();
        $projectMembers = collect([$projectMemberOwner, $projectMemberEmployee]);

        $tags = Tag::factory()->forOrganization($organization)->createMany(2);

        $task1 = Task::factory()->forProject($projectWithClient)->forOrganization($organization)->create();
        $task2 = Task::factory()->forProject($projectWithoutClient)->forOrganization($organization)->create();
        $tasks = collect([$task1, $task2]);

        $report1 = Report::factory()->forOrganization($organization)->create();
        $report2 = Report::factory()->forOrganization($organization)->create();
        $reports = collect([$report1, $report2]);

        $timeEntries = TimeEntry::factory()->forOrganization($organization)->forMember($memberOwner)->createMany(2);
        $timeEntriesWithTask = TimeEntry::factory()->forTask($task1)->forOrganization($organization)->forMember($memberEmployee)->createMany(2);
        $timeEntriesWithProject = TimeEntry::factory()->forProject($projectWithClient)->forOrganization($organization)->forMember($memberPlaceholder)->createMany(2);
        $timeEntries = $timeEntries->merge($timeEntriesWithTask)->merge($timeEntriesWithProject);

        return (object) [
            'organization' => $organization,
            'clients' => $clients,
            'projects' => $projects,
            'projectMembers' => $projectMembers,
            'tags' => $tags,
            'members' => $members,
            'tasks' => $tasks,
            'timeEntries' => $timeEntries,
            'owner' => $userOwner,
            'reports' => $reports,
        ];
    }

    private function assertOrganizationDeleted(Organization $organization): void
    {
        Event::assertDispatched(function (BeforeOrganizationDeletion $event) use ($organization) {
            return $event->organization->is($organization);
        }, 1);
        $this->assertSame(0, Organization::query()->where('id', $organization->id)->count());
        $this->assertSame(0, Client::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(0, Project::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(0, ProjectMember::query()->whereBelongsToOrganization($organization)->count());
        $this->assertSame(0, Tag::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(0, Member::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(0, Task::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(0, Report::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(0, TimeEntry::query()->whereBelongsTo($organization, 'organization')->count());
    }

    private function assertOrganizationNothingDeleted(Organization $organization, bool $specialCase = false): void
    {
        $this->assertSame(1, Organization::query()->where('id', $organization->id)->count());
        $this->assertSame(2, Client::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(2, Project::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(2, ProjectMember::query()->whereBelongsToOrganization($organization)->count());
        $this->assertSame(2, Tag::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(3, Member::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(2, Task::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame(2, Report::query()->whereBelongsTo($organization, 'organization')->count());
        $this->assertSame($specialCase ? 7 : 6, TimeEntry::query()->whereBelongsTo($organization, 'organization')->count());
    }

    public function test_delete_organization_resets_the_current_organization_of_users_that_had_the_deleted_organization_as_current_organization(): void
    {
        // Arrange
        $userOwner = User::factory()->create();
        $organization = Organization::factory()->withOwner($userOwner)->create();
        $userOwner->currentOrganization()->associate($organization);
        $userOwner->save();

        // Act
        $this->deletionService->deleteOrganization($organization);

        // Assert
        $this->assertOrganizationDeleted($organization);
        $userOwner->refresh();
        $this->assertNull($userOwner->current_team_id);
        $this->assertNotSame($organization->id, $userOwner->current_team_id);
    }

    public function test_delete_organization_deletes_all_resources_of_the_organization_but_does_not_delete_other_resources(): void
    {
        // Arrange
        $organization = $this->createOrganizationWithAllRelations();
        $otherOrganization = $this->createOrganizationWithAllRelations();

        // Act
        $this->deletionService->deleteOrganization($organization->organization);

        // Assert
        $this->assertOrganizationDeleted($organization->organization);
        $this->assertOrganizationNothingDeleted($otherOrganization->organization);
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Start deleting organization'
            && $log->context['organization_id'] === $organization->organization->getKey(),
            1
        );
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Finished deleting organization'
            && $log->context['organization_id'] === $organization->organization->getKey(),
            1
        );
    }

    public function test_delete_organization_rolls_back_on_error_if_transaction_is_active(): void
    {
        // Arrange
        $organization = $this->createOrganizationWithAllRelations();
        $otherOrganization = $this->createOrganizationWithAllRelations();
        $brokenTimeEntry = TimeEntry::factory()->forOrganization($otherOrganization->organization)->forProject($organization->projects->get(0))->create();

        // Act
        try {
            $this->deletionService->deleteOrganization($organization->organization);
            $this->fail();
        } catch (QueryException) {
            $this->assertTrue(true);
        }

        // Assert
        Event::assertNotDispatched(function (BeforeOrganizationDeletion $event) use ($otherOrganization): bool {
            return $event->organization->is($otherOrganization->organization);
        });
        Event::assertDispatched(function (BeforeOrganizationDeletion $event) use ($organization): bool {
            return $event->organization->is($organization->organization);
        }, 1);
        $this->assertOrganizationNothingDeleted($organization->organization);
        $this->assertOrganizationNothingDeleted($otherOrganization->organization, true);
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Start deleting organization'
            && $log->context['organization_id'] === $organization->organization->getKey(),
            1
        );
        Log::assertNotLogged(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Finished deleting organization'
            && $log->context['organization_id'] === $organization->organization->getKey()
        );
    }

    public function test_delete_user_fails_if_user_is_owner_of_an_organization_with_multiple_members(): void
    {
        // Arrange
        $organization = $this->createOrganizationWithAllRelations();
        $memberOwner = $organization->owner;

        // Act
        try {
            $this->deletionService->deleteUser($memberOwner);
            $this->fail();
        } catch (CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers $exception) {
            // Assert
            $this->assertTrue(true);
        }
    }

    public function test_delete_user_rolls_back_on_error_if_transaction_is_active(): void
    {
        // Arrange
        $user = User::factory()->create();
        $organization = Organization::factory()->create();
        $memberOwner = Member::factory()->forUser($user)->forOrganization($organization)->role(Role::Owner)->create();
        $otherOrganization = Organization::factory()->create();

        $brokenTimeEntry = TimeEntry::factory()->forMember($memberOwner)->forOrganization($otherOrganization)->create();

        // Act
        try {
            $this->deletionService->deleteUser($user);
            $this->fail();
        } catch (QueryException) {
            $this->assertTrue(true);
        }

        // Assert
        $this->assertDatabaseHas(User::class, [
            'id' => $user->getKey(),
        ]);
        $this->assertDatabaseHas(Organization::class, [
            'id' => $organization->getKey(),
        ]);
        $this->assertDatabaseHas(Member::class, [
            'id' => $memberOwner->getKey(),
        ]);
        $this->assertDatabaseHas(TimeEntry::class, [
            'id' => $brokenTimeEntry->getKey(),
        ]);
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Start deleting user'
            && $log->context['id'] === $user->getKey(),
            1
        );
        Log::assertNotLogged(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Finished deleting user'
            && $log->context['id'] === $user->getKey()
        );
    }

    public function test_delete_user_deletes_all_resources_of_the_user_but_does_not_delete_other_resources(): void
    {
        // Arrange
        $this->mockPublicStorage();
        $user = User::factory()->withProfilePicture()->withPersonalOrganization()->create();
        $otherUser = User::factory()->withProfilePicture()->withPersonalOrganization()->create();
        Storage::disk(config('filesystems.public'))->assertExists($user->profile_photo_path);
        Storage::disk(config('filesystems.public'))->assertExists($otherUser->profile_photo_path);

        // Act
        $this->deletionService->deleteUser($user);

        // Assert
        $this->assertDatabaseMissing(User::class, [
            'id' => $user->getKey(),
        ]);
        $this->assertDatabaseHas(User::class, [
            'id' => $otherUser->getKey(),
        ]);
        $this->assertDatabaseMissing(Organization::class, [
            'id' => $user->current_team_id,
        ]);
        $this->assertDatabaseHas(Organization::class, [
            'id' => $otherUser->current_team_id,
        ]);
        $this->assertDatabaseHas(Member::class, [
            'user_id' => $otherUser->getKey(),
        ]);
        $this->assertDatabaseMissing(Member::class, [
            'user_id' => $user->getKey(),
        ]);
        Storage::disk(config('filesystems.public'))->assertMissing($user->profile_photo_path);
        Storage::disk(config('filesystems.public'))->assertExists($otherUser->profile_photo_path);
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Start deleting user'
            && $log->context['id'] === $user->getKey(),
            1
        );
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Finished deleting user'
            && $log->context['id'] === $user->getKey(),
            1
        );
    }

    public function test_delete_user_deletes_owned_organizations_that_have_only_one_member_and_makes_makes_the_user_placeholder_in_not_owned_organizations(): void
    {
        // Arrange
        $user = User::factory()->create();
        $otherUser = User::factory()->create();
        $organizationOwned = Organization::factory()->withOwner($user)->create();
        $organizationNotOwned = Organization::factory()->withOwner($otherUser)->create();
        $memberOwned = Member::factory()->forUser($user)->forOrganization($organizationOwned)->role(Role::Owner)->create();
        $memberNotOwned = Member::factory()->forUser($user)->forOrganization($organizationNotOwned)->role(Role::Employee)->create();
        TimeEntry::factory()->forOrganization($organizationOwned)->forMember($memberOwned)->createMany(2);
        TimeEntry::factory()->forOrganization($organizationNotOwned)->forMember($memberNotOwned)->createMany(2);
        $this->assertDatabaseCount(User::class, 2);

        // Act
        $this->deletionService->deleteUser($user);

        // Assert
        $this->assertDatabaseCount(Organization::class, 1);
        $this->assertDatabaseCount(User::class, 2);
        $this->assertDatabaseMissing(User::class, [
            'id' => $user->getKey(),
        ]);
        $this->assertDatabaseHas(User::class, [
            'id' => $otherUser->getKey(),
            'is_placeholder' => false,
        ]);
        $this->assertDatabaseHas(User::class, [
            'is_placeholder' => true,
        ]);
        $this->assertDatabaseMissing(Organization::class, [
            'id' => $organizationOwned->getKey(),
        ]);
        $this->assertDatabaseHas(Organization::class, [
            'id' => $organizationNotOwned->getKey(),
        ]);
        $this->assertDatabaseMissing(Member::class, [
            'id' => $memberOwned->getKey(),
        ]);
        $this->assertDatabaseHas(Member::class, [
            'id' => $memberNotOwned->getKey(),
            'organization_id' => $organizationNotOwned->getKey(),
            'role' => Role::Placeholder->value,
        ]);
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Start deleting user'
            && $log->context['id'] === $user->getKey(),
            1
        );
        Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
            && $log->message === 'Finished deleting user'
            && $log->context['id'] === $user->getKey(),
            1
        );
    }

    public function test_delete_user_with_current_organization_set_to_owned_org_that_will_be_deleted_does_not_cause_foreign_key_violation(): void
    {
        // Arrange
        // User A creates an organization and invites User B
        $userA = User::factory()->create();
        $userB = User::factory()->create();
        $organizationOfA = Organization::factory()->withOwner($userA)->create();
        $organizationOfB = Organization::factory()->withOwner($userB)->create();
        Member::factory()->forUser($userA)->forOrganization($organizationOfA)->role(Role::Owner)->create();
        Member::factory()->forUser($userB)->forOrganization($organizationOfB)->role(Role::Owner)->create();
        $memberBInOrgA = Member::factory()->forUser($userB)->forOrganization($organizationOfA)->role(Role::Employee)->create();
        TimeEntry::factory()->forOrganization($organizationOfA)->forMember($memberBInOrgA)->createMany(2);

        // User B's current_organization_id points to their own org (the one that will be deleted)
        $userB->update(['current_team_id' => $organizationOfB->getKey()]);

        // Act
        $this->deletionService->deleteUser($userB);

        // Assert
        $this->assertDatabaseMissing(User::class, [
            'id' => $userB->getKey(),
        ]);
        $this->assertDatabaseMissing(Organization::class, [
            'id' => $organizationOfB->getKey(),
        ]);
        $this->assertDatabaseHas(Organization::class, [
            'id' => $organizationOfA->getKey(),
        ]);
        // The placeholder user should exist with current_team_id set to the org where they are a placeholder
        $placeholderUser = User::query()->where('is_placeholder', true)->first();
        $this->assertNotNull($placeholderUser);
        $this->assertSame($organizationOfA->getKey(), $placeholderUser->current_team_id);
        $this->assertDatabaseHas(Member::class, [
            'id' => $memberBInOrgA->getKey(),
            'user_id' => $placeholderUser->getKey(),
            'organization_id' => $organizationOfA->getKey(),
            'role' => Role::Placeholder->value,
        ]);
    }
}
