<?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 SPEC §M10.1: categories CRUD + in-use refusal on delete.
 */
final class CategoriesControllerTest extends AppTestCase
{
    public function testListReturnsSeededCategories(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request('GET', '/api/v1/admin/categories', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertGreaterThanOrEqual(5, $body['total']);
        $slugs = array_map(static fn (array $r): string => $r['slug'], $body['items']);
        self::assertContains('brute_force', $slugs);
    }

    public function testCreateRejectsBadSlug(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/categories',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode([
                'slug' => 'BAD-Slug',
                'name' => 'X',
                'decay_function' => 'exponential',
                'decay_param' => 14,
            ]),
        );
        self::assertSame(400, $response->getStatusCode());
        $details = $this->decode($response)['details'];
        self::assertArrayHasKey('slug', $details);
    }

    public function testCreateAndShow(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/categories',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode([
                'slug' => 'phishing',
                'name' => 'Phishing',
                'description' => 'fake-domain reports',
                'decay_function' => 'exponential',
                'decay_param' => 14,
                'is_active' => true,
            ]),
        );
        self::assertSame(201, $response->getStatusCode());
        $created = $this->decode($response);
        self::assertSame('phishing', $created['slug']);

        $get = $this->request('GET', '/api/v1/admin/categories/' . $created['id'], [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $get->getStatusCode());
        self::assertSame('phishing', $this->decode($get)['slug']);
    }

    public function testCreateRejectsDuplicateSlug(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/categories',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode([
                'slug' => 'brute_force', // already seeded
                'name' => 'Dup',
                'decay_function' => 'linear',
                'decay_param' => 30,
            ]),
        );
        self::assertSame(400, $response->getStatusCode());
        self::assertArrayHasKey('slug', $this->decode($response)['details']);
    }

    public function testPatchUpdatesFields(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $catId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'spam']);

        $response = $this->request(
            'PATCH',
            '/api/v1/admin/categories/' . $catId,
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode(['name' => 'Spam (renamed)', 'is_active' => false]),
        );
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('Spam (renamed)', $body['name']);
        self::assertFalse($body['is_active']);
    }

    public function testDeleteRefusedWhenReferencedByPolicy(): void
    {
        // Seeded policies reference all five seeded categories, so any
        // seeded category will trip the in-use guard.
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $catId = (int) $this->db->fetchOne('SELECT id FROM categories WHERE slug = :s', ['s' => 'brute_force']);

        $response = $this->request('DELETE', '/api/v1/admin/categories/' . $catId, [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(409, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('category_in_use', $body['error']);
        self::assertGreaterThan(0, $body['usage']['policies']);
    }

    public function testDeleteRefusedWhenReferencedByReports(): void
    {
        // Create a fresh category, attach a single report, then try to
        // delete it. Policy refs are zero (we didn't add it to any
        // policy); only the report ref blocks the delete.
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $createResp = $this->request(
            'POST',
            '/api/v1/admin/categories',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode([
                'slug' => 'tmp_only', 'name' => 'Tmp',
                'decay_function' => 'linear', 'decay_param' => 5,
            ]),
        );
        $catId = (int) $this->decode($createResp)['id'];
        $reporterId = $this->createReporter('rep-cat-test');

        $stmt = $this->db->prepare(
            'INSERT INTO reports (ip_bin, ip_text, category_id, reporter_id, weight_at_report, received_at) '
            . 'VALUES (:b, :t, :c, :r, :w, :now)'
        );
        $stmt->bindValue('b', IpAddress::fromString('203.0.113.5')->binary(), ParameterType::LARGE_OBJECT);
        $stmt->bindValue('t', '203.0.113.5');
        $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();

        $delete = $this->request('DELETE', '/api/v1/admin/categories/' . $catId, [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(409, $delete->getStatusCode());
        $body = $this->decode($delete);
        self::assertSame('category_in_use', $body['error']);
        self::assertSame(0, $body['usage']['policies']);
        self::assertGreaterThan(0, $body['usage']['reports']);
    }

    public function testDeleteSucceedsWhenNotInUse(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $createResp = $this->request(
            'POST',
            '/api/v1/admin/categories',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode([
                'slug' => 'orphan', 'name' => 'Orphan',
                'decay_function' => 'linear', 'decay_param' => 5,
            ]),
        );
        $catId = (int) $this->decode($createResp)['id'];

        $delete = $this->request('DELETE', '/api/v1/admin/categories/' . $catId, [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(204, $delete->getStatusCode());
    }

    public function testNonAdminCannotCreate(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request(
            'POST',
            '/api/v1/admin/categories',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode([
                'slug' => 'x', 'name' => 'X',
                'decay_function' => 'linear', 'decay_param' => 5,
            ]),
        );
        self::assertSame(403, $response->getStatusCode());
    }
}
