<?php

use App\Models\CategoryModel;
use App\Models\TaskModel;
use App\Services\AnalyticsService;
use App\Services\CategoryService;
use App\Services\RecurrenceService;
use App\Services\TaskService;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Tieasy;
use Tests\Support\Auth\AuthTestTrait;
use Tests\Support\Categories\CategoryTestTrait;

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

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

    private AnalyticsService $analytics;
    private TaskService $tasks;
    private RecurrenceService $recurrence;

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

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

    private function complete(int $userId, int $taskId, ?string $date = null): void
    {
        $result = $this->recurrence->completeOccurrence($userId, $this->occurrenceId($taskId, $date));
        $this->assertTrue($result['ok'] ?? false, (string) ($result['message'] ?? 'complete failed'));
    }

    public function testPercentageHelperRoundingAndZeroTotal(): void
    {
        $this->assertSame(0.0, AnalyticsService::ratioPercent(0, 0));
        $this->assertSame(0.0, AnalyticsService::ratioPercent(5, 0));
        $this->assertSame(50.0, AnalyticsService::ratioPercent(1, 2));
        $this->assertSame('0', AnalyticsService::formatPercent(0.0));
        $this->assertSame('50', AnalyticsService::formatPercent(50.0));
        $this->assertSame('100', AnalyticsService::formatPercent(100.0));
        $this->assertSame('33.3', AnalyticsService::formatPercent(AnalyticsService::ratioPercent(1, 3)));
        $this->assertSame('66.7', AnalyticsService::formatPercent(AnalyticsService::ratioPercent(2, 3)));
        $this->assertStringNotContainsString('333333', AnalyticsService::formatPercent(100 / 3));
    }

    public function testZeroTasksReturnsZeroCounts(): void
    {
        $user = $this->createUser('analytics-svc-empty@example.com', 1);
        $data = $this->analytics->build((int) $user['id']);

        $this->assertTrue($data['status']['is_empty']);
        $this->assertSame(0, $data['status']['total']);
        $this->assertSame(0, $data['status']['pending']['count']);
        $this->assertSame(0, $data['status']['completed']['count']);
        $this->assertSame('0', $data['status']['pending']['percent_label']);
        $this->assertSame('0', $data['status']['completed']['percent_label']);
        $this->assertTrue($data['priority']['is_empty']);
        $this->assertCount(4, $data['priority']['items']);
        foreach ($data['priority']['items'] as $item) {
            $this->assertSame(0, $item['count']);
            $this->assertSame('0', $item['percent_label']);
        }
        $this->assertTrue($data['category']['is_empty']);
        $this->assertSame([], $data['category']['items']);
        $this->assertTrue($data['chart']['status']['empty']);
        $this->assertTrue($data['chart']['priority']['empty']);
        $this->assertTrue($data['chart']['category']['empty']);
    }

    public function testPendingOnlyCompletedOnlyAndMixed(): void
    {
        $user = $this->createUser('analytics-svc-status@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');

        $pendingOnly = $this->analytics->statusBreakdown((int) $user['id']);
        $this->assertSame(0, $pendingOnly['total']);

        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Pending A', ['due_date' => '2026-01-01']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Pending B', ['due_date' => '2026-12-31']);
        $pending = $this->analytics->statusBreakdown((int) $user['id']);
        $this->assertSame(2, $pending['pending']['count']);
        $this->assertSame(0, $pending['completed']['count']);
        $this->assertSame('100', $pending['pending']['percent_label']);
        $this->assertSame('0', $pending['completed']['percent_label']);

        $doneUser = $this->createUser('analytics-svc-done@example.com', 1);
        $doneCat  = $this->createCategory((int) $doneUser['id'], 'Work');
        $doneTask = $this->makeTask((int) $doneUser['id'], (int) $doneCat['id'], 'Done only');
        $this->complete((int) $doneUser['id'], (int) $doneTask['id']);
        $completedOnly = $this->analytics->statusBreakdown((int) $doneUser['id']);
        $this->assertSame(0, $completedOnly['pending']['count']);
        $this->assertSame(1, $completedOnly['completed']['count']);
        $this->assertSame('100', $completedOnly['completed']['percent_label']);

        $mixedUser = $this->createUser('analytics-svc-mix@example.com', 1);
        $mixedCat  = $this->createCategory((int) $mixedUser['id'], 'Work');
        $keep      = $this->makeTask((int) $mixedUser['id'], (int) $mixedCat['id'], 'Still pending');
        $finish    = $this->makeTask((int) $mixedUser['id'], (int) $mixedCat['id'], 'Finished', ['due_date' => '2026-08-01']);
        $this->complete((int) $mixedUser['id'], (int) $finish['id']);
        $mixed = $this->analytics->statusBreakdown((int) $mixedUser['id']);
        $this->assertSame(1, $mixed['pending']['count']);
        $this->assertSame(1, $mixed['completed']['count']);
        $this->assertSame(2, $mixed['total']);
        $this->assertSame('50', $mixed['pending']['percent_label']);
        $this->assertSame('50', $mixed['completed']['percent_label']);
        $this->assertGreaterThan(0, (int) $keep['id']);
    }

    public function testDeletedOccurrenceAndSoftDeletedTaskExcluded(): void
    {
        $user = $this->createUser('analytics-svc-deleted@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $keep = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Kept');
        $tomb = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Tombstone', ['due_date' => '2026-08-01']);
        $gone = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Soft deleted', ['due_date' => '2026-08-02']);

        db_connect()->table('task_occurrences')
            ->where('id', $this->occurrenceId((int) $tomb['id']))
            ->update(['is_deleted' => 1]);
        (new TaskModel())->delete((int) $gone['id']);

        $data = $this->analytics->build((int) $user['id']);
        $this->assertSame(1, $data['status']['total']);
        $this->assertSame(1, $data['status']['pending']['count']);
        $this->assertSame((int) $keep['id'] > 0, true);
    }

    public function testAnotherUsersOccurrencesExcluded(): void
    {
        $alice = $this->createUser('analytics-svc-iso-a@example.com', 1);
        $bob   = $this->createUser('analytics-svc-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'], 'Shared title');
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Shared title');
        $this->makeTask((int) $bob['id'], (int) $bCat['id'], 'Shared title two', ['due_date' => '2026-08-01']);

        $aliceData = $this->analytics->build((int) $alice['id']);
        $bobData   = $this->analytics->build((int) $bob['id']);
        $this->assertSame(1, $aliceData['status']['total']);
        $this->assertSame(2, $bobData['status']['total']);
        $this->assertSame(1, $aliceData['category']['items'][0]['count'] ?? 0);
        $this->assertSame(2, $bobData['category']['items'][0]['count'] ?? 0);
    }

    public function testUndatedOccurrenceIsIncluded(): void
    {
        $user = $this->createUser('analytics-svc-undated@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Personal');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'No due date', ['due_date' => '']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Dated far away', ['due_date' => '2020-01-01']);

        $data = $this->analytics->build((int) $user['id']);
        $this->assertSame(2, $data['status']['total']);
        $undated = db_connect()->table('task_occurrences')
            ->where('occurrence_date', null)
            ->where('is_deleted', 0)
            ->countAllResults();
        $this->assertSame(1, $undated);
    }

    public function testRecurringOccurrencesCountIndependently(): void
    {
        $user = $this->createUser('analytics-svc-recur@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $task = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Weekly series', [
            'due_date' => '2026-09-01',
            'priority' => '2',
        ]);
        db_connect()->table('tasks')->where('id', $task['id'])->update(['is_recurring' => 1]);

        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-08', 'status' => 'completed', 'completed_at' => '2026-09-08 09:00:00']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-15', 'status' => 'completed', 'completed_at' => '2026-09-15 09:00:00']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-22', 'status' => 'pending']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-09-29', 'status' => 'pending']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-10-06', 'status' => 'pending']);
        $this->createOccurrence((int) $task['id'], ['occurrence_date' => '2026-10-13', 'status' => 'pending']);
        $this->complete((int) $user['id'], (int) $task['id'], '2026-09-01');

        $data = $this->analytics->build((int) $user['id']);
        $this->assertSame(3, $data['status']['completed']['count']);
        $this->assertSame(4, $data['status']['pending']['count']);
        $this->assertSame(7, $data['status']['total']);
        $this->assertSame(7, $data['priority']['items'][1]['count']);
        $this->assertSame('Important but Not Urgent', $data['priority']['items'][1]['label']);
        $this->assertSame(4, $data['category']['total']);
        $this->assertSame(4, $data['category']['items'][0]['count']);
    }

    public function testPriorityFourKeysLabelsFromConfigAndPercentages(): void
    {
        $user   = $this->createUser('analytics-svc-pri@example.com', 1);
        $cat    = $this->createCategory((int) $user['id'], 'Work');
        $config = new Tieasy();

        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P1a', ['priority' => '1', 'due_date' => '2026-08-01']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P1b', ['priority' => '1', 'due_date' => '2026-08-02']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P1c', ['priority' => '1', 'due_date' => '2026-08-03']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P1d', ['priority' => '1', 'due_date' => '2026-08-04']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P2a', ['priority' => '2', 'due_date' => '2026-08-05']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P2b', ['priority' => '2', 'due_date' => '2026-08-06']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P2c', ['priority' => '2', 'due_date' => '2026-08-07']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P3a', ['priority' => '3', 'due_date' => '2026-08-08']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P3b', ['priority' => '3', 'due_date' => '2026-08-09']);
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'P4a', ['priority' => '4', 'due_date' => '2026-08-10']);

        $data  = $this->analytics->priorityBreakdown((int) $user['id']);
        $items = [];
        foreach ($data['items'] as $item) {
            $items[$item['key']] = $item;
        }

        $this->assertSame(array_keys($config->priorities), array_keys($items));
        $this->assertSame($config->priorities[1], $items[1]['label']);
        $this->assertSame($config->priorities[2], $items[2]['label']);
        $this->assertSame($config->priorities[3], $items[3]['label']);
        $this->assertSame($config->priorities[4], $items[4]['label']);
        $this->assertSame(4, $items[1]['count']);
        $this->assertSame(3, $items[2]['count']);
        $this->assertSame(2, $items[3]['count']);
        $this->assertSame(1, $items[4]['count']);
        $this->assertSame(10, $data['total']);
        $this->assertSame('40', $items[1]['percent_label']);
        $this->assertSame('30', $items[2]['percent_label']);
        $this->assertSame('20', $items[3]['percent_label']);
        $this->assertSame('10', $items[4]['percent_label']);
    }

    public function testZeroCountPrioritiesStillRepresented(): void
    {
        $user = $this->createUser('analytics-svc-pri-zero@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $this->makeTask((int) $user['id'], (int) $cat['id'], 'Only P1');

        $data = $this->analytics->priorityBreakdown((int) $user['id']);
        $this->assertCount(4, $data['items']);
        $this->assertSame(1, $data['items'][0]['count']);
        $this->assertSame(0, $data['items'][1]['count']);
        $this->assertSame(0, $data['items'][2]['count']);
        $this->assertSame(0, $data['items'][3]['count']);
        $this->assertSame('100', $data['items'][0]['percent_label']);
        $this->assertSame('0', $data['items'][1]['percent_label']);
    }

    public function testDeletedOccurrencesExcludedFromPriority(): void
    {
        $user = $this->createUser('analytics-svc-pri-del@example.com', 1);
        $cat  = $this->createCategory((int) $user['id'], 'Work');
        $keep = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Keep P1');
        $drop = $this->makeTask((int) $user['id'], (int) $cat['id'], 'Drop P1', ['due_date' => '2026-08-01']);
        db_connect()->table('task_occurrences')
            ->where('id', $this->occurrenceId((int) $drop['id']))
            ->update(['is_deleted' => 1]);

        $data = $this->analytics->priorityBreakdown((int) $user['id']);
        $this->assertSame(1, $data['total']);
        $this->assertSame(1, $data['items'][0]['count']);
        $this->assertGreaterThan(0, (int) $keep['id']);
    }

    public function testCategoryPendingOnlyDenominatorAndZeroOmitted(): void
    {
        $user     = $this->createUser('analytics-svc-cat@example.com', 1);
        $personal = $this->createCategory((int) $user['id'], 'Personal');
        $work     = $this->createCategory((int) $user['id'], 'Work');
        $finance  = $this->createCategory((int) $user['id'], 'Finance');
        $health   = $this->createCategory((int) $user['id'], 'Health');

        foreach (['a', 'b', 'c', 'd', 'e', 'f'] as $i => $suffix) {
            $this->makeTask((int) $user['id'], (int) $personal['id'], 'Personal ' . $suffix, ['due_date' => '2026-07-0' . ($i + 1)]);
        }
        $this->makeTask((int) $user['id'], (int) $work['id'], 'Work a', ['due_date' => '2026-07-10']);
        $this->makeTask((int) $user['id'], (int) $work['id'], 'Work b', ['due_date' => '2026-07-11']);
        $this->makeTask((int) $user['id'], (int) $work['id'], 'Work c', ['due_date' => '2026-07-12']);
        $this->makeTask((int) $user['id'], (int) $finance['id'], 'Finance a', ['due_date' => '2026-07-13']);
        $done = $this->makeTask((int) $user['id'], (int) $health['id'], 'Health completed', ['due_date' => '2026-07-14']);
        $this->complete((int) $user['id'], (int) $done['id']);

        $data = $this->analytics->categoryPendingBreakdown((int) $user['id']);
        $this->assertSame(10, $data['total']);
        $byName = [];
        foreach ($data['items'] as $item) {
            $byName[$item['name']] = $item;
        }
        $this->assertSame(6, $byName['Personal']['count']);
        $this->assertSame('60', $byName['Personal']['percent_label']);
        $this->assertSame(3, $byName['Work']['count']);
        $this->assertSame('30', $byName['Work']['percent_label']);
        $this->assertSame(1, $byName['Finance']['count']);
        $this->assertSame('10', $byName['Finance']['percent_label']);
        $this->assertArrayNotHasKey('Health', $byName);
    }

    public function testMovedTasksCountUnderNewCategoryAndDeletedCategoryExcluded(): void
    {
        $user   = $this->createUser('analytics-svc-move@example.com', 1);
        $source = $this->createCategory((int) $user['id'], 'Old Home');
        $target = $this->createCategory((int) $user['id'], 'New Home');
        $this->makeTask((int) $user['id'], (int) $source['id'], 'Moving task');

        $moved = (new CategoryService())->moveAndDelete((int) $user['id'], (int) $source['id'], (int) $target['id']);
        $this->assertTrue($moved['ok'] ?? false, (string) ($moved['message'] ?? 'move failed'));

        $data   = $this->analytics->categoryPendingBreakdown((int) $user['id']);
        $names  = array_column($data['items'], 'name');
        $this->assertSame(['New Home'], $names);
        $this->assertSame(1, $data['items'][0]['count']);

        $goneCat = $this->createCategory((int) $user['id'], 'Doomed');
        $this->makeTask((int) $user['id'], (int) $goneCat['id'], 'Will vanish', ['due_date' => '2026-08-01']);
        $deleted = (new CategoryService())->deleteWithTasks((int) $user['id'], (int) $goneCat['id']);
        $this->assertTrue($deleted['ok'] ?? false);
        $after = $this->analytics->categoryPendingBreakdown((int) $user['id']);
        $this->assertSame(['New Home'], array_column($after['items'], 'name'));
        $this->assertNull((new CategoryModel())->find((int) $goneCat['id']));
    }

    public function testSoftDeletedCategoryWithoutTaskCleanupIsExcluded(): void
    {
        $user = $this->createUser('analytics-svc-cat-soft@example.com', 1);
        $keep = $this->createCategory((int) $user['id'], 'Keep');
        $drop = $this->createCategory((int) $user['id'], 'Drop');
        $this->makeTask((int) $user['id'], (int) $keep['id'], 'Visible');
        $this->makeTask((int) $user['id'], (int) $drop['id'], 'Hidden by category', ['due_date' => '2026-08-01']);
        (new CategoryModel())->delete((int) $drop['id']);

        $status = $this->analytics->statusBreakdown((int) $user['id']);
        $this->assertSame(1, $status['total']);
        $cats = $this->analytics->categoryPendingBreakdown((int) $user['id']);
        $this->assertSame(['Keep'], array_column($cats['items'], 'name'));
    }

    public function testEncodeForScriptEscapesQuotesAndTags(): void
    {
        $chart = [
            'category' => [
                'empty'  => false,
                'labels' => ["Client's Work", 'He said "Go"', '<img src=x onerror=alert(1)>'],
                'counts' => [1, 1, 1],
                'colors' => ['#111', '#222', '#333'],
            ],
        ];
        $json = $this->analytics->encodeForScript($chart);
        $this->assertStringContainsString('\u0027', $json);
        $this->assertStringContainsString('\u0022', $json);
        $this->assertStringContainsString('\u003C', $json);
        $this->assertStringNotContainsString("Client's Work", $json);
        $this->assertStringNotContainsString('<img', $json);
        $decoded = json_decode($json, true);
        $this->assertIsArray($decoded);
        $this->assertSame("Client's Work", $decoded['category']['labels'][0]);
        $this->assertSame('He said "Go"', $decoded['category']['labels'][1]);
        $this->assertSame('<img src=x onerror=alert(1)>', $decoded['category']['labels'][2]);
    }
}
