<?php

use App\Models\TaskModel;
use App\Services\DashboardService;
use App\Services\RecurrenceService;
use App\Services\TaskService;
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 SearchFlowTest 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
     *
     * @return array<string, string>
     */
    private function searchUrl(string $q, array $query = []): array
    {
        return array_merge([
            'q'        => $q,
            'date'     => 'today',
            'status'   => 'pending',
            'category' => 'all',
        ], $query);
    }

    public function testUnauthenticatedSearchRedirectsToSignIn(): void
    {
        $this->withSession([])->get('tasks/search', ['q' => 'invoice'])->assertRedirectTo('/sign-in');
    }

    public function testBasicSearchMatchingAndEmptyStates(): void
    {
        $user = $this->createUser('search-flow-basic@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Send Client Invoice', [
            'due_time' => '16:30',
            'priority' => '1',
        ]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Unrelated chore');
        $session = $this->authSession($user);

        $page = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', ['date' => 'all']));
        $page->assertOK();
        $body = (string) $page->response()->getBody();
        $this->assertStringContainsString('Send Client', $body);
        $this->assertStringContainsString('<mark class="search-hit">Invoice</mark>', $body);
        $page->assertDontSee('Unrelated chore');
        $this->assertStringContainsString('<h1 class="inner-title">invoice</h1>', $body);
        $this->assertStringContainsString('Important &amp; Urgent', $body);
        $this->assertStringContainsString('Personal • ' . date('j M Y') . ' • 4:30 PM', $body);
        $this->assertStringNotContainsString('Personal • •', $body);
        $this->assertStringContainsString('tasks/' . $task['id'] . '/edit', $body);
        $page->assertSee('Delete this task?');
        $page->assertSee('Delete this occurrence');

        $this->withSession($session)->get('tasks/search', $this->searchUrl('Invoice', ['date' => 'all']))
            ->assertSee('Send Client');
        $this->withSession($session)->get('tasks/search', $this->searchUrl('client', ['date' => 'all']))
            ->assertSee('Send');
        $this->withSession($session)->get('tasks/search', $this->searchUrl('send cli', ['date' => 'all']))
            ->assertSee('Invoice');
        $this->withSession($session)->get('tasks/search', $this->searchUrl('  invoice  ', ['date' => 'all']))
            ->assertSee('Send Client');

        $miss = $this->withSession($session)->get('tasks/search', $this->searchUrl('xyzzy', ['date' => 'all']));
        $miss->assertSee('No tasks found for "xyzzy".');
        $miss->assertDontSee('Send Client Invoice');

        $blank = $this->withSession($session)->get('tasks/search', $this->searchUrl('   ', ['date' => 'all']));
        $blank->assertSee(DashboardService::MSG_SEARCH_EMPTY);
        $blank->assertDontSee('Send Client Invoice');
        $blank->assertDontSee('Unrelated chore');

        $xss = $this->withSession($session)->get('tasks/search', $this->searchUrl('<script>alert(1)</script>', ['date' => 'all']));
        $xssBody = (string) $xss->response()->getBody();
        $this->assertStringContainsString('&lt;script&gt;alert(1)&lt;/script&gt;', $xssBody);
        $this->assertStringNotContainsString('<script>alert(1)</script>', $xssBody);
    }

    public function testIsolationAndDeletedRows(): void
    {
        $alice = $this->createUser('search-flow-iso-a@example.com', 1);
        $bob   = $this->createUser('search-flow-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'], 'Alice secret invoice');
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Bob secret invoice');
        $gone = $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Deleted invoice task');
        (new TaskModel())->delete((int) $gone['id']);
        $tomb = $this->makeTask((int) $alice['id'], (int) $aCat['id'], 'Tombstone invoice');
        db_connect()->table('task_occurrences')->where('id', $this->occ((int) $tomb['id'])['id'])->update(['is_deleted' => 1]);

        $page = $this->withSession($this->authSession($alice))->get('tasks/search', $this->searchUrl('invoice', ['date' => 'all']));
        $page->assertSee('Alice secret');
        $page->assertDontSee('Bob secret');
        $page->assertDontSee('Deleted invoice task');
        $page->assertDontSee('Tombstone invoice');
    }

    public function testRecurringOccurrencesAndDateFilters(): void
    {
        $user = $this->createUser('search-flow-rec@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $series = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Daily invoice series', [
            'due_date'        => '2026-09-01',
            'is_recurring'    => '1',
            'recurrence_type' => 'daily',
            'interval_value'  => '1',
            'priority'        => '2',
        ]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Undated invoice', ['due_date' => '']);
        $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'));
        $session  = $this->authSession($user);

        $all = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', ['date' => 'all']));
        $all->assertSee('Daily');
        $all->assertSee('series');
        $all->assertSee('Undated');
        $this->assertGreaterThan(1, substr_count((string) $all->response()->getBody(), 'Daily'));
        $all->assertSee('Important but Not Urgent');

        $todayPage = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice'));
        $todayPage->assertDontSee('Undated');

        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Tomorrow invoice', ['due_date' => $tomorrow]);
        $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', ['date' => 'tomorrow']))
            ->assertSee('Tomorrow');

        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Monday invoice', ['due_date' => $monday]);
        $week = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', ['date' => 'week']));
        $week->assertSee('Monday');

        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Plus6 invoice', ['due_date' => $plus6]);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Plus7 invoice', ['due_date' => $plus7]);
        $next7 = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', ['date' => 'next7']));
        $next7->assertSee('Plus6');
        $next7->assertDontSee('Plus7');

        $month = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', ['date' => 'month']));
        if (substr($monday, 0, 7) === date('Y-m')) {
            $month->assertSee('Monday');
        }
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'This month invoice', ['due_date' => $today]);
        $this->withSession($session)->get('tasks/search', $this->searchUrl('This month', ['date' => 'month']))
            ->assertSee('This month');

        $custom = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', [
            'date' => 'custom',
            'from' => $tomorrow,
            'to'   => $tomorrow,
        ]));
        $custom->assertSee('Tomorrow');

        $sep01 = $this->occ((int) $series['id'], '2026-09-01');
        $sep02 = $this->occ((int) $series['id'], '2026-09-02');
        $this->assertNotSame($sep01['id'], $sep02['id']);
    }

    public function testCombinedFiltersStatusCategoryAndMutations(): void
    {
        $alice = $this->createUser('search-flow-mut@example.com', 1);
        $bob   = $this->createUser('search-flow-mut-b@example.com', 1);
        $a     = $this->createCategory((int) $alice['id'], 'CatA');
        $b     = $this->createCategory((int) $alice['id'], 'CatB');
        $bobCat = $this->createCategory((int) $bob['id'], 'Bob');
        $pending = $this->makeTask((int) $alice['id'], (int) $a['id'], 'Pending invoice A');
        $done    = $this->makeTask((int) $alice['id'], (int) $a['id'], 'Completed invoice A');
        (new RecurrenceService())->completeOccurrence((int) $alice['id'], (int) $this->occ((int) $done['id'])['id']);
        $this->makeTask((int) $alice['id'], (int) $b['id'], 'Tomorrow invoice B', [
            'due_date' => date('Y-m-d', strtotime('+1 day')),
        ]);
        $bobTask = $this->makeTask((int) $bob['id'], (int) $bobCat['id'], 'Bob invoice');
        $session = $this->authSession($alice);

        $combo = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', [
            'date'     => 'today',
            'status'   => 'pending',
            'category' => (string) $a['id'],
        ]));
        $comboBody = (string) $combo->response()->getBody();
        $this->assertStringContainsString('task-title">Pending', $comboBody);
        $this->assertStringNotContainsString('task-title">Completed', $comboBody);
        $this->assertStringNotContainsString('task-title">Tomorrow', $comboBody);
        $combo->assertSee('CatA');

        $completed = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', [
            'date'   => 'all',
            'status' => 'completed',
        ]));
        $completedBody = (string) $completed->response()->getBody();
        $this->assertStringContainsString('task-title">Completed', $completedBody);
        $this->assertStringNotContainsString('task-title">Pending', $completedBody);

        $occId = (int) $this->occ((int) $pending['id'])['id'];
        $this->withSession($session)->post('tasks/occurrences/' . $occId . '/complete')->assertOK();
        $afterPending = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', [
            'date'     => 'today',
            'status'   => 'pending',
            'category' => (string) $a['id'],
        ]));
        $this->assertStringNotContainsString('task-title">Pending', (string) $afterPending->response()->getBody());
        $afterAll = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', [
            'date'   => 'all',
            'status' => 'all',
            'category' => (string) $a['id'],
        ]));
        $this->assertStringContainsString('task-title">Pending', (string) $afterAll->response()->getBody());

        $oneOff = $this->makeTask((int) $alice['id'], (int) $a['id'], 'Delete me invoice');
        $this->withSession($session)->post('tasks/' . $oneOff['id'] . '/delete')->assertOK();
        $this->withSession($session)->get('tasks/search', $this->searchUrl('Delete me', ['date' => 'all']))
            ->assertDontSee('Delete me invoice');

        $series = $this->makeTask((int) $alice['id'], (int) $a['id'], 'Recurring invoice', [
            'due_date'        => '2026-09-01',
            'is_recurring'    => '1',
            'recurrence_type' => 'daily',
            'interval_value'  => '1',
        ]);
        $first  = $this->occ((int) $series['id'], '2026-09-01');
        $second = $this->occ((int) $series['id'], '2026-09-02');
        $this->withSession($session)->post('tasks/occurrences/' . $first['id'] . '/complete')->assertOK();
        $this->assertSame('completed', db_connect()->table('task_occurrences')->where('id', $first['id'])->get()->getRowArray()['status']);
        $this->assertSame('pending', db_connect()->table('task_occurrences')->where('id', $second['id'])->get()->getRowArray()['status']);

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

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

    public function testDashboardFiltersReachSearchAndEditReturnIsAllowlisted(): void
    {
        $user = $this->createUser('search-flow-nav@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Keep invoice', [
            'due_date' => date('Y-m-d', strtotime('+1 day')),
        ]);
        $session = $this->authSession($user);

        $dash = $this->withSession($session)->get('dashboard', [
            'date'     => 'tomorrow',
            'status'   => 'pending',
            'category' => 'all',
        ]);
        $dashBody = (string) $dash->response()->getBody();
        $this->assertStringContainsString('name="q"', $dashBody);
        $this->assertStringContainsString('name="date"', $dashBody);
        $this->assertStringContainsString('value="tomorrow"', $dashBody);

        $search = $this->withSession($session)->get('tasks/search', $this->searchUrl('invoice', ['date' => 'tomorrow']));
        $search->assertSee('Keep');
        $this->assertStringContainsString('task-title">Keep', (string) $search->response()->getBody());
        $search->assertSee('Tomorrow');
        $this->assertStringContainsString('q=invoice', (string) $search->response()->getBody());

        $held = $this->withSession($session + [
            DashboardService::RETURN_KEY => 'tasks/search?date=tomorrow&status=pending&category=all&q=invoice',
            DashboardService::SESSION_KEY => [
                'date'     => 'tomorrow',
                'status'   => 'pending',
                'category' => 'all',
                'from'     => '',
                'to'       => '',
            ],
        ])->get('tasks/' . $this->taskIdByTitle((int) $user['id'], 'Keep invoice') . '/edit');
        $held->assertOK();
        $editBody = (string) $held->response()->getBody();
        $this->assertStringContainsString('tasks/search', $editBody);
        $this->assertStringContainsString('q=invoice', $editBody);
        $this->assertStringNotContainsString('https://evil.example', $editBody);

        $dashSvc = new DashboardService();
        $this->assertSame('/dashboard', $dashSvc->safeReturn('https://evil.example/phish'));
        $this->assertSame('/dashboard', $dashSvc->safeReturn('//evil.example'));
    }

    public function testDashboardSearchFormDoesNotIgnoreActiveFilters(): void
    {
        $user = $this->createUser('search-flow-form@example.com', 1);
        $this->createCategory((int) $user['id'], 'Personal');
        $page = $this->withSession($this->authSession($user))->get('dashboard', [
            'date'     => 'tomorrow',
            'status'   => 'completed',
            'category' => 'all',
        ]);
        $body = (string) $page->response()->getBody();
        $this->assertStringContainsString('search-form', $body);
        $this->assertStringContainsString('tasks/search', $body);
        $this->assertStringContainsString('name="status" value="completed"', $body);
        $this->assertStringContainsString('name="date" value="tomorrow"', $body);
    }

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