<?php

use App\Models\TaskModel;
use App\Services\NotificationService;
use App\Services\RecurrenceService;
use App\Services\TaskService;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Tieasy;
use Tests\Support\Auth\AuthTestTrait;
use Tests\Support\Categories\CategoryTestTrait;

/**
 * @internal
 */
final class NotificationServiceTest extends CIUnitTestCase
{
    use DatabaseTestTrait;
    use AuthTestTrait;
    use CategoryTestTrait;

    protected $namespace = 'App';
    protected $refresh   = true;
    protected $migrate   = true;
    protected $seed      = '';

    private NotificationService $notes;
    private TaskService $tasks;
    private RecurrenceService $recurrence;

    protected function setUp(): void
    {
        parent::setUp();
        $this->notes      = new NotificationService();
        $this->tasks      = new TaskService();
        $this->recurrence = new RecurrenceService();
    }

    /**
     * @param array<string, string> $override
     *
     * @return array<string, mixed>
     */
    private function makeTask(int $userId, int $categoryId, string $title, array $override = []): array
    {
        $result = $this->tasks->create($userId, $this->tasks->collect(array_merge([
            'title'       => $title,
            'priority'    => '1',
            'category_id' => (string) $categoryId,
            'due_date'    => '2026-08-29',
            'due_time'    => '',
        ], $override)));
        $this->assertTrue($result['ok'] ?? false, (string) ($result['message'] ?? 'create failed'));

        return $result['task'];
    }

    private function occurrenceId(int $taskId, ?string $date = null): int
    {
        $builder = db_connect()->table('task_occurrences')->where('task_id', $taskId);
        if ($date !== null) {
            $builder->where('occurrence_date', $date);
        }
        $row = $builder->get()->getRowArray();
        $this->assertIsArray($row);

        return (int) $row['id'];
    }

    private function generateAt(string $now): int
    {
        $this->notes->nowOverride = $now;

        return $this->notes->generateDueNotifications();
    }

    private function dueCount(int $userId, ?int $occurrenceId = null): int
    {
        $builder = db_connect()->table('notifications')
            ->where('user_id', $userId)
            ->where('kind', NotificationService::KIND_DUE);
        if ($occurrenceId !== null) {
            $builder->where('occurrence_id', $occurrenceId);
        }

        return $builder->countAllResults();
    }

    public function testBadgeFormatting(): void
    {
        $this->assertSame('', NotificationService::formatBadge(0));
        $this->assertSame('3', NotificationService::formatBadge(3));
        $this->assertSame('99', NotificationService::formatBadge(99));
        $this->assertSame('99+', NotificationService::formatBadge(100));
    }

    public function testPriorityLabelsRemainCentral(): void
    {
        $this->assertSame('Not Important & Not Urgent', (new Tieasy())->priorities[4]);
    }

