<?php

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

/**
 * @internal
 */
final class DashboardFlowTest 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'    => date('Y-m-d'),
            'due_time'    => '',
        ], $override)));
        $this->assertTrue($result['ok'] ?? false, (string) ($result['message'] ?? 'create failed'));

        return $result['task'];
    }

    private function occ(int $taskId, ?string $date = null): array
    {
        $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 $row;
    }

    /**
     * @param array<string, string> $query
     */
    private function dashboardUrl(array $query = []): array
    {
        return array_merge([
            'date'     => 'today',
            'status'   => 'pending',
            'category' => 'all',
        ], $query);
    }

    public function testUnauthenticatedDashboardRedirectsToSignIn(): void
    {
        $this->withSession([])->get('dashboard')->assertRedirectTo('/sign-in');
    }

    public function testDefaultFiltersAndUserIsolation(): void
    {
        $alice = $this->createUser('dash-flow-a@example.com', 1);
        $bob   = $this->createUser('dash-flow-b@example.com', 1);
        $aCat  = $this->createCategory((int) $alice['id'], 'Personal');
        $bCat  = $this->createCategory((int) $bob['id'], 'BobOnly');
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Alice today');
        $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Alice tomorrow', ['due_date' => date('Y-m-d', strtotime('+1 day'))]);
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob secret');

        $page = $this->withSession($this->authSession($alice))->get('dashboard');
        $page->assertOK();
        $body = (string) $page->response()->getBody();
        $this->assertStringContainsString('>Today <', $body);
        $this->assertStringContainsString('>Pending <', $body);
        $this->assertStringContainsString('>All Categories <', $body);
        $page->assertSee('Alice today');
        $page->assertDontSee('Alice tomorrow');
        $page->assertDontSee('Bob secret');
        $page->assertDontSee('BobOnly');
        $page->assertDontSee('Suggested by AI');
        $page->assertDontSee('Add tasks that require immediate attention and directly impact');
        $page->assertSee('Important &amp; Urgent');
        $page->assertSee('Important but Not Urgent');
        $page->assertSee('Not Important but Urgent');
        $page->assertSee('Not Important &amp; Not Urgent');
        $this->assertStringContainsString('tasks/create?priority=1', $body);
        $page->assertSee('No tasks due today.');
    }

    public function testDateFiltersOverHttp(): void
    {
        $user = $this->createUser('dash-flow-dates@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $today    = date('Y-m-d');
        $tomorrow = date('Y-m-d', strtotime('+1 day'));
        $plus6    = date('Y-m-d', strtotime('+6 days'));
        $plus7    = date('Y-m-d', strtotime('+7 days'));
        $dow      = (int) date('N');
        $monday   = date('Y-m-d', strtotime('-' . ($dow - 1) . ' days'));
        $prevSun  = date('Y-m-d', strtotime($monday . ' -1 day'));
        $monthStart = date('Y-m-01');
        $prevMonth  = date('Y-m-d', strtotime($monthStart . ' -1 day'));

        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http today', ['due_date' => $today]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http tomorrow', ['due_date' => $tomorrow]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http plus6', ['due_date' => $plus6]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http plus7', ['due_date' => $plus7]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http monday', ['due_date' => $monday]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http prevSun', ['due_date' => $prevSun]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http prevMonth', ['due_date' => $prevMonth]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Http undated', ['due_date' => '']);

        $session = $this->authSession($user);

        $todayPage = $this->withSession($session)->get('dashboard', $this->dashboardUrl());
        $todayPage->assertSee('Http today');
        $todayPage->assertDontSee('Http tomorrow');
        $todayPage->assertDontSee('Http undated');

        $tomorrowPage = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'tomorrow']));
        $tomorrowPage->assertSee('Http tomorrow');
        $tomorrowPage->assertDontSee('Http today');
        $tomorrowPage->assertSee('Tomorrow');

        $week = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'week']));
        $week->assertSee('Http monday');
        $week->assertSee('Http today');
        $week->assertDontSee('Http prevSun');

        $next7 = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'next7']));
        $next7->assertSee('Http today');
        $next7->assertSee('Http plus6');
        $next7->assertDontSee('Http plus7');
        $next7->assertSee('Next 7 Days');

        $month = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'month']));
        $month->assertSee('Http today');
        $month->assertDontSee('Http prevMonth');

        $custom = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date' => 'custom',
            'from' => $tomorrow,
            'to'   => $tomorrow,
        ]));
        $custom->assertSee('Http tomorrow');
        $custom->assertDontSee('Http today');

        $invalid = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date' => 'custom',
            'from' => $plus7,
            'to'   => $today,
        ]));
        $invalid->assertSee(DashboardService::MSG_CUSTOM_ORDER);
        $invalid->assertSee('Today');

        $all = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'all']));
        $all->assertSee('Http undated');
        $all->assertSee('Http today');
        $all->assertSee('All Dates');
    }

    public function testStatusCategoryCombinedAndForeignCategoryIgnored(): void
    {
        $alice = $this->createUser('dash-flow-combo@example.com', 1);
        $bob   = $this->createUser('dash-flow-combo-b@example.com', 1);
        $a     = $this->createCategory((int) $alice['id'], 'CatA');
        $b     = $this->createCategory((int) $alice['id'], 'CatB');
        $bobCat = $this->createCategory((int) $bob['id'], 'Foreign');
        $this->makeTask((int) $alice['id'], (int) $a['id'], 'Pending A');
        $done = $this->makeTask((int) $alice['id'], (int) $a['id'], 'Completed A');
        (new RecurrenceService())->completeOccurrence((int) $alice['id'], (int) $this->occ((int) $done['id'])['id']);
        $this->makeTask((int) $alice['id'], (int) $b['id'], 'Tomorrow B', [
            'due_date' => date('Y-m-d', strtotime('+1 day')),
            'priority' => '2',
        ]);
        $gone = $this->makeTask((int) $alice['id'], (int) $a['id'], 'Deleted occ');
        db_connect()->table('task_occurrences')->where('id', $this->occ((int) $gone['id'])['id'])->update(['is_deleted' => 1]);

        $session = $this->authSession($alice);
        $pendingA = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['category' => (string) $a['id']]));
        $pendingA->assertSee('Pending A');
        $pendingA->assertSee('CatA');
        $pendingA->assertDontSee('Completed A');
        $pendingA->assertDontSee('Tomorrow B');
        $pendingA->assertDontSee('Deleted occ');

        $completed = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date'   => 'all',
            'status' => 'completed',
        ]));
        $completed->assertSee('Completed A');
        $completed->assertDontSee('Pending A');
        $completed->assertSee('Completed');

        $allStatus = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date'   => 'all',
            'status' => 'all',
        ]));
        $allStatus->assertSee('Pending A');
        $allStatus->assertSee('Completed A');
        $allStatus->assertDontSee('Deleted occ');

        $tomorrowB = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date'     => 'tomorrow',
            'status'   => 'all',
            'category' => (string) $b['id'],
        ]));
        $tomorrowB->assertSee('Tomorrow B');
        $tomorrowB->assertDontSee('Pending A');

        $foreign = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date'     => 'all',
            'category' => (string) $bobCat['id'],
        ]));
        $foreign->assertSee('Pending A');
        $foreign->assertSee('All Categories');
        $foreign->assertDontSee('Foreign');
    }

    public function testPriorityCardsDetailInvalidIdAndNoPriorityRepeatOnRows(): void
    {
        $user = $this->createUser('dash-flow-pri@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P1 task', ['priority' => '1']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P3 task', ['priority' => '3']);

        $session = $this->authSession($user);
        $p1Id = $this->taskIdByTitle((int) $user['id'], 'P1 task');
        $dash = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'all']));
        $dash->assertSee('P1 task');
        $dash->assertSee('P3 task');
        $this->assertStringContainsString('tasks/' . $p1Id . '/edit', (string) $dash->response()->getBody());

        $p1 = $this->withSession($session)->get('tasks/priority/1', $this->dashboardUrl(['date' => 'all']));
        $p1->assertOK();
        $p1->assertSee('P1 task');
        $p1->assertDontSee('P3 task');
        $p1Body = (string) $p1->response()->getBody();
        $this->assertDoesNotMatchRegularExpression('/class="task-meta">[^<]*Important/', $p1Body);
        $this->assertStringContainsString('tasks/' . $p1Id . '/edit', $p1Body);

        $this->expectException(PageNotFoundException::class);
        $this->withSession($session)->get('tasks/priority/9');
    }

    public function testMetadataRenderingAndEditIsolation(): void
    {
        $alice = $this->createUser('dash-flow-meta@example.com', 1);
        $bob   = $this->createUser('dash-flow-meta-b@example.com', 1);
        $cat   = $this->createCategory((int) $alice['id'], 'Personal');
        $bobCat = $this->createCategory((int) $bob['id'], 'BobCat');
        $this->makeTask((int) $alice['id'], (int) $cat['id'], 'With time', [
            'due_date' => date('Y-m-d'),
            'due_time' => '16:30',
        ]);
        $this->makeTask((int) $alice['id'], (int) $cat['id'], 'Date only', ['due_date' => date('Y-m-d')]);
        $this->makeTask((int) $alice['id'], (int) $cat['id'], 'No date', ['due_date' => '']);
        $bobTask = $this->makeTask((int) $bob['id'], (int) $bobCat['id'], 'Bob edit me');

        $session = $this->authSession($alice);
        $timed = $this->withSession($session)->get('dashboard', $this->dashboardUrl());
        $timed->assertSee('Personal • ' . date('j M Y') . ' • 4:30 PM');
        $timed->assertSee('With time');
        $timed->assertSee('Date only');
        $this->assertStringNotContainsString('Personal • •', (string) $timed->response()->getBody());

        $undated = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'all']));
        $undatedBody = (string) $undated->response()->getBody();
        $this->assertMatchesRegularExpression('/<span class="task-meta">Personal<\/span>\s*<span class="task-title">No date<\/span>/', $undatedBody);

        $denied = $this->withSession($session)->get('tasks/' . $bobTask['id'] . '/edit');
        $denied->assertRedirect();
        $this->assertStringNotContainsString('Bob edit me', (string) $denied->response()->getBody());
    }

    public function testCompleteOccurrenceAndIdempotencyOverHttp(): void
    {
        $alice = $this->createUser('dash-flow-done@example.com', 1);
        $bob   = $this->createUser('dash-flow-done-b@example.com', 1);
        $aCat  = $this->createCategory((int) $alice['id'], 'Personal');
        $bCat  = $this->createCategory((int) $bob['id'], 'Bob');
        $oneOff = $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Finish me');
        $task  = $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Daily series', [
            'due_date'        => '2026-09-01',
            'is_recurring'    => '1',
            'recurrence_type' => 'daily',
            'interval_value'  => '1',
        ]);
        $bobTask = $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob occ');
        $oneOffOcc = $this->occ((int) $oneOff['id']);
        $first = $this->occ((int) $task['id'], '2026-09-01');
        $second = $this->occ((int) $task['id'], '2026-09-02');
        $bobOcc = $this->occ((int) $bobTask['id']);

        $session = $this->authSession($alice);
        $complete = $this->withSession($session)->post('tasks/occurrences/' . $first['id'] . '/complete');
        $complete->assertOK();
        $row = db_connect()->table('task_occurrences')->where('id', $first['id'])->get()->getRowArray();
        $this->assertSame('completed', $row['status']);
        $this->assertNotNull($row['completed_at']);
        $other = db_connect()->table('task_occurrences')->where('id', $second['id'])->get()->getRowArray();
        $this->assertSame('pending', $other['status']);
        $this->assertNull($other['completed_at']);
        $stamp = $row['completed_at'];

        $again = $this->withSession($session)->post('tasks/occurrences/' . $first['id'] . '/complete');
        $again->assertOK();
        $this->assertSame($stamp, db_connect()->table('task_occurrences')->where('id', $first['id'])->get()->getRowArray()['completed_at']);

        $this->withSession($session)->post('tasks/occurrences/' . $oneOffOcc['id'] . '/complete')->assertOK();

        $pendingView = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date'   => 'all',
            'status' => 'pending',
        ]));
        $pendingView->assertDontSee('Finish me');

        $completedView = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date'   => 'all',
            'status' => 'completed',
        ]));
        $completedView->assertSee('Finish me');
        $completedView->assertSee('Daily series');

        $allView = $this->withSession($session)->get('dashboard', $this->dashboardUrl([
            'date'   => 'all',
            'status' => 'all',
        ]));
        $allView->assertSee('Finish me');

        $stolen = $this->withSession($session)->post('tasks/occurrences/' . $bobOcc['id'] . '/complete');
        $stolen->assertStatus(404);
        $this->assertStringNotContainsString('Bob occ', (string) $stolen->response()->getBody());
    }

    public function testDeleteNonRecurringAndRecurringChooserRoutes(): void
    {
        $alice = $this->createUser('dash-flow-del@example.com', 1);
        $bob   = $this->createUser('dash-flow-del-b@example.com', 1);
        $aCat  = $this->createCategory((int) $alice['id'], 'Personal');
        $bCat  = $this->createCategory((int) $bob['id'], 'Bob');
        $one   = $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'One-off');
        $series = $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Series', [
            'due_date'        => '2026-09-01',
            'is_recurring'    => '1',
            'recurrence_type' => 'daily',
            'interval_value'  => '1',
        ]);
        $bobTask = $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob one-off');
        $occId = (int) $this->occ((int) $one['id'])['id'];

        $session = $this->authSession($alice);
        $page = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'all']));
        $page->assertSee('Delete this task?');
        $page->assertSee('Delete this occurrence');
        $page->assertSee('Delete this and future occurrences');

        $del = $this->withSession($session)->post('tasks/' . $one['id'] . '/delete');
        $del->assertOK();
        $this->assertNotNull((new TaskModel())->withDeleted()->find($one['id'])['deleted_at'] ?? null);
        $this->assertSame(0, db_connect()->table('task_occurrences')->where('task_id', $one['id'])->countAllResults());
        $this->assertSame(0, db_connect()->table('task_occurrences')->where('id', $occId)->countAllResults());

        $stolen = $this->withSession($session)->post('tasks/' . $bobTask['id'] . '/delete');
        $stolen->assertStatus(404);
        $this->assertNotNull((new TaskModel())->find($bobTask['id']));

        $recurringDel = $this->withSession($session)->post('tasks/' . $series['id'] . '/delete');
        $recurringDel->assertStatus(400);
        $this->assertNotNull((new TaskModel())->find($series['id']));

        $sep03 = $this->occ((int) $series['id'], '2026-09-03');
        $this->withSession($session)->post('tasks/occurrences/' . $sep03['id'] . '/complete')->assertOK();
        $this->withSession($session)->post('tasks/occurrences/' . $sep03['id'] . '/delete')->assertOK();
        $this->assertSame(1, (int) db_connect()->table('task_occurrences')->where('id', $sep03['id'])->get()->getRowArray()['is_deleted']);
        $this->assertGreaterThan(0, db_connect()->table('task_occurrences')->where('task_id', $series['id'])->where('occurrence_date', '2026-09-04')->where('is_deleted', 0)->countAllResults());
        $this->assertNotNull((new TaskModel())->find($series['id']));

        $sep05 = $this->occ((int) $series['id'], '2026-09-05');
        $this->withSession($session)->post('tasks/occurrences/' . $sep05['id'] . '/delete-future')->assertOK();
        $this->assertSame(0, db_connect()->table('task_occurrences')->where('task_id', $series['id'])->where('occurrence_date', '2026-09-06')->where('is_deleted', 0)->countAllResults());
        $this->assertGreaterThan(0, db_connect()->table('task_occurrences')->where('task_id', $series['id'])->where('occurrence_date', '2026-09-02')->where('is_deleted', 0)->countAllResults());

        $this->expectException(PageNotFoundException::class);
        $this->withSession($session)->get('tasks/' . $series['id'] . '/delete');
    }

    public function testAmbiguousDeleteFutureRouteRemovedAndNoGetMutations(): void
    {
        $routes = (string) file_get_contents(APPPATH . 'Config/Routes.php');
        $this->assertStringNotContainsString("post('tasks/(:num)/delete-future'", $routes);
        $this->assertStringContainsString("post('tasks/occurrences/(:num)/delete-future'", $routes);
        $this->assertStringContainsString("post('tasks/occurrences/(:num)/delete'", $routes);
        $this->assertStringContainsString("post('tasks/occurrences/(:num)/complete'", $routes);
        $this->assertStringContainsString("post('tasks/(:num)/delete'", $routes);
        $this->assertStringNotContainsString("get('tasks/(:num)/delete'", $routes);
        $this->assertStringNotContainsString("get('tasks/occurrences", $routes);

        $user = $this->createUser('dash-flow-routes@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Route task');
        $session = $this->authSession($user);

        $this->expectException(PageNotFoundException::class);
        $this->withSession($session)->post('tasks/' . $task['id'] . '/delete-future');
    }

    public function testFilterPersistenceToPriorityDetail(): void
    {
        $user = $this->createUser('dash-flow-persist@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'PersistCat');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Persist today');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Persist tomorrow', [
            'due_date' => date('Y-m-d', strtotime('+1 day')),
        ]);

        $session = $this->authSession($user);
        $dash = $this->withSession($session)->get('dashboard', $this->dashboardUrl(['date' => 'tomorrow']));
        $dash->assertSee('Persist tomorrow');
        $this->assertStringContainsString('date=tomorrow', (string) $dash->response()->getBody());
        $detail = $this->withSession($session)->get('tasks/priority/1', $this->dashboardUrl(['date' => 'tomorrow']));
        $detail->assertSee('Persist tomorrow');
        $detail->assertDontSee('Persist today');
        $detail->assertSee('Tomorrow');
        $held = $this->withSession($session + [
            DashboardService::SESSION_KEY => [
                'date'     => 'tomorrow',
                'status'   => 'pending',
                'category' => 'all',
                'from'     => '',
                'to'       => '',
            ],
        ])->get('tasks/priority/1');
        $held->assertSee('Persist tomorrow');
        $held->assertDontSee('Persist today');
    }

    private function taskIdByTitle(int $userId, string $title): int
    {
        $row = db_connect()->table('tasks')->where('user_id', $userId)->where('title', $title)->get()->getRowArray();
        $this->assertIsArray($row);

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