<?php

declare(strict_types=1);

namespace App\Tests\Integration\Admin;

use App\Domain\Auth\Role;
use App\Domain\Auth\TokenKind;
use App\Tests\Integration\Support\AppTestCase;

/**
 * Covers SPEC §M07.2: policy CRUD + preview, RBAC split (Viewer reads,
 * Admin writes), threshold replacement on PATCH, 409 on delete with
 * referencing consumers.
 */
final class PoliciesControllerTest extends AppTestCase
{
    public function testViewerCanListSeededPolicies(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request('GET', '/api/v1/admin/policies', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame(3, $body['total']); // strict, moderate, paranoid
        $names = array_map(static fn (array $p): string => $p['name'], $body['items']);
        self::assertContains('strict', $names);
        self::assertContains('moderate', $names);
        self::assertContains('paranoid', $names);
    }

    public function testShowIncludesThresholdsKeyedBySlug(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'moderate']);

        $response = $this->request('GET', "/api/v1/admin/policies/{$policyId}", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('moderate', $body['name']);
        self::assertNotEmpty($body['thresholds']);
        $slugs = array_map(static fn (array $t): string => $t['category_slug'], $body['thresholds']);
        self::assertContains('brute_force', $slugs);
    }

    public function testViewerCannotCreate(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request(
            'POST',
            '/api/v1/admin/policies',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode([
                'name' => 'aggressive',
                'thresholds' => ['brute_force' => 0.5],
            ]) ?: null,
        );
        self::assertSame(403, $response->getStatusCode());
    }

    public function testAdminCanCreatePolicyWithThresholds(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/policies',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode([
                'name' => 'aggressive',
                'description' => 'block everything',
                'include_manual_blocks' => true,
                'thresholds' => [
                    'brute_force' => 0.1,
                    'spam' => 0.2,
                ],
            ]) ?: null,
        );
        self::assertSame(201, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('aggressive', $body['name']);
        self::assertCount(2, $body['thresholds']);
    }

    public function testAdminCanCreatePolicyWithoutThresholds(): void
    {
        // The UI's create form deliberately omits the threshold matrix
        // ("configure on edit page after creation"). A policy with zero
        // thresholds is valid per SPEC §4 (absent row = category not
        // considered) and may still emit manual blocks.
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/policies',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'empty', 'include_manual_blocks' => true]) ?: null,
        );
        self::assertSame(201, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('empty', $body['name']);
        self::assertSame([], $body['thresholds']);
    }

    public function testCreateRejectsDuplicateName(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/policies',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'moderate', 'thresholds' => ['brute_force' => 1.0]]) ?: null,
        );
        self::assertSame(400, $response->getStatusCode());
        $details = $this->decode($response)['details'];
        self::assertArrayHasKey('name', $details);
    }

    public function testCreateRejectsUnknownCategorySlug(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/policies',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'bogus', 'thresholds' => ['unknown_slug' => 1.0]]) ?: null,
        );
        self::assertSame(400, $response->getStatusCode());
        $details = $this->decode($response)['details'];
        self::assertStringContainsString('unknown_slug', (string) $details['thresholds']);
    }

    public function testPatchReplacesThresholdsWholesale(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'moderate']);

        $response = $this->request(
            'PATCH',
            "/api/v1/admin/policies/{$policyId}",
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['thresholds' => ['brute_force' => 5.0]]) ?: null,
        );
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertCount(1, $body['thresholds']);
        self::assertSame('brute_force', $body['thresholds'][0]['category_slug']);
        self::assertEqualsWithDelta(5.0, $body['thresholds'][0]['threshold'], 0.0001);
    }

    public function testDeleteWithReferencingConsumerReturns409(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'moderate']);
        $this->db->insert('consumers', [
            'name' => 'fw-edge',
            'policy_id' => $policyId,
            'is_active' => 1,
        ]);

        $response = $this->request('DELETE', "/api/v1/admin/policies/{$policyId}", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(409, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('policy_in_use', $body['error']);
        self::assertNotEmpty($body['consumers']);
    }

    public function testDeleteSucceedsWithoutReferences(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $createResp = $this->request(
            'POST',
            '/api/v1/admin/policies',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'tmp', 'thresholds' => ['brute_force' => 1.0]]) ?: null,
        );
        $newId = (int) $this->decode($createResp)['id'];

        $deleteResp = $this->request('DELETE', "/api/v1/admin/policies/{$newId}", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(204, $deleteResp->getStatusCode());
    }

    public function testPreviewReturnsCountSampleAndPolicyName(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'paranoid']);

        $response = $this->request('GET', "/api/v1/admin/policies/{$policyId}/preview", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertArrayHasKey('count', $body);
        self::assertArrayHasKey('sample', $body);
        self::assertSame('paranoid', $body['policy']);
        self::assertIsArray($body['sample']);
    }

    public function testScoreDistributionReturnsBucketsAndThresholds(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :n', ['n' => 'moderate']);

        $response = $this->request('GET', "/api/v1/admin/policies/{$policyId}/score-distribution", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertArrayHasKey('buckets', $body);
        self::assertIsArray($body['buckets']);
        self::assertArrayHasKey('thresholds', $body);
        self::assertIsArray($body['thresholds']);
        self::assertArrayHasKey('bucket_size', $body);
        self::assertEqualsWithDelta(5.0, $body['bucket_size'], 0.0001);
        self::assertSame('moderate', $body['policy']);
        foreach ($body['buckets'] as $bucket) {
            self::assertArrayHasKey('start', $bucket);
            self::assertArrayHasKey('count', $bucket);
        }
    }
}
