<?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;

final class ReportersControllerTest extends AppTestCase
{
    public function testNonAdminCannotCreateReporter(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Operator);
        $response = $this->request(
            'POST',
            '/api/v1/admin/reporters',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'web-prod']) ?: null,
        );
        self::assertSame(403, $response->getStatusCode());
    }

    public function testAdminCanCreateAndFetchReporter(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);

        $created = $this->request(
            'POST',
            '/api/v1/admin/reporters',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode([
                'name' => 'web-prod-01',
                'description' => 'first webserver',
                'trust_weight' => 1.5,
            ]) ?: null,
        );
        self::assertSame(201, $created->getStatusCode());
        $body = $this->decode($created);
        self::assertSame('web-prod-01', $body['name']);
        self::assertSame(1.5, $body['trust_weight']);
        self::assertTrue($body['is_active']);

        $id = (int) $body['id'];
        $detail = $this->request('GET', "/api/v1/admin/reporters/{$id}", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $detail->getStatusCode());
        self::assertSame('web-prod-01', $this->decode($detail)['name']);
    }

    public function testCreateRejectsOutOfRangeTrustWeight(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $response = $this->request(
            'POST',
            '/api/v1/admin/reporters',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'bad', 'trust_weight' => 5.0]) ?: null,
        );
        self::assertSame(400, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('validation_failed', $body['error']);
        self::assertArrayHasKey('trust_weight', $body['details']);
    }

    public function testDuplicateNameRejected(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $this->request(
            'POST',
            '/api/v1/admin/reporters',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'dup']) ?: null,
        );
        $second = $this->request(
            'POST',
            '/api/v1/admin/reporters',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['name' => 'dup']) ?: null,
        );
        self::assertSame(400, $second->getStatusCode());
    }

    public function testPatchUpdatesFields(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $reporterId = $this->createReporter('web-edit');

        $patch = $this->request(
            'PATCH',
            "/api/v1/admin/reporters/{$reporterId}",
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['trust_weight' => 0.25, 'is_active' => false]) ?: null,
        );
        self::assertSame(200, $patch->getStatusCode());
        $body = $this->decode($patch);
        self::assertSame(0.25, $body['trust_weight']);
        self::assertFalse($body['is_active']);
    }

    public function testPatchTogglesAuditEnabled(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $reporterId = $this->createReporter('web-audit-toggle');

        // Default is true on create.
        $detail = $this->request('GET', "/api/v1/admin/reporters/{$reporterId}", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertTrue($this->decode($detail)['audit_enabled']);

        $patch = $this->request(
            'PATCH',
            "/api/v1/admin/reporters/{$reporterId}",
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['audit_enabled' => false]) ?: null,
        );
        self::assertSame(200, $patch->getStatusCode());
        self::assertFalse($this->decode($patch)['audit_enabled']);
    }

    public function testAuditEnabledToggleEmitsDedicatedAuditRow(): void
    {
        // SEC_REVIEW F41: an admin flipping `audit_enabled` for a reporter
        // must leave a flat alertable trail SOC tooling can match on with
        // `action = 'reporter.audit_toggled'` — without walking into the
        // details_json `changes` blob of the standard `reporter.updated` row.
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $reporterId = $this->createReporter('web-audit-toggle-audit');

        $this->request(
            'PATCH',
            "/api/v1/admin/reporters/{$reporterId}",
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['audit_enabled' => false]) ?: null,
        );

        $rows = $this->db->fetchAllAssociative(
            "SELECT action, details_json FROM audit_log WHERE target_type = 'reporter' AND target_id = ? ORDER BY id",
            [(string) $reporterId],
        );
        $actions = array_column($rows, 'action');
        // Both signals fire: the existing standard row plus the dedicated toggle.
        self::assertContains('reporter.updated', $actions);
        self::assertContains('reporter.audit_toggled', $actions);

        $toggleRow = null;
        foreach ($rows as $row) {
            if ($row['action'] === 'reporter.audit_toggled') {
                $toggleRow = $row;
                break;
            }
        }
        self::assertNotNull($toggleRow);
        $meta = json_decode((string) $toggleRow['details_json'], true);
        self::assertSame(true, $meta['from'] ?? null);
        self::assertSame(false, $meta['to'] ?? null);
    }

    public function testAuditEnabledNoOpDoesNotEmitDedicatedRow(): void
    {
        // PATCHing `audit_enabled` to its current value (no-op) must NOT
        // fire the toggle signal — SOC alerts would otherwise see noise.
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $reporterId = $this->createReporter('web-audit-noop');

        // Default is true; PATCH it to true (no-op).
        $this->request(
            'PATCH',
            "/api/v1/admin/reporters/{$reporterId}",
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            json_encode(['audit_enabled' => true]) ?: null,
        );

        $actions = $this->db->fetchFirstColumn(
            "SELECT action FROM audit_log WHERE target_type = 'reporter' AND target_id = ?",
            [(string) $reporterId],
        );
        self::assertNotContains('reporter.audit_toggled', $actions);
    }

    public function testDeleteWithoutReportsSoftDeletes(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $reporterId = $this->createReporter('web-disposable');

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

        $detail = $this->request('GET', "/api/v1/admin/reporters/{$reporterId}", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $detail->getStatusCode());
        self::assertFalse($this->decode($detail)['is_active']);
    }

    public function testDeleteWithReportsReturns409(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
        $reporterId = $this->createReporter('web-with-reports');

        $categoryId = (int) $this->db->fetchOne(
            'SELECT id FROM categories WHERE slug = :slug',
            ['slug' => 'brute_force']
        );

        $this->db->insert('reports', [
            'category_id' => $categoryId,
            'reporter_id' => $reporterId,
            'ip_bin' => str_repeat("\0", 12) . "\xff\xff\x01\x02",
            'ip_text' => '0.0.0.1',
            'weight_at_report' => '1.00',
            'received_at' => '2026-01-01 00:00:00',
        ]);

        $delete = $this->request('DELETE', "/api/v1/admin/reporters/{$reporterId}", [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(409, $delete->getStatusCode());
    }
}
