<?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\Infrastructure\Reputation\CidrEvaluatorFactory;
use App\Tests\Integration\Support\AppTestCase;

/**
 * Covers SPEC §6: manual blocks CRUD with split RBAC (Viewer reads, Operator
 * writes) and CIDR canonicalization. Each test boots a clean DB and Slim
 * app — no shared state.
 */
final class ManualBlocksControllerTest extends AppTestCase
{
    public function testViewerCanList(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request('GET', '/api/v1/admin/manual-blocks', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertArrayHasKey('items', $body);
        self::assertSame(0, $body['total']);
    }

    public function testViewerCannotCreate(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['kind' => 'ip', 'ip' => '203.0.113.5', 'reason' => 'x']) ?: null,
        );
        self::assertSame(403, $response->getStatusCode());
    }

    public function testOperatorCanCreateIpBlock(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
        $response = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode([
                'kind' => 'ip',
                'ip' => '198.51.100.5',
                'reason' => 'manual block test',
            ]) ?: null,
        );
        self::assertSame(201, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('ip', $body['kind']);
        self::assertSame('198.51.100.5', $body['ip']);
        self::assertNull($body['cidr']);
        self::assertArrayNotHasKey('normalized_from', $body);
    }

    public function testOperatorCanCreateCanonicalSubnetBlock(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
        $response = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['kind' => 'subnet', 'cidr' => '198.51.100.0/24', 'reason' => 'subnet']) ?: null,
        );
        self::assertSame(201, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('subnet', $body['kind']);
        self::assertSame('198.51.100.0/24', $body['cidr']);
        self::assertSame(24, $body['prefix_length']);
        self::assertArrayNotHasKey('normalized_from', $body);
    }

    public function testNonCanonicalSubnetIsAutoNormalizedAndAnnounced(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
        $response = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['kind' => 'subnet', 'cidr' => '203.0.113.55/24', 'reason' => 'non-canonical']) ?: null,
        );
        self::assertSame(201, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('203.0.113.0/24', $body['cidr']);
        self::assertSame('203.0.113.55/24', $body['normalized_from']);
    }

    public function testV6SubnetIsAccepted(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
        $response = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['kind' => 'subnet', 'cidr' => '2001:db8::/32', 'reason' => 'v6']) ?: null,
        );
        self::assertSame(201, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('2001:db8::/32', $body['cidr']);
        self::assertSame(32, $body['prefix_length']);
    }

    public function testRejectsIpKindWithCidr(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
        $response = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['kind' => 'ip', 'ip' => '1.2.3.4', 'cidr' => '1.2.3.0/24']) ?: null,
        );
        self::assertSame(400, $response->getStatusCode());
    }

    public function testRejectsBadIp(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
        $response = $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['kind' => 'ip', 'ip' => 'not-an-ip']) ?: null,
        );
        self::assertSame(400, $response->getStatusCode());
    }

    public function testListFiltersByKind(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $this->createBlock($token, ['kind' => 'ip', 'ip' => '10.0.0.1']);
        $this->createBlock($token, ['kind' => 'subnet', 'cidr' => '10.0.0.0/8']);

        $response = $this->request('GET', '/api/v1/admin/manual-blocks?kind=subnet', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        $body = $this->decode($response);
        self::assertSame(1, $body['total']);
        self::assertSame('subnet', $body['items'][0]['kind']);
    }

    public function testDeleteRemovesEntryAndInvalidatesEvaluator(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $payload = $this->decode($this->createBlock($token, ['kind' => 'subnet', 'cidr' => '10.0.0.0/16']));
        $id = (int) $payload['id'];

        /** @var CidrEvaluatorFactory $factory */
        $factory = $this->container->get(CidrEvaluatorFactory::class);
        $evaluator = $factory->get();
        self::assertCount(1, $evaluator->manualBlockedSubnets());
        self::assertTrue($evaluator->isManuallyBlocked(IpAddress::fromString('10.0.0.5')));

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

        $evaluator2 = $factory->get();
        self::assertNotSame($evaluator, $evaluator2, 'evaluator should be rebuilt after invalidate()');
        self::assertSame([], $evaluator2->manualBlockedSubnets());
        self::assertFalse($evaluator2->isManuallyBlocked(IpAddress::fromString('10.0.0.5')));
    }

    public function testEvaluatorHandlesV4Slash16AsSingleCidr(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $this->createBlock($token, ['kind' => 'subnet', 'cidr' => '10.10.0.0/16']);

        /** @var CidrEvaluatorFactory $factory */
        $factory = $this->container->get(CidrEvaluatorFactory::class);
        $subnets = $factory->get()->manualBlockedSubnets();
        self::assertCount(1, $subnets);
        self::assertSame('10.10.0.0/16', $subnets[0]->text());
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function createBlock(string $token, array $payload): \Psr\Http\Message\ResponseInterface
    {
        return $this->request(
            'POST',
            '/api/v1/admin/manual-blocks',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode($payload) ?: null,
        );
    }

}
