<?php

declare(strict_types=1);

namespace App\Tests\Integration\Audit;

use App\Domain\Auth\TokenKind;
use App\Domain\Settings\AppSettings;
use App\Tests\Integration\Support\AppTestCase;

/**
 * SPEC §M12 covers admin-side audit emission. C20 adds two public-endpoint
 * audit entries — `report.received` and `blocklist.requested` — gated by
 * runtime feature flags so the high-volume rows can be silenced without
 * restarting the api.
 */
final class PublicEndpointAuditTest extends AppTestCase
{
    public function testReportReceivedEmitsAuditWithReporterActor(): void
    {
        $reporterId = $this->createReporter('rep-audit');
        $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);

        $resp = $this->request(
            'POST',
            '/api/v1/report',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode(['ip' => '203.0.113.42', 'category' => 'brute_force', 'metadata' => ['ua' => 'curl']]),
        );
        self::assertSame(202, $resp->getStatusCode());

        $row = $this->db->fetchAssociative(
            "SELECT actor_kind, actor_id, action, target_type, target_label, details_json FROM audit_log WHERE action = 'report.received' ORDER BY id DESC LIMIT 1"
        );
        self::assertIsArray($row);
        self::assertSame('reporter', $row['actor_kind']);
        self::assertSame((string) $reporterId, $row['actor_id']);
        self::assertSame('report', $row['target_type']);
        self::assertSame('203.0.113.42', $row['target_label']);

        $details = json_decode((string) $row['details_json'], true);
        self::assertIsArray($details);
        self::assertSame('203.0.113.42', $details['ip']);
        self::assertSame('brute_force', $details['category']);
        self::assertSame($reporterId, $details['reporter_id']);
        self::assertSame('rep-audit', $details['reporter_name']);
        self::assertTrue($details['has_metadata']);
    }

    public function testReportReceivedSuppressedWhenToggleDisabled(): void
    {
        /** @var AppSettings $settings */
        $settings = $this->container->get(AppSettings::class);
        $settings->setBool(AppSettings::KEY_AUDIT_REPORT_RECEIVED_ENABLED, false);

        $reporterId = $this->createReporter('rep-quiet');
        $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);

        $resp = $this->request(
            'POST',
            '/api/v1/report',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode(['ip' => '198.51.100.7', 'category' => 'scanner']),
        );
        self::assertSame(202, $resp->getStatusCode());

        $count = (int) $this->db->fetchOne(
            "SELECT COUNT(*) FROM audit_log WHERE action = 'report.received'"
        );
        self::assertSame(0, $count);
    }

    public function testBlocklistRequestedEmitsAuditWithConsumerActor(): void
    {
        $token = $this->setupConsumerToken('moderate', 'fw-audit');
        $resp = $this->request('GET', '/api/v1/blocklist', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $resp->getStatusCode());

        $row = $this->db->fetchAssociative(
            "SELECT actor_kind, actor_id, action, target_type, target_label, details_json FROM audit_log WHERE action = 'blocklist.requested' ORDER BY id DESC LIMIT 1"
        );
        self::assertIsArray($row);
        self::assertSame('consumer', $row['actor_kind']);
        self::assertNotNull($row['actor_id']);
        self::assertSame('blocklist', $row['target_type']);
        self::assertSame('moderate', $row['target_label']);

        $details = json_decode((string) $row['details_json'], true);
        self::assertIsArray($details);
        self::assertSame('moderate', $details['policy_name']);
        self::assertSame('text', $details['format']);
        self::assertSame(200, $details['status']);
        self::assertArrayHasKey('entries', $details);
    }

    public function testBlocklist304StillEmitsAuditWithStatus304(): void
    {
        $token = $this->setupConsumerToken('moderate', 'fw-etag');
        $first = $this->request('GET', '/api/v1/blocklist', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        $etag = $first->getHeaderLine('ETag');

        $second = $this->request('GET', '/api/v1/blocklist', [
            'Authorization' => 'Bearer ' . $token,
            'If-None-Match' => $etag,
        ]);
        self::assertSame(304, $second->getStatusCode());

        $statuses = $this->db->fetchAllAssociative(
            "SELECT details_json FROM audit_log WHERE action = 'blocklist.requested' ORDER BY id ASC"
        );
        self::assertCount(2, $statuses);
        $second = json_decode((string) $statuses[1]['details_json'], true);
        self::assertIsArray($second);
        self::assertSame(304, $second['status']);
    }

    public function testBlocklistRequestedSuppressedWhenToggleDisabled(): void
    {
        /** @var AppSettings $settings */
        $settings = $this->container->get(AppSettings::class);
        $settings->setBool(AppSettings::KEY_AUDIT_BLOCKLIST_REQUEST_ENABLED, false);

        $token = $this->setupConsumerToken('moderate', 'fw-quiet');
        $resp = $this->request('GET', '/api/v1/blocklist', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $resp->getStatusCode());

        $count = (int) $this->db->fetchOne(
            "SELECT COUNT(*) FROM audit_log WHERE action = 'blocklist.requested'"
        );
        self::assertSame(0, $count);
    }

    public function testReportSuppressedWhenReporterAuditDisabled(): void
    {
        $reporterId = $this->createReporter('rep-silent');
        $this->db->update('reporters', ['audit_enabled' => 0], ['id' => $reporterId]);
        $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);

        $resp = $this->request(
            'POST',
            '/api/v1/report',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode(['ip' => '203.0.113.99', 'category' => 'spam']),
        );
        self::assertSame(202, $resp->getStatusCode());

        $count = (int) $this->db->fetchOne(
            "SELECT COUNT(*) FROM audit_log WHERE action = 'report.received'"
        );
        self::assertSame(0, $count);
    }

    public function testBlocklistSuppressedWhenConsumerAuditDisabled(): void
    {
        $token = $this->setupConsumerToken('moderate', 'fw-silent');
        $this->db->update('consumers', ['audit_enabled' => 0], ['name' => 'fw-silent']);

        $resp = $this->request('GET', '/api/v1/blocklist', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $resp->getStatusCode());

        $count = (int) $this->db->fetchOne(
            "SELECT COUNT(*) FROM audit_log WHERE action = 'blocklist.requested'"
        );
        self::assertSame(0, $count);
    }

    public function testFailedReportDoesNotEmitAudit(): void
    {
        $reporterId = $this->createReporter('rep-bad');
        $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);

        $resp = $this->request(
            'POST',
            '/api/v1/report',
            ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
            (string) json_encode(['ip' => 'not-an-ip', 'category' => 'spam']),
        );
        self::assertSame(400, $resp->getStatusCode());

        $count = (int) $this->db->fetchOne(
            "SELECT COUNT(*) FROM audit_log WHERE action = 'report.received'"
        );
        self::assertSame(0, $count);
    }

    private function setupConsumerToken(string $policyName, string $consumerName): string
    {
        $policyId = (int) $this->db->fetchOne(
            'SELECT id FROM policies WHERE name = :n',
            ['n' => $policyName],
        );
        $this->db->insert('consumers', [
            'name' => $consumerName,
            'policy_id' => $policyId,
            'is_active' => 1,
        ]);
        $consumerId = (int) $this->db->lastInsertId();

        return $this->createToken(TokenKind::Consumer, consumerId: $consumerId);
    }
}
