<?php

declare(strict_types=1);

namespace App\Tests\Integration\Admin;

use App\Domain\Auth\Role;
use App\Domain\Auth\TokenKind;
use App\Domain\Ip\IpAddress;
use App\Tests\Integration\Support\AppTestCase;
use Doctrine\DBAL\ParameterType;

/**
 * Covers the dashboard endpoint shape: scalar counters, by-hour
 * histogram, top reporters/categories, jobs status. Cache TTL is 30s
 * but the test issues each request with fresh state — we don't
 * exercise the cache window directly here.
 */
final class StatsControllerTest extends AppTestCase
{
    public function testDashboardEmptyShape(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request('GET', '/api/v1/admin/stats/dashboard', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);

        foreach (['active_blocks', 'manual_blocks_count', 'allowlist_count', 'reports_24h'] as $k) {
            self::assertArrayHasKey($k, $body);
            self::assertSame(0, $body[$k]);
        }
        self::assertSame([], $body['reports_24h_by_hour']);
        self::assertSame([], $body['top_reporters_24h']);
        self::assertSame([], $body['top_categories_24h']);
        // blocked_ips_by_day_7d always emits 8 days (today + the 7 prior, zero-filled).
        self::assertArrayHasKey('blocked_ips_by_day_7d', $body);
        $blocked = $body['blocked_ips_by_day_7d'];
        self::assertArrayHasKey('days', $blocked);
        self::assertArrayHasKey('series', $blocked);
        self::assertCount(8, $blocked['days']);
        // Seeders create the default categories — every active one should
        // appear as a flat-zero series even with no reports yet.
        self::assertNotEmpty($blocked['series']);
        foreach ($blocked['series'] as $row) {
            self::assertArrayHasKey('category', $row);
            self::assertArrayHasKey('counts', $row);
            self::assertCount(8, $row['counts']);
            self::assertSame(0, array_sum(array_map('intval', $row['counts'])));
        }
        self::assertSame('moderate', $body['reference_policy']);
    }

    public function testDashboardCountsFromSeededReports(): void
    {
        // Use a fresh class-level container/session per test (setUp does this).
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $reporterId = $this->createReporter('rep-stats');

        for ($i = 0; $i < 3; ++$i) {
            $this->seedReport(sprintf('203.0.113.%d', 10 + $i), 'brute_force', $reporterId);
        }
        $this->seedReport('203.0.113.20', 'spam', $reporterId);

        $body = $this->decode($this->request('GET', '/api/v1/admin/stats/dashboard', [
            'Authorization' => 'Bearer ' . $token,
        ]));

        self::assertSame(4, $body['reports_24h']);
        self::assertNotEmpty($body['reports_24h_by_hour']);
        self::assertSame('rep-stats', $body['top_reporters_24h'][0]['name']);
        self::assertSame(4, $body['top_reporters_24h'][0]['count']);
        $catSlugs = array_map(static fn (array $r): string => $r['slug'], $body['top_categories_24h']);
        self::assertContains('brute_force', $catSlugs);
        self::assertContains('spam', $catSlugs);
    }

    public function testManualBlocksAndAllowlistCounters(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $this->db->insert('manual_blocks', [
            'kind' => 'ip',
            'ip_bin' => IpAddress::fromString('203.0.113.5')->binary(),
            'reason' => 'x',
        ], ['ip_bin' => ParameterType::LARGE_OBJECT]);
        $this->db->insert('allowlist', [
            'kind' => 'ip',
            'ip_bin' => IpAddress::fromString('203.0.113.6')->binary(),
            'reason' => 'y',
        ], ['ip_bin' => ParameterType::LARGE_OBJECT]);

        $body = $this->decode($this->request('GET', '/api/v1/admin/stats/dashboard', [
            'Authorization' => 'Bearer ' . $token,
        ]));

        self::assertSame(1, $body['manual_blocks_count']);
        self::assertSame(1, $body['allowlist_count']);
    }

    public function testBlockedIpsByDayBucketsByCategory(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $reporterId = $this->createReporter('rep-cat-bucket');

        // Two distinct IPs in `brute_force`, one in `spam`. The same IP
        // reported twice in `brute_force` must still count as one (the
        // sum is COUNT DISTINCT ip_bin per category per day).
        $this->seedReport('203.0.113.10', 'brute_force', $reporterId);
        $this->seedReport('203.0.113.10', 'brute_force', $reporterId);
        $this->seedReport('203.0.113.11', 'brute_force', $reporterId);
        $this->seedReport('203.0.113.20', 'spam', $reporterId);

        $body = $this->decode($this->request('GET', '/api/v1/admin/stats/dashboard', [
            'Authorization' => 'Bearer ' . $token,
        ]));

        $blocked = $body['blocked_ips_by_day_7d'];
        self::assertCount(8, $blocked['days']);

        $byCategory = [];
        foreach ($blocked['series'] as $row) {
            $byCategory[$row['category']] = array_sum(array_map('intval', $row['counts']));
        }
        self::assertArrayHasKey('brute_force', $byCategory);
        self::assertArrayHasKey('spam', $byCategory);
        self::assertSame(2, $byCategory['brute_force']);
        self::assertSame(1, $byCategory['spam']);
    }

    private function seedReport(string $ip, string $categorySlug, int $reporterId): void
    {
        $catId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => $categorySlug]);
        $ipObj = IpAddress::fromString($ip);
        $stmt = $this->db->prepare(
            'INSERT INTO reports (ip_bin, ip_text, category_id, reporter_id, weight_at_report, received_at, metadata_json) '
            . 'VALUES (:b, :t, :c, :r, :w, :now, NULL)'
        );
        $stmt->bindValue('b', $ipObj->binary(), ParameterType::LARGE_OBJECT);
        $stmt->bindValue('t', $ipObj->text());
        $stmt->bindValue('c', $catId, ParameterType::INTEGER);
        $stmt->bindValue('r', $reporterId, ParameterType::INTEGER);
        $stmt->bindValue('w', '1.00');
        $stmt->bindValue('now', (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s'));
        $stmt->executeStatement();
    }
}