    public function testDateTimeDoesNotNotifyEarlyAndNotifiesWhenDueOrOverdue(): void
    {
        $user = $this->createUser('note-svc-time@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Submit invoice', [
            'due_date' => '2026-08-29',
            'due_time' => '16:30',
        ]);
        $occId = $this->occurrenceId((int) $task['id']);

        $this->assertSame(0, $this->generateAt('2026-08-29 16:29:59'));
        $this->assertSame(0, $this->dueCount((int) $user['id']));

        $this->assertSame(1, $this->generateAt('2026-08-29 16:30:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $occId));
        $row = db_connect()->table('notifications')->where('occurrence_id', $occId)->get()->getRowArray();
        $this->assertSame('Submit invoice is due now.', $row['message'] ?? null);

        $this->assertSame(0, $this->generateAt('2026-08-29 18:00:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $occId));
    }

    public function testOverduePendingIsPickedUpOnLaterRun(): void
    {
        $user = $this->createUser('note-svc-overdue@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Yesterday timed', [
            'due_date' => '2026-08-28',
            'due_time' => '09:00',
        ]);

        $this->assertSame(1, $this->generateAt('2026-08-29 08:00:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $task['id'])));
    }

    public function testDateOnlyDueTodayFutureAndPast(): void
    {
        $user = $this->createUser('note-svc-date@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $today = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Date only today', [
            'due_date' => '2026-08-29',
            'due_time' => '',
        ]);
        $future = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Date only future', [
            'due_date' => '2026-08-30',
            'due_time' => '',
        ]);
        $past = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Date only past', [
            'due_date' => '2026-08-28',
            'due_time' => '',
        ]);

        $this->assertSame(0, $this->generateAt('2026-08-27 23:59:59'));
        $created = $this->generateAt('2026-08-29 00:00:00');
        $this->assertSame(2, $created);
        $this->assertSame(1, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $today['id'])));
        $this->assertSame(0, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $future['id'])));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $past['id'])));

        $todayRow = db_connect()->table('notifications')->where('occurrence_id', $this->occurrenceId((int) $today['id']))->get()->getRowArray();
        $pastRow  = db_connect()->table('notifications')->where('occurrence_id', $this->occurrenceId((int) $past['id']))->get()->getRowArray();
        $this->assertSame('Date only today is due today.', $todayRow['message'] ?? null);
        $this->assertSame('Date only past is overdue.', $pastRow['message'] ?? null);
    }

    public function testUndatedGeneratesNothing(): void
    {
        $user = $this->createUser('note-svc-undated@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'No date', ['due_date' => '', 'due_time' => '']);

        $this->assertSame(0, $this->generateAt('2026-08-29 12:00:00'));
        $this->assertSame(0, $this->dueCount((int) $user['id']));
    }

    public function testCompletedDeletedTaskAndOccurrenceAreSkipped(): void
    {
        $user = $this->createUser('note-svc-skip@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $done = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Completed first', [
            'due_date' => '2026-08-29',
            'due_time' => '10:00',
        ]);
        $this->recurrence->completeOccurrence((int) $user['id'], $this->occurrenceId((int) $done['id']));

        $tomb = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Tombstoned', [
            'due_date' => '2026-08-29',
            'due_time' => '10:00',
        ]);
        db_connect()->table('task_occurrences')->where('id', $this->occurrenceId((int) $tomb['id']))->update(['is_deleted' => 1]);

        $gone = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Soft deleted task', [
            'due_date' => '2026-08-29',
            'due_time' => '10:00',
        ]);
        (new TaskModel())->delete((int) $gone['id']);

        $this->assertSame(0, $this->generateAt('2026-08-29 18:00:00'));
    }

    public function testInactiveUserIsSkipped(): void
    {
        $user = $this->createUser('note-svc-inactive@example.com', 1, 0);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Inactive due', [
            'due_date' => '2026-08-29',
            'due_time' => '',
        ]);

        $this->assertSame(0, $this->generateAt('2026-08-29 12:00:00'));
    }

    public function testForeignUsersGetSeparateNotifications(): void
    {
        $alice = $this->createUser('note-svc-iso-a@example.com', 1);
        $bob   = $this->createUser('note-svc-iso-b@example.com', 1);
        $aCat  = $this->createCategory((int) $alice['id'], 'Personal');
        $bCat  = $this->createCategory((int) $bob['id'], 'Personal');
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Shared title');
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Shared title');

        $this->assertSame(2, $this->generateAt('2026-08-29 12:00:00'));
        $this->assertSame(1, $this->dueCount((int) $alice['id']));
        $this->assertSame(1, $this->dueCount((int) $bob['id']));
        $this->assertSame(1, $this->notes->unreadCount((int) $alice['id']));
        $this->assertSame(1, $this->notes->unreadCount((int) $bob['id']));
    }

    public function testDuplicatePreventionAndReadIsNotRecreated(): void
    {
        $user = $this->createUser('note-svc-dup@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Once only');
        $occ  = $this->occurrenceId((int) $task['id']);

        $this->assertSame(1, $this->generateAt('2026-08-29 12:00:00'));
        $this->assertSame(0, $this->generateAt('2026-08-29 12:00:00'));
        $this->assertSame(0, $this->generateAt('2026-08-30 09:00:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $occ));

        $note = db_connect()->table('notifications')->where('occurrence_id', $occ)->get()->getRowArray();
        $this->assertIsArray($note);
        $this->notes->nowOverride = '2026-08-29 13:00:00';
        $this->notes->markRead((int) $user['id'], (int) $note['id']);
        $this->assertSame(0, $this->generateAt('2026-08-29 14:00:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $occ));
        $this->assertSame(0, $this->notes->unreadCount((int) $user['id']));
    }

    public function testRecurringOccurrencesNotifyIndependently(): void
    {
        $user = $this->createUser('note-svc-recur@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Weekly due', [
            'due_date' => '2026-09-01',
            'due_time' => '',
        ]);
        db_connect()->table('tasks')->where('id', $task['id'])->update(['is_recurring' => 1]);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-08', 'status' => 'pending']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-15', 'status' => 'completed', 'completed_at' => '2026-09-15 09:00:00']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-22', 'status' => 'pending', 'is_deleted' => 1]);

        $this->assertSame(2, $this->generateAt('2026-09-20 12:00:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $task['id'], '2026-09-01')));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $task['id'], '2026-09-08')));
        $this->assertSame(0, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $task['id'], '2026-09-15')));
        $this->assertSame(0, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $task['id'], '2026-09-22')));
    }

    public function testDeleteFuturePreventsLaterGeneration(): void
    {
        $user = $this->createUser('note-svc-future@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Series', [
            'due_date' => '2026-09-01',
        ]);
        db_connect()->table('tasks')->where('id', $task['id'])->update(['is_recurring' => 1]);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-08']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-15']);
        $cutoff = $this->occurrenceId((int) $task['id'], '2026-09-08');
        $result = $this->recurrence->deleteThisAndFuture((int) $user['id'], $cutoff);
        $this->assertTrue($result['ok'] ?? false, (string) ($result['message'] ?? 'delete-future failed'));

        $this->assertSame(1, $this->generateAt('2026-09-20 12:00:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id'], $this->occurrenceId((int) $task['id'], '2026-09-01')));
        $this->assertSame(0, $this->dueCount((int) $user['id'], $cutoff));
    }

    public function testCompleteBeforeGenerationPreventsNotification(): void
    {
        $user = $this->createUser('note-svc-complete@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Finish first', [
            'due_date' => '2026-08-29',
            'due_time' => '16:00',
        ]);
        $this->recurrence->completeOccurrence((int) $user['id'], $this->occurrenceId((int) $task['id']));
        $this->assertSame(0, $this->generateAt('2026-08-29 17:00:00'));
    }

    public function testExistingNotificationRemainsAfterComplete(): void
    {
        $user = $this->createUser('note-svc-after@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Notify then complete');
        $this->assertSame(1, $this->generateAt('2026-08-29 12:00:00'));
        $this->recurrence->completeOccurrence((int) $user['id'], $this->occurrenceId((int) $task['id']));
        $this->assertSame(0, $this->generateAt('2026-08-29 13:00:00'));
        $this->assertSame(1, $this->dueCount((int) $user['id']));
    }

    public function testMarkReadIdempotentAndForeignRejected(): void
    {
        $alice = $this->createUser('note-svc-read-a@example.com', 1);
        $bob   = $this->createUser('note-svc-read-b@example.com', 1);
        $aCat  = $this->createCategory((int) $alice['id'], 'Work');
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Alice due');
        $this->generateAt('2026-08-29 12:00:00');
        $note = db_connect()->table('notifications')->where('user_id', $alice['id'])->get()->getRowArray();
        $this->assertIsArray($note);

        $this->notes->nowOverride = '2026-08-29 12:05:00';
        $first = $this->notes->markRead((int) $alice['id'], (int) $note['id']);
        $this->assertTrue($first['ok'] ?? false);
        $readAt = $first['notification']['read_at'] ?? null;
        $this->assertNotEmpty($readAt);

        $this->notes->nowOverride = '2026-08-29 15:00:00';
        $second = $this->notes->markRead((int) $alice['id'], (int) $note['id']);
        $this->assertTrue($second['ok'] ?? false);
        $this->assertSame($readAt, $second['notification']['read_at'] ?? null);

        $stolen = $this->notes->markRead((int) $bob['id'], (int) $note['id']);
        $this->assertFalse($stolen['ok'] ?? true);
        $this->assertSame(404, $stolen['status'] ?? 0);

        $this->createCategory((int) $bob['id'], 'Work');
        $this->notes->markAllRead((int) $bob['id']);
        $aliceRow = db_connect()->table('notifications')->where('id', $note['id'])->get()->getRowArray();
        $this->assertSame(1, (int) ($aliceRow['is_read'] ?? 0));
    }

    public function testMarkAllReadOnlyTouchesCurrentUser(): void
    {
        $alice = $this->createUser('note-svc-all-a@example.com', 1);
        $bob   = $this->createUser('note-svc-all-b@example.com', 1);
        $aCat  = $this->createCategory((int) $alice['id'], 'Work');
        $bCat  = $this->createCategory((int) $bob['id'], 'Work');
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'A1', ['due_date' => '2026-08-28']);
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'A2', ['due_date' => '2026-08-29']);
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'B1');
        $this->generateAt('2026-08-29 12:00:00');

        $this->notes->nowOverride = '2026-08-29 12:10:00';
        $this->assertSame(2, $this->notes->markAllRead((int) $alice['id']));
        $this->assertSame(0, $this->notes->unreadCount((int) $alice['id']));
        $this->assertSame(1, $this->notes->unreadCount((int) $bob['id']));
        $this->assertSame(0, $this->notes->markAllRead((int) $alice['id']));
    }

    public function testOpenMissingTaskIsGraceful(): void
    {
        $user = $this->createUser('note-svc-gone@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Will vanish');
        $this->generateAt('2026-08-29 12:00:00');
        $note = db_connect()->table('notifications')->where('user_id', $user['id'])->get()->getRowArray();
        $this->assertIsArray($note);
        (new TaskModel())->delete((int) $task['id']);

        $opened = $this->notes->open((int) $user['id'], (int) $note['id']);
        $this->assertTrue($opened['ok'] ?? false);
        $this->assertSame('/notifications', $opened['redirect'] ?? null);
        $this->assertSame(NotificationService::MSG_TASK_GONE, $opened['flash'] ?? null);
        $updated = db_connect()->table('notifications')->where('id', $note['id'])->get()->getRowArray();
        $this->assertSame(1, (int) ($updated['is_read'] ?? 0));
    }

    public function testListNewestFirst(): void
    {
        $user = $this->createUser('note-svc-order@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Older', ['due_date' => '2026-08-27']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Newer', ['due_date' => '2026-08-28']);
        $this->generateAt('2026-08-29 12:00:00');
        db_connect()->table('notifications')->like('message', 'Older', 'after')->update(['created_at' => '2026-08-27 09:00:00']);
        db_connect()->table('notifications')->like('message', 'Newer', 'after')->update(['created_at' => '2026-08-28 09:00:00']);

        $list = $this->notes->listForUser((int) $user['id']);
        $this->assertSame('Newer is overdue.', $list[0]['message'] ?? null);
        $this->assertSame('Older is overdue.', $list[1]['message'] ?? null);
    }

    public function testOpenOwnedTask(): void
    {
        $user = $this->createUser('note-svc-open@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], "Client's invoice");
        $this->generateAt('2026-08-29 12:00:00');
        $note = db_connect()->table('notifications')->where('user_id', $user['id'])->get()->getRowArray();
        $opened = $this->notes->open((int) $user['id'], (int) $note['id']);
        $this->assertSame('/tasks/' . $task['id'] . '/edit', $opened['redirect'] ?? null);
        $this->assertArrayNotHasKey('flash', $opened);
    }
}
