<?php

namespace MintyPHP\Tests\Module\Notifications\Service;

use MintyPHP\Module\Notifications\Repository\NotificationRepository;
use MintyPHP\Module\Notifications\Service\NotificationMessage;
use MintyPHP\Module\Notifications\Service\NotificationService;
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class NotificationServiceTest extends TestCase
{
    private NotificationRepository&MockObject $notifRepo;
    private UserTenantRepositoryInterface&MockObject $utRepo;
    private NotificationService $service;

    protected function setUp(): void
    {
        $this->notifRepo = $this->createMock(NotificationRepository::class);
        $this->utRepo = $this->createMock(UserTenantRepositoryInterface::class);
        $this->service = new NotificationService($this->notifRepo, $this->utRepo);
    }

    public function testCreateForUserInsertsNotification(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('create')
            ->with($this->callback(function (array $data): bool {
                return $data['recipient_user_id'] === 5
                    && $data['tenant_id'] === 1
                    && $data['type'] === 'user.created'
                    && $data['title'] === 'New user: Alice';
            }))
            ->willReturn(42);

        $result = $this->service->createForUser(5, 1, 'user.created', 'New user: Alice', null, 'admin/users/edit/abc', []);

        $this->assertSame(42, $result);
    }

    public function testCreateForUserRejectsMissingFields(): void
    {
        $this->notifRepo->expects($this->never())->method('create');

        $this->assertFalse($this->service->createForUser(0, 1, 'user.created', 'Test', null, null, []));
        $this->assertFalse($this->service->createForUser(1, 1, '', 'Test', null, null, []));
        $this->assertFalse($this->service->createForUser(1, 1, 'user.created', '', null, null, []));
    }

    public function testUnreadCountReturnsCorrectCount(): void
    {
        $this->notifRepo->expects($this->any())->method('countUnreadByUser')->with(3, null)->willReturn(7);

        $this->assertSame(7, $this->service->unreadCount(3));
    }

    public function testUnreadCountReturnsZeroForInvalidUser(): void
    {
        $this->notifRepo->expects($this->never())->method('countUnreadByUser');

        $this->assertSame(0, $this->service->unreadCount(0));
    }

    public function testMarkAsReadUpdatesState(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('markRead')
            ->with(10, 3, null)
            ->willReturn(true);

        $this->assertTrue($this->service->markAsRead(3, 10));
    }

    public function testMarkAsReadRejectsInvalidIds(): void
    {
        $this->notifRepo->expects($this->never())->method('markRead');

        $this->assertFalse($this->service->markAsRead(0, 10));
        $this->assertFalse($this->service->markAsRead(3, 0));
    }

    public function testMarkAllAsReadAffectsAllUserNotifications(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('markAllReadByUser')
            ->with(3, null)
            ->willReturn(5);

        $this->assertSame(5, $this->service->markAllAsRead(3));
    }

    public function testDismissDeletesNotification(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('delete')
            ->with(10, 3, null)
            ->willReturn(true);

        $this->assertTrue($this->service->dismiss(3, 10));
    }

    public function testPurgeOldReadRemovesExpiredOnly(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('purgeReadOlderThanDays')
            ->with(90)
            ->willReturn(12);

        $this->assertSame(12, $this->service->purgeOldRead(90));
    }

    public function testDeleteAllForUserDelegates(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('deleteAllByUser')
            ->with(5)
            ->willReturn(3);

        $this->assertSame(3, $this->service->deleteAllForUser(5));
    }

    public function testListForUserReturnsNotifications(): void
    {
        $expected = [
            ['id' => 1, 'title' => 'Test', 'is_read' => 0],
            ['id' => 2, 'title' => 'Test 2', 'is_read' => 1],
        ];
        $this->notifRepo->expects($this->any())->method('listByUser')->with(3, null, 50, 0)->willReturn($expected);

        $this->assertSame($expected, $this->service->listForUser(3));
    }

    public function testListForUserPassesTenantScope(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('listByUser')
            ->with(3, 9, 25, 0)
            ->willReturn([]);

        $this->assertSame([], $this->service->listForUser(3, 9, 25));
    }

    // --- createForTenantUsers ---

    public function testCreateForTenantUsersCreatesForAllUsersExcludingActor(): void
    {
        $this->utRepo->expects($this->once())
            ->method('listActiveUserIdsByTenantId')
            ->with(1)
            ->willReturn([10, 20, 30]);

        $this->notifRepo->expects($this->exactly(2))
            ->method('create')
            ->willReturn(1);

        $result = $this->service->createForTenantUsers(1, 'system', 'Hello', null, null, [], 20);

        $this->assertSame(2, $result);
    }

    public function testCreateForTenantUsersSkipsSuppressedDuplicates(): void
    {
        $this->utRepo->expects($this->once())
            ->method('listActiveUserIdsByTenantId')
            ->with(1)
            ->willReturn([10, 20, 30]);

        $this->notifRepo->expects($this->exactly(3))
            ->method('create')
            ->willReturnOnConsecutiveCalls(11, false, 13);

        $result = $this->service->createForTenantUsers(1, 'user.activated', 'Activated', null, null, ['user_id' => 99], null);

        $this->assertSame(2, $result);
    }

    public function testCreateForTenantUsersCreatesForAllWhenNoExclude(): void
    {
        $this->utRepo->expects($this->once())
            ->method('listActiveUserIdsByTenantId')
            ->with(1)
            ->willReturn([10, 20]);

        $this->notifRepo->expects($this->exactly(2))
            ->method('create')
            ->willReturn(1);

        $result = $this->service->createForTenantUsers(1, 'system', 'Hello', null, null, [], null);

        $this->assertSame(2, $result);
    }

    public function testCreateForTenantUsersRejectsInvalidInputs(): void
    {
        $this->utRepo->expects($this->never())->method('listActiveUserIdsByTenantId');
        $this->notifRepo->expects($this->never())->method('create');

        $this->assertSame(0, $this->service->createForTenantUsers(0, 'system', 'Hello', null, null, [], null));
        $this->assertSame(0, $this->service->createForTenantUsers(1, '', 'Hello', null, null, [], null));
        $this->assertSame(0, $this->service->createForTenantUsers(1, 'system', '', null, null, [], null));
    }

    // --- createForTenantAdminUsers ---

    public function testCreateForTenantAdminUsersFiltersToAllowedUsers(): void
    {
        $this->utRepo->expects($this->once())
            ->method('listActiveUserIdsByTenantId')
            ->with(1)
            ->willReturn([10, 20, 30, 40]);

        $this->notifRepo->expects($this->exactly(2))
            ->method('create')
            ->willReturn(1);

        $allowedUserIds = array_flip([20, 30]);
        $result = $this->service->createForTenantAdminUsers(1, 'system', 'Hello', null, null, [], null, $allowedUserIds);

        $this->assertSame(2, $result);
    }

    public function testCreateForTenantAdminUsersExcludesActor(): void
    {
        $this->utRepo->expects($this->once())
            ->method('listActiveUserIdsByTenantId')
            ->with(1)
            ->willReturn([10, 20, 30]);

        $this->notifRepo->expects($this->once())
            ->method('create')
            ->willReturn(1);

        $allowedUserIds = array_flip([20, 30]);
        $result = $this->service->createForTenantAdminUsers(1, 'system', 'Hello', null, null, [], 20, $allowedUserIds);

        $this->assertSame(1, $result);
    }

    public function testCreateForTenantAdminUsersRejectsEmptyAllowedUsers(): void
    {
        $this->utRepo->expects($this->never())->method('listActiveUserIdsByTenantId');
        $this->notifRepo->expects($this->never())->method('create');

        $this->assertSame(0, $this->service->createForTenantAdminUsers(1, 'system', 'Hello', null, null, [], null, []));
    }

    public function testCreateForTenantAdminUsersRejectsInvalidInputs(): void
    {
        $this->utRepo->expects($this->never())->method('listActiveUserIdsByTenantId');
        $this->notifRepo->expects($this->never())->method('create');

        $allowed = array_flip([10]);
        $this->assertSame(0, $this->service->createForTenantAdminUsers(0, 'system', 'Hello', null, null, [], null, $allowed));
        $this->assertSame(0, $this->service->createForTenantAdminUsers(1, '', 'Hello', null, null, [], null, $allowed));
        $this->assertSame(0, $this->service->createForTenantAdminUsers(1, 'system', '', null, null, [], null, $allowed));
    }

    public function testCreateForUserAddsDedupeMetadataForCoreTypes(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('create')
            ->with($this->callback(function (array $data): bool {
                return $data['type'] === 'user.deleted'
                    && !empty($data['dedupe_fingerprint'])
                    && !empty($data['dedupe_bucket']);
            }))
            ->willReturn(55);

        $result = $this->service->createForUser(5, 1, 'user.deleted', 'User deleted: Bob', null, null, ['user_id' => 33]);
        $this->assertSame(55, $result);
    }

    public function testCreateForUserSkipsDedupeMetadataForNonCoreTypes(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('create')
            ->with($this->callback(function (array $data): bool {
                return !isset($data['dedupe_fingerprint']);
            }))
            ->willReturn(56);

        $result = $this->service->createForUser(5, 1, 'system', 'System', null, null, ['user_id' => 33]);
        $this->assertSame(56, $result);
    }

    public function testCreateFromMessagePersistsLocalizationMetadata(): void
    {
        $message = NotificationMessage::localized(
            'user.created',
            'New user: %s',
            ['Alice'],
            null,
            [],
            'admin/users/edit/abc',
            ['user_id' => 33]
        );

        $this->notifRepo->expects($this->once())
            ->method('create')
            ->with($this->callback(function (array $data): bool {
                if (!isset($data['data']['__message']) || !is_array($data['data']['__message'])) {
                    return false;
                }

                return $data['type'] === 'user.created'
                    && $data['data']['__message']['title_key'] === 'New user: %s'
                    && ($data['data']['__message']['title_params'] ?? []) === ['Alice']
                    && $data['data']['user_id'] === 33;
            }))
            ->willReturn(57);

        $result = $this->service->createFromMessage(5, 1, $message);
        $this->assertSame(57, $result);
    }

    public function testCreateForTenantUsersFromMessageCreatesForAllEligibleUsers(): void
    {
        $message = NotificationMessage::plain('system', 'Hello', null, null, []);

        $this->utRepo->expects($this->once())
            ->method('listActiveUserIdsByTenantId')
            ->with(1)
            ->willReturn([10, 20, 30]);

        $this->notifRepo->expects($this->exactly(2))
            ->method('create')
            ->willReturn(1);

        $result = $this->service->createForTenantUsersFromMessage(1, $message, 20);
        $this->assertSame(2, $result);
    }

    public function testCreateForTenantAdminUsersFromMessageRejectsEmptyAllowedSet(): void
    {
        $message = NotificationMessage::plain('system', 'Hello', null, null, []);

        $this->utRepo->expects($this->never())->method('listActiveUserIdsByTenantId');
        $this->notifRepo->expects($this->never())->method('create');

        $result = $this->service->createForTenantAdminUsersFromMessage(1, $message, null, []);
        $this->assertSame(0, $result);
    }

    public function testListForUserLocalizesFromStoredMessageMetadata(): void
    {
        $this->notifRepo->expects($this->once())
            ->method('listByUser')
            ->with(3, null, 50, 0)
            ->willReturn([
                [
                    'id' => 1,
                    'type' => 'user.created',
                    'title' => 'fallback',
                    'body' => null,
                    'link' => null,
                    'data' => json_encode([
                        'user_id' => 10,
                        '__message' => [
                            'title_key' => 'New user: %s',
                            'title_params' => ['Alice'],
                            'body_key' => '',
                            'body_params' => [],
                        ],
                    ], JSON_THROW_ON_ERROR),
                    'is_read' => 0,
                    'created' => '2026-03-19 10:00:00',
                ],
            ]);

        $result = $this->service->listForUser(3);

        $this->assertCount(1, $result);
        $this->assertSame(t('New user: %s', 'Alice'), $result[0]['title']);
        $this->assertSame(['user_id' => 10], $result[0]['data']);
    }
}
