<?php

use App\Models\CategoryModel;
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 TaskServiceTest extends CIUnitTestCase
{
    use DatabaseTestTrait;
    use AuthTestTrait;
    use CategoryTestTrait;

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

    private TaskService $tasks;

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

    /**
     * @param array<string, string> $override
     *
     * @return array<string, mixed>
     */
    private function payload(int $categoryId, array $override = []): array
    {
        return $this->tasks->collect(array_merge([
            'title'       => 'Buy milk',
            'priority'    => '1',
            'category_id' => (string) $categoryId,
            'due_date'    => '',
            'due_time'    => '',
        ], $override));
    }

    public function testBlankAndWhitespaceTitlesAreRejected(): void
    {
        $user = $this->createUser('task-title@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');

        $blank = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], ['title' => '']));
        $this->assertFalse($blank['ok']);
        $this->assertSame(TaskService::MSG_TITLE_REQUIRED, $blank['errors']['title']);

        $space = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], ['title' => '   ']));
        $this->assertFalse($space['ok']);
        $this->assertSame(TaskService::MSG_TITLE_REQUIRED, $space['errors']['title']);

        $this->assertSame(0, db_connect()->table('tasks')->where('user_id', $user['id'])->countAllResults());
    }

    public function testInvalidPriorityAndMissingCategoryAreRejected(): void
    {
        $user = $this->createUser('task-pri@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');

        $badPriority = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], ['priority' => '9']));
        $this->assertFalse($badPriority['ok']);
        $this->assertSame(TaskService::MSG_PRIORITY, $badPriority['errors']['priority']);

        $missing = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], ['category_id' => '']));
        $this->assertFalse($missing['ok']);
        $this->assertSame(TaskService::MSG_CATEGORY, $missing['errors']['category_id']);
    }

    public function testOtherUsersCategoryIsRejected(): void
    {
        $alice = $this->createUser('task-alice-cat@example.com', 1);
        $bob   = $this->createUser('task-bob-cat@example.com', 1);
        $aliceCat = $this->createCategory((int) $alice['id'], 'Alice Home');
        $bobCat   = $this->createCategory((int) $bob['id'], 'Bob Home');

        $result = $this->tasks->create((int) $alice['id'], $this->payload((int) $bobCat['id']));
        $this->assertFalse($result['ok']);
        $this->assertSame(TaskService::MSG_CATEGORY, $result['errors']['category_id']);
        $this->assertSame(0, db_connect()->table('tasks')->where('user_id', $alice['id'])->countAllResults());
        $this->assertNotNull((new CategoryModel())->find($aliceCat['id']));
    }

    public function testNoCategoriesBlocksCreate(): void
    {
        $user   = $this->createUser('task-none@example.com', 1);
        $result = $this->tasks->create((int) $user['id'], $this->payload(0, ['category_id' => '1']));
        $this->assertFalse($result['ok']);
        $this->assertSame(TaskService::MSG_NO_CATEGORIES, $result['errors']['category_id']);
    }

    public function testValidUndatedTaskCreatesOnePendingOccurrenceWithNulls(): void
    {
        $user = $this->createUser('task-undated@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');

        $result = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], [
            'due_date' => '',
            'due_time' => '14:30',
        ]));
        $this->assertTrue($result['ok']);
        $this->assertSame((int) $user['id'], (int) $result['task']['user_id']);
        $this->assertNull($result['task']['due_date']);
        $this->assertNull($result['task']['due_time']);
        $this->assertSame(0, (int) $result['task']['is_recurring']);
        $this->assertNotSame(date('Y-m-d'), $result['task']['due_date']);

        $taskId = (int) $result['task']['id'];
        $this->assertSame(1, db_connect()->table('task_occurrences')->where('task_id', $taskId)->countAllResults());

        $occurrence = $this->tasks->primaryOccurrence($taskId);
        $this->assertIsArray($occurrence);
        $this->assertNull($occurrence['occurrence_date']);
        $this->assertNull($occurrence['occurrence_time']);
        $this->assertSame('pending', $occurrence['status']);
        $this->assertSame(0, db_connect()->table('task_recurrence_rules')->where('task_id', $taskId)->countAllResults());
    }

    public function testDatedTaskStoresDateTimeOnTaskAndOccurrence(): void
    {
        $user = $this->createUser('task-dated@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Office');

        $result = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], [
            'due_date' => '2026-09-02',
            'due_time' => '14:30',
            'priority' => '2',
        ]));
        $this->assertTrue($result['ok']);
        $this->assertSame('2026-09-02', $result['task']['due_date']);
        $this->assertSame('14:30:00', $result['task']['due_time']);
        $this->assertSame(2, (int) $result['task']['priority']);

        $occurrence = $this->tasks->primaryOccurrence((int) $result['task']['id']);
        $this->assertSame('2026-09-02', $occurrence['occurrence_date']);
        $this->assertSame('14:30:00', $occurrence['occurrence_time']);
        $this->assertSame('pending', $occurrence['status']);
    }

    public function testInvalidDateAndTimeAreRejected(): void
    {
        $user = $this->createUser('task-bad-dt@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');

        $badDate = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], [
            'due_date' => '2026-02-30',
        ]));
        $this->assertFalse($badDate['ok']);
        $this->assertSame(TaskService::MSG_DATE, $badDate['errors']['due_date']);

        $badTime = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], [
            'due_date' => '2026-09-02',
            'due_time' => '25:00',
        ]));
        $this->assertFalse($badTime['ok']);
        $this->assertSame(TaskService::MSG_TIME, $badTime['errors']['due_time']);
    }

    public function testCreateRollsBackWhenOccurrenceStepFails(): void
    {
        $user = $this->createUser('task-tx@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');

        $this->tasks->beforeCommit = static function (): void {
            throw new \RuntimeException('forced rollback');
        };

        $result = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id']));
        $this->assertFalse($result['ok']);
        $this->assertSame(TaskService::MSG_FAIL, $result['message']);
        $this->assertSame(0, db_connect()->table('tasks')->where('user_id', $user['id'])->countAllResults());
        $this->assertSame(0, db_connect()->table('task_occurrences')->countAllResults());
    }

    public function testIncompleteRecurringIsRejected(): void
    {
        $user = $this->createUser('task-recur@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');

        $result = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], [
            'is_recurring' => '1',
        ]));
        $this->assertFalse($result['ok']);
        $this->assertSame(\App\Services\RecurrenceService::MSG_START, $result['errors']['due_date']);
        $this->assertSame(0, db_connect()->table('tasks')->where('user_id', $user['id'])->countAllResults());
        $this->assertSame(0, db_connect()->table('task_recurrence_rules')->countAllResults());
    }

    public function testEditUpdatesSingleOccurrenceAndPreservesCompletion(): void
    {
        $user = $this->createUser('task-edit@example.com', 1);
        $one  = $this->createCategory((int) $user['id'], 'Home');
        $two  = $this->createCategory((int) $user['id'], 'Office');

        $created = $this->tasks->create((int) $user['id'], $this->payload((int) $one['id'], [
            'due_date' => '2026-09-02',
            'due_time' => '09:00',
        ]));
        $taskId = (int) $created['task']['id'];
        $occId  = (int) $this->tasks->primaryOccurrence($taskId)['id'];

        db_connect()->table('task_occurrences')->where('id', $occId)->update([
            'status'       => 'completed',
            'completed_at' => '2026-08-20 12:00:00',
        ]);

        $updated = $this->tasks->update((int) $user['id'], $taskId, $this->payload((int) $two['id'], [
            'title'    => 'Buy oat milk',
            'priority' => '3',
            'due_date' => '2026-09-05',
            'due_time' => '16:45',
        ]));
        $this->assertTrue($updated['ok']);
        $this->assertSame('Buy oat milk', $updated['task']['title']);
        $this->assertSame(3, (int) $updated['task']['priority']);
        $this->assertSame((int) $two['id'], (int) $updated['task']['category_id']);
        $this->assertSame('2026-09-05', $updated['task']['due_date']);
        $this->assertSame('16:45:00', $updated['task']['due_time']);

        $this->assertSame(1, db_connect()->table('task_occurrences')->where('task_id', $taskId)->countAllResults());
        $occurrence = $this->tasks->primaryOccurrence($taskId);
        $this->assertSame($occId, (int) $occurrence['id']);
        $this->assertSame('2026-09-05', $occurrence['occurrence_date']);
        $this->assertSame('16:45:00', $occurrence['occurrence_time']);
        $this->assertSame('completed', $occurrence['status']);
        $this->assertNotNull($occurrence['completed_at']);
    }

    public function testRemovingDueDateClearsOccurrenceDateAndTime(): void
    {
        $user = $this->createUser('task-clear@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');
        $created = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], [
            'due_date' => '2026-09-02',
            'due_time' => '09:00',
        ]));
        $taskId = (int) $created['task']['id'];

        $updated = $this->tasks->update((int) $user['id'], $taskId, $this->payload((int) $cat['id'], [
            'due_date' => '',
            'due_time' => '11:00',
        ]));
        $this->assertTrue($updated['ok']);
        $this->assertNull($updated['task']['due_date']);
        $this->assertNull($updated['task']['due_time']);

        $occurrence = $this->tasks->primaryOccurrence($taskId);
        $this->assertNull($occurrence['occurrence_date']);
        $this->assertNull($occurrence['occurrence_time']);
        $this->assertSame(1, db_connect()->table('task_occurrences')->where('task_id', $taskId)->countAllResults());
    }

    public function testCannotUpdateAnotherUsersTask(): void
    {
        $alice = $this->createUser('task-own-a@example.com', 1);
        $bob   = $this->createUser('task-own-b@example.com', 1);
        $aliceCat = $this->createCategory((int) $alice['id'], 'Alice');
        $bobCat   = $this->createCategory((int) $bob['id'], 'Bob');
        $bobsTask = $this->tasks->create((int) $bob['id'], $this->payload((int) $bobCat['id'], [
            'title' => 'Secret Bob Task',
        ]));

        $result = $this->tasks->update((int) $alice['id'], (int) $bobsTask['task']['id'], $this->payload((int) $aliceCat['id'], [
            'title' => 'Hacked',
        ]));
        $this->assertFalse($result['ok']);
        $this->assertSame(TaskService::MSG_NOT_FOUND, $result['message']);
        $this->assertArrayNotHasKey('errors', $result);
        $this->seeInDatabase('tasks', ['id' => $bobsTask['task']['id'], 'title' => 'Secret Bob Task']);
    }

    public function testCannotReassignTaskToAnotherUsersCategory(): void
    {
        $alice = $this->createUser('task-reassign-a@example.com', 1);
        $bob   = $this->createUser('task-reassign-b@example.com', 1);
        $aliceCat = $this->createCategory((int) $alice['id'], 'Alice');
        $bobCat   = $this->createCategory((int) $bob['id'], 'Bob');
        $created  = $this->tasks->create((int) $alice['id'], $this->payload((int) $aliceCat['id']));

        $result = $this->tasks->update((int) $alice['id'], (int) $created['task']['id'], $this->payload((int) $bobCat['id'], [
            'title' => 'Still Alice',
        ]));
        $this->assertFalse($result['ok']);
        $this->assertSame(TaskService::MSG_CATEGORY, $result['errors']['category_id']);
        $this->seeInDatabase('tasks', [
            'id'          => $created['task']['id'],
            'title'       => 'Buy milk',
            'category_id' => $aliceCat['id'],
        ]);
    }

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

    public function testPostedUserIdIsIgnoredByCollect(): void
    {
        $collected = $this->tasks->collect([
            'title'       => 'Own it',
            'priority'    => '1',
            'category_id' => '3',
            'user_id'     => '999',
        ]);
        $this->assertArrayNotHasKey('user_id', $collected);
    }

    public function testDeleteOwnedSoftDeletesNonRecurringAndRemovesOccurrences(): void
    {
        $user = $this->createUser('task-del@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Home');
        $created = $this->tasks->create((int) $user['id'], $this->payload((int) $cat['id'], ['title' => 'Drop me']));
        $taskId  = (int) $created['task']['id'];
        $this->assertGreaterThan(0, db_connect()->table('task_occurrences')->where('task_id', $taskId)->countAllResults());

        $result = $this->tasks->deleteOwned((int) $user['id'], $taskId);
        $this->assertTrue($result['ok']);
        $this->assertNull((new \App\Models\TaskModel())->find($taskId));
        $this->assertNotNull((new \App\Models\TaskModel())->withDeleted()->find($taskId)['deleted_at']);
        $this->assertSame(0, db_connect()->table('task_occurrences')->where('task_id', $taskId)->countAllResults());
        $this->assertSame(0, db_connect()->table('task_recurrence_rules')->where('task_id', $taskId)->countAllResults());
    }

    public function testDeleteOwnedRejectsRecurringAndForeignTasks(): void
    {
        $alice = $this->createUser('task-del-a@example.com', 1);
        $bob   = $this->createUser('task-del-b@example.com', 1);
        $aCat  = $this->createCategory((int) $alice['id'], 'Home');
        $bCat  = $this->createCategory((int) $bob['id'], 'Bob');
        $series = $this->tasks->create((int) $alice['id'], $this->payload((int) $aCat['id'], [
            'title'           => 'Series',
            'due_date'        => '2026-09-01',
            'is_recurring'    => '1',
            'recurrence_type' => 'daily',
            'interval_value'  => '1',
        ]));
        $bobTask = $this->tasks->create((int) $bob['id'], $this->payload((int) $bCat['id'], ['title' => 'Bob task']));

        $recurring = $this->tasks->deleteOwned((int) $alice['id'], (int) $series['task']['id']);
        $this->assertFalse($recurring['ok']);
        $this->assertSame(400, $recurring['status']);
        $this->assertNotNull((new \App\Models\TaskModel())->find($series['task']['id']));

        $foreign = $this->tasks->deleteOwned((int) $alice['id'], (int) $bobTask['task']['id']);
        $this->assertFalse($foreign['ok']);
        $this->assertSame(404, $foreign['status']);
        $this->assertSame(TaskService::MSG_NOT_FOUND, $foreign['message']);
        $this->assertNotNull((new \App\Models\TaskModel())->find($bobTask['task']['id']));
    }
}
