<?php

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

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

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

    /**
     * @param array<string, string> $override
     *
     * @return array<string, mixed>
     */
    private function makeTask(int $userId, int $categoryId, string $title, array $override = []): array
    {
        $result = (new TaskService())->create($userId, (new TaskService())->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 generateAt(string $now): int
    {
        $svc = new NotificationService();
        $svc->nowOverride = $now;

        return $svc->generateDueNotifications();
    }

    public function testUnauthenticatedNotificationsRedirectsToSignIn(): void
    {
        $this->withSession([])->get('notifications')->assertRedirectTo('/sign-in');
        $this->withSession([])->post('notifications/read-all')->assertRedirectTo('/sign-in');
        $this->withSession([])->post('notifications/1/read')->assertRedirectTo('/sign-in');
        $this->withSession([])->post('notifications/1/open')->assertRedirectTo('/sign-in');
    }

    public function testEmptyListAndBellWithoutBadge(): void
    {
        $user = $this->createUser('note-flow-empty@example.com', 1);
        $page = $this->withSession($this->authSession($user))->get('notifications');
        $page->assertOK();
        $page->assertSee(NotificationService::MSG_EMPTY);
        $page->assertSee('Notifications');
        $page->assertDontSee('Mark all as read');
        $page->assertDontSee('In-app notifications ship in Phase 10.');
        $body = (string) $page->response()->getBody();
        $this->assertStringNotContainsString('class="bell-badge"', $body);
        $this->assertStringContainsString('href="' . site_url('notifications') . '"', $body);
    }

    public function testListShowsOwnNewestUnreadAndHidesForeign(): void
    {
        $alice = $this->createUser('note-flow-list-a@example.com', 1);
        $bob   = $this->createUser('note-flow-list-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'], "Client's Work", ['due_date' => '2026-08-27']);
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Newer item', ['due_date' => '2026-08-28']);
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob secret due');
        $this->generateAt('2026-08-29 12:00:00');
        db_connect()->table('notifications')->like('message', 'Client', 'after')->update(['created_at' => '2026-08-27 09:00:00']);
        db_connect()->table('notifications')->like('message', 'Newer', 'after')->update(['created_at' => '2026-08-28 10:00:00']);

        $page = $this->withSession($this->authSession($alice))->get('notifications');
        $page->assertOK();
        $body = (string) $page->response()->getBody();
        $this->assertStringContainsString('Client&#039;s Work is overdue.', $body);
        $this->assertStringContainsString('Newer item is overdue.', $body);
        $page->assertDontSee('Bob secret');
        $page->assertSee('Mark all as read');
        $page->assertSee('Task Due');
        $this->assertStringContainsString('class="bell-badge">2</span>', $body);
        $this->assertTrue(
            strpos($body, 'Newer item is overdue.') < strpos($body, 'Client&#039;s Work is overdue.'),
        );
        $this->assertStringContainsString('notification-row is-unread', $body);
        $this->assertStringContainsString('Client&#039;s Work', $body);
    }

    public function testDashboardBellBadgeIsUserScoped(): void
    {
        $alice = $this->createUser('note-flow-bell-a@example.com', 1);
        $bob   = $this->createUser('note-flow-bell-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'], 'Alice bell');
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob bell 1', ['due_date' => '2026-08-28']);
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob bell 2');
        $this->generateAt('2026-08-29 12:00:00');

        $aliceDash = $this->withSession($this->authSession($alice))->get('dashboard');
        $aliceBody = (string) $aliceDash->response()->getBody();
        $this->assertStringContainsString('class="bell-badge">1</span>', $aliceBody);
        $this->assertStringContainsString((new Tieasy())->priorities[4], html_entity_decode(strip_tags($aliceBody)));

        $bobDash = $this->withSession($this->authSession($bob))->get('dashboard');
        $this->assertStringContainsString('class="bell-badge">2</span>', (string) $bobDash->response()->getBody());
    }

    public function testMarkReadAndMarkAllAndGetDoesNotMutate(): void
    {
        $alice = $this->createUser('note-flow-read-a@example.com', 1);
        $bob   = $this->createUser('note-flow-read-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'], 'Alice one', ['due_date' => '2026-08-27']);
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Alice two', ['due_date' => '2026-08-28']);
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob stays unread');
        $this->generateAt('2026-08-29 12:00:00');
        $aliceNotes = db_connect()->table('notifications')->where('user_id', $alice['id'])->orderBy('id', 'ASC')->get()->getResultArray();
        $bobNote    = db_connect()->table('notifications')->where('user_id', $bob['id'])->get()->getRowArray();
        $this->assertCount(2, $aliceNotes);
        $this->assertIsArray($bobNote);

        $this->expectException(PageNotFoundException::class);
        $this->withSession($this->authSession($alice))->get('notifications/' . $aliceNotes[0]['id'] . '/read');
    }

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

        $read = $this->withSession($this->authSession($alice))->post('notifications/' . $note['id'] . '/read');
        $read->assertRedirectTo('/notifications');
        $row = db_connect()->table('notifications')->where('id', $note['id'])->get()->getRowArray();
        $this->assertSame(1, (int) ($row['is_read'] ?? 0));
        $this->assertNotEmpty($row['read_at'] ?? null);
        $firstReadAt = $row['read_at'];

        $again = $this->withSession($this->authSession($alice))->post('notifications/' . $note['id'] . '/read');
        $again->assertRedirectTo('/notifications');
        $row2 = db_connect()->table('notifications')->where('id', $note['id'])->get()->getRowArray();
        $this->assertSame($firstReadAt, $row2['read_at'] ?? null);

        $this->expectException(PageNotFoundException::class);
        $this->withSession($this->authSession($bob))->post('notifications/' . $note['id'] . '/read');
    }

    public function testMarkAllReadHttp(): void
    {
        $alice = $this->createUser('note-flow-all-a@example.com', 1);
        $bob   = $this->createUser('note-flow-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-27']);
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'A2', ['due_date' => '2026-08-28']);
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob unread');
        $this->generateAt('2026-08-29 12:00:00');

        $this->withSession($this->authSession($alice))->post('notifications/read-all')->assertRedirectTo('/notifications');
        $this->assertSame(0, (new NotificationService())->unreadCount((int) $alice['id']));
        $this->assertSame(1, (new NotificationService())->unreadCount((int) $bob['id']));

        $page = $this->withSession($this->authSession($alice))->get('notifications');
        $page->assertDontSee('Mark all as read');
        $this->assertStringNotContainsString('class="bell-badge"', (string) $page->response()->getBody());
        $this->withSession($this->authSession($alice))->post('notifications/read-all')->assertRedirectTo('/notifications');
    }

    public function testOpenGoesToOwnedTaskAndSetsReturn(): void
    {
        $user = $this->createUser('note-flow-open@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Open me');
        $this->generateAt('2026-08-29 12:00:00');
        $note = db_connect()->table('notifications')->where('user_id', $user['id'])->get()->getRowArray();

        $session = $this->authSession($user);
        $open = $this->withSession($session)->post('notifications/' . $note['id'] . '/open');
        $open->assertRedirectTo('/tasks/' . $task['id'] . '/edit');

        $edit = $this->withSession(array_merge($session, [
            DashboardService::RETURN_KEY => 'notifications',
        ]))->get('tasks/' . $task['id'] . '/edit');
        $edit->assertOK();
        $edit->assertSee('Open me');
        $this->assertStringContainsString(site_url('notifications'), (string) $edit->response()->getBody());
    }

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

        $open = $this->withSession($this->authSession($user))->post('notifications/' . $note['id'] . '/open');
        $open->assertRedirectTo('/notifications');
        $row = db_connect()->table('notifications')->where('id', $note['id'])->get()->getRowArray();
        $this->assertSame(1, (int) ($row['is_read'] ?? 0));
        $this->assertSame(NotificationService::MSG_TASK_GONE, session()->getFlashdata('error'));
    }

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

        $this->expectException(PageNotFoundException::class);
        $this->withSession($this->authSession($bob))->post('notifications/' . $note['id'] . '/open');
    }

    public function testAnalyticsStillUsesCentralPriorityLabels(): void
    {
        $user = $this->createUser('note-flow-pri@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P4', ['priority' => '4']);
        $page = $this->withSession($this->authSession($user))->get('analytics');
        $page->assertOK();
        $this->assertStringContainsString('Not Important &amp; Not Urgent', (string) $page->response()->getBody());
    }
}
