<?php

use App\Services\DashboardService;
use App\Services\RecurrenceService;
use App\Services\TaskService;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Tests\Support\Auth\AuthTestTrait;
use Tests\Support\Categories\CategoryTestTrait;

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

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

    private TaskService $tasks;
    private RecurrenceService $recurrence;
    private DashboardService $dash;

    protected function setUp(): void
    {
        parent::setUp();
        $this->tasks      = new TaskService();
        $this->recurrence = new RecurrenceService();
        $this->dash       = new DashboardService();
        $this->dash->todayOverride = '2026-08-29';
    }

    /**
     * @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'));
        $this->assertIsArray($result['task'] ?? null);

        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'];
    }

    /**
     * @param array<string, string> $override
     *
     * @return array{date: string, status: string, category: string, from: string, to: string}
     */
    private function filters(array $override = []): array
    {
        return array_merge($this->dash->defaults(), $override);
    }

    /**
     * @return list<string>
     */
    private function titles(array $rows): array
    {
        return array_values(array_map(static fn (array $row): string => (string) $row['title'], $rows));
    }

    public function testDefaultsAreTodayPendingAllCategories(): void
    {
        $defaults = $this->dash->defaults();
        $this->assertSame(DashboardService::DATE_TODAY, $defaults['date']);
        $this->assertSame(DashboardService::STATUS_PENDING, $defaults['status']);
        $this->assertSame('all', $defaults['category']);
        $this->assertNotSame(DashboardService::DATE_ALL, $defaults['date']);
    }

    public function testDateBoundsMatchSpec(): void
    {
        $this->assertSame(['2026-08-29', '2026-08-29'], $this->dash->dateBounds($this->filters()));
        $this->assertSame(['2026-08-30', '2026-08-30'], $this->dash->dateBounds($this->filters(['date' => 'tomorrow'])));
        $this->assertSame(['2026-08-24', '2026-08-30'], $this->dash->dateBounds($this->filters(['date' => 'week'])));
        $this->assertSame(['2026-08-29', '2026-09-04'], $this->dash->dateBounds($this->filters(['date' => 'next7'])));
        $this->assertSame(['2026-08-01', '2026-08-31'], $this->dash->dateBounds($this->filters(['date' => 'month'])));
        $this->assertSame([null, null], $this->dash->dateBounds($this->filters(['date' => 'all'])));
        $this->assertSame(['2026-08-10', '2026-08-12'], $this->dash->dateBounds($this->filters([
            'date' => 'custom',
            'from' => '2026-08-10',
            'to'   => '2026-08-12',
        ])));
    }

    public function testResolveRejectsInvalidCustomRangeAndForeignCategory(): void
    {
        $user = $this->createUser('dash-svc-alice@example.com', 1);
        $bob  = $this->createUser('dash-svc-bob@example.com', 1);
        $bobCat = $this->createCategory((int) $bob['id'], 'Secret');

        $invalid = $this->dash->resolve((int) $user['id'], [
            'date' => 'custom',
            'from' => '2026-08-10',
            'to'   => '2026-08-01',
        ], null);
        $this->assertSame(DashboardService::MSG_CUSTOM_ORDER, $invalid['error']);
        $this->assertSame('today', $invalid['filters']['date']);

        $badDates = $this->dash->resolve((int) $user['id'], [
            'date' => 'custom',
            'from' => 'not-a-date',
            'to'   => '2026-08-01',
        ], null);
        $this->assertSame(DashboardService::MSG_CUSTOM_INVALID, $badDates['error']);

        $foreign = $this->dash->resolve((int) $user['id'], [
            'category' => (string) $bobCat['id'],
        ], null);
        $this->assertNull($foreign['error']);
        $this->assertSame('all', $foreign['filters']['category']);
        $this->assertSame('today', $foreign['filters']['date']);
        $this->assertSame('pending', $foreign['filters']['status']);
    }

    public function testFormatMetaOmitsMissingPieces(): void
    {
        $this->assertSame(
            'Personal • 29 Aug 2026 • 4:30 PM',
            $this->dash->formatMeta([
                'category_name'    => 'Personal',
                'occurrence_date'  => '2026-08-29',
                'occurrence_time'  => '16:30:00',
            ]),
        );
        $this->assertSame(
            'Personal • 29 Aug 2026',
            $this->dash->formatMeta([
                'category_name'    => 'Personal',
                'occurrence_date'  => '2026-08-29',
                'occurrence_time'  => null,
            ]),
        );
        $this->assertSame('Personal', $this->dash->formatMeta([
            'category_name'   => 'Personal',
            'occurrence_date' => null,
            'occurrence_time' => '16:30:00',
        ]));
        $this->assertSame('', $this->dash->formatMeta([
            'category_name'   => '',
            'occurrence_date' => null,
            'occurrence_time' => null,
        ]));
        $this->assertStringNotContainsString('• •', $this->dash->formatMeta([
            'category_name'   => 'Personal',
            'occurrence_date' => null,
            'occurrence_time' => null,
        ]));
    }

    public function testEmptyMessages(): void
    {
        $this->assertSame('No tasks due today.', $this->dash->emptyMessage($this->filters()));
        $this->assertSame('No pending tasks match these filters.', $this->dash->emptyMessage($this->filters(['date' => 'tomorrow'])));
        $this->assertSame('No completed tasks match these filters.', $this->dash->emptyMessage($this->filters(['status' => 'completed'])));
        $this->assertSame('No tasks match these filters.', $this->dash->emptyMessage($this->filters(['status' => 'all', 'date' => 'all'])));
    }

    public function testTodayExcludesUndatedAndOtherDays(): void
    {
        $user = $this->createUser('dash-svc-today@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Due today');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Due tomorrow', ['due_date' => '2026-08-30']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Undated', ['due_date' => '']);

        $titles = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters()));
        $this->assertSame(['Due today'], $titles);

        $all = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'all'])));
        $this->assertContains('Due today', $all);
        $this->assertContains('Due tomorrow', $all);
        $this->assertContains('Undated', $all);
    }

    public function testTomorrowThisWeekNext7ThisMonthAndCustom(): void
    {
        $user = $this->createUser('dash-svc-dates@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Mon', ['due_date' => '2026-08-24']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Sat today', ['due_date' => '2026-08-29']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Sun', ['due_date' => '2026-08-30']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Plus6', ['due_date' => '2026-09-04']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Plus7', ['due_date' => '2026-09-05']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'PrevSun', ['due_date' => '2026-08-23']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Jul', ['due_date' => '2026-07-31']);

        $this->assertSame(['Sun'], $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'tomorrow']))));

        $week = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'week'])));
        $this->assertContains('Mon', $week);
        $this->assertContains('Sat today', $week);
        $this->assertContains('Sun', $week);
        $this->assertNotContains('PrevSun', $week);
        $this->assertNotContains('Plus6', $week);

        $next7 = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'next7'])));
        $this->assertContains('Sat today', $next7);
        $this->assertContains('Sun', $next7);
        $this->assertContains('Plus6', $next7);
        $this->assertNotContains('Plus7', $next7);
        $this->assertNotContains('Mon', $next7);

        $month = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'month'])));
        $this->assertContains('Mon', $month);
        $this->assertContains('Sat today', $month);
        $this->assertNotContains('Jul', $month);
        $this->assertNotContains('Plus6', $month);

        $custom = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters([
            'date' => 'custom',
            'from' => '2026-08-30',
            'to'   => '2026-08-30',
        ])));
        $this->assertSame(['Sun'], $custom);
    }

    public function testStatusAndDeletedOccurrenceExclusion(): void
    {
        $user = $this->createUser('dash-svc-status@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $pending = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Still pending');
        $done    = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Already done');
        $gone    = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Tombstoned');
        $this->recurrence->completeOccurrence((int) $user['id'], $this->occurrenceId((int) $done['id']));
        db_connect()->table('task_occurrences')->where('id', $this->occurrenceId((int) $gone['id']))->update(['is_deleted' => 1]);

        $pendingTitles = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'all'])));
        $this->assertSame(['Still pending'], $pendingTitles);

        $completed = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'all', 'status' => 'completed'])));
        $this->assertSame(['Already done'], $completed);

        $all = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'all', 'status' => 'all'])));
        $this->assertContains('Still pending', $all);
        $this->assertContains('Already done', $all);
        $this->assertNotContains('Tombstoned', $all);
    }

    public function testCategoryFilterAndIsolation(): void
    {
        $alice = $this->createUser('dash-svc-cat-a@example.com', 1);
        $bob   = $this->createUser('dash-svc-cat-b@example.com', 1);
        $work  = $this->createCategory((int) $alice['id'], 'Work');
        $home  = $this->createCategory((int) $alice['id'], 'Home');
        $bobCat = $this->createCategory((int) $bob['id'], 'BobCat');
        $this->makeTask((int) $alice['id'], (int) $work['id'], 'Alice work');
        $this->makeTask((int) $alice['id'], (int) $home['id'], 'Alice home');
        $this->makeTask((int) $bob['id'], (int) $bobCat['id'], 'Bob secret');

        $workOnly = $this->titles($this->dash->occurrences((int) $alice['id'], $this->filters([
            'date'     => 'all',
            'category' => (string) $work['id'],
        ])));
        $this->assertSame(['Alice work'], $workOnly);

        $aliceAll = $this->titles($this->dash->occurrences((int) $alice['id'], $this->filters(['date' => 'all'])));
        $this->assertContains('Alice work', $aliceAll);
        $this->assertContains('Alice home', $aliceAll);
        $this->assertNotContains('Bob secret', $aliceAll);

        $soft = $this->makeTask((int) $alice['id'], (int) $work['id'], 'Soft deleted');
        (new \App\Models\TaskModel())->delete((int) $soft['id']);
        $afterSoft = $this->titles($this->dash->occurrences((int) $alice['id'], $this->filters(['date' => 'all'])));
        $this->assertNotContains('Soft deleted', $afterSoft);
    }

    public function testCombinedFiltersAndPriorityGrouping(): void
    {
        $user = $this->createUser('dash-svc-combo@example.com', 1);
        $a    = $this->createCategory((int) $user['id'], 'CatA');
        $b    = $this->createCategory((int) $user['id'], 'CatB');
        $this->makeTask((int) $user['id'], (int) $a['id'], 'Today pending A p1', ['priority' => '1']);
        $this->makeTask((int) $user['id'], (int) $a['id'], 'Today pending A p2', ['priority' => '2']);
        $this->makeTask((int) $user['id'], (int) $b['id'], 'Tomorrow all B', ['due_date' => '2026-08-30', 'priority' => '3']);
        $done = $this->makeTask((int) $user['id'], (int) $a['id'], 'Completed A', ['priority' => '4']);
        $this->recurrence->completeOccurrence((int) $user['id'], $this->occurrenceId((int) $done['id']));

        $combo = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters([
            'date'     => 'today',
            'status'   => 'pending',
            'category' => (string) $a['id'],
        ])));
        $this->assertContains('Today pending A p1', $combo);
        $this->assertContains('Today pending A p2', $combo);
        $this->assertNotContains('Tomorrow all B', $combo);
        $this->assertNotContains('Completed A', $combo);

        $allCompleted = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters([
            'date'   => 'all',
            'status' => 'completed',
        ])));
        $this->assertSame(['Completed A'], $allCompleted);

        $tomorrowAllB = $this->titles($this->dash->occurrences((int) $user['id'], $this->filters([
            'date'     => 'tomorrow',
            'status'   => 'all',
            'category' => (string) $b['id'],
        ])));
        $this->assertSame(['Tomorrow all B'], $tomorrowAllB);

        $grouped = $this->dash->groupedByPriority((int) $user['id'], $this->filters(['date' => 'all', 'status' => 'all']));
        $this->assertSame(['Today pending A p1'], $this->titles($grouped[1]));
        $this->assertSame(['Today pending A p2'], $this->titles($grouped[2]));
        $this->assertSame(['Tomorrow all B'], $this->titles($grouped[3]));
        $this->assertSame(['Completed A'], $this->titles($grouped[4]));
        $this->assertSame(
            ['Today pending A p2'],
            $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'all', 'status' => 'all']), 2)),
        );
    }

    public function testOrderingDatedTimedThenUntimedThenUndated(): void
    {
        $user = $this->createUser('dash-svc-order@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Undated last', ['due_date' => '']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Later day', ['due_date' => '2026-08-30', 'due_time' => '08:00']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Today untimed', ['due_date' => '2026-08-29']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Today afternoon', ['due_date' => '2026-08-29', 'due_time' => '16:30']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Today morning', ['due_date' => '2026-08-29', 'due_time' => '09:00']);

        $this->assertSame(
            ['Today morning', 'Today afternoon', 'Today untimed', 'Later day', 'Undated last'],
            $this->titles($this->dash->occurrences((int) $user['id'], $this->filters(['date' => 'all']))),
        );
    }

    public function testSanitizePriorityOnlyAllowsOneThroughFour(): void
    {
        $this->assertSame(1, $this->dash->sanitizePriority(1));
        $this->assertSame(4, $this->dash->sanitizePriority('4'));
        $this->assertNull($this->dash->sanitizePriority(0));
        $this->assertNull($this->dash->sanitizePriority(5));
        $this->assertNull($this->dash->sanitizePriority('abc'));
    }

    public function testSafeReturnRejectsExternalUrls(): void
    {
        $this->assertSame('/dashboard', $this->dash->safeReturn('https://evil.example/phish'));
        $this->assertSame('/dashboard', $this->dash->safeReturn('//evil.example'));
        $this->assertSame('/dashboard?date=today&status=pending&category=all', $this->dash->safeReturn('dashboard?date=today&status=pending&category=all'));
        $this->assertSame('/tasks/priority/2?date=all&status=all&category=all', $this->dash->safeReturn('tasks/priority/2?date=all&status=all&category=all'));
        $this->assertSame('/dashboard', $this->dash->safeReturn('tasks/priority/9'));
        $this->assertSame('/notifications', $this->dash->safeReturn('notifications'));
        $this->assertSame('/notifications', $this->dash->safeReturn('notifications?next=https://evil.example'));
        $this->assertSame('/dashboard', $this->dash->safeReturn('https://evil.example/notifications'));
    }
}
