<?php

namespace MintyPHP\Tests\Service\Audit;

use MintyPHP\Http\RequestContext;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Service\Audit\FrontendTelemetryIngestService;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Service\Security\RateLimiterService;
use MintyPHP\Service\Settings\SettingsFrontendTelemetryGateway;
use PHPUnit\Framework\TestCase;

class FrontendTelemetryIngestServiceTest extends TestCase
{
    protected function tearDown(): void
    {
        RequestContext::resetForTests();
        $_SESSION = [];
    }

    public function testIngestSkipsWhenDisabled(): void
    {
        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->never())->method('record');

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(false);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->method('all')->willReturn([]);

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
        $result = $service->ingest([
            'event_type' => 'frontend.warn_once',
            'message' => 'warn',
        ]);

        $this->assertSame(['accepted' => false, 'reason' => 'disabled'], $result);
    }

    public function testIngestRejectsInvalidPayload(): void
    {
        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->never())->method('record');

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(true);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->method('all')->willReturn([]);

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
        $result = $service->ingest([
            'event_type' => 'unknown',
            'message' => '',
        ]);

        $this->assertSame(['accepted' => false, 'reason' => 'invalid'], $result);
    }

    public function testIngestRecordsSanitizedPayload(): void
    {
        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())
            ->method('record')
            ->with(
                'frontend.warn_once',
                'success',
                $this->callback(function (array $context): bool {
                    $metadata = $context['metadata'] ?? [];
                    $message = (string) ($metadata['message'] ?? '');
                    if (!str_contains($message, '[REDACTED_EMAIL]')) {
                        return false;
                    }

                    if (!str_contains($message, 'token=[REDACTED]')) {
                        return false;
                    }

                    $meta = is_array($metadata['meta'] ?? null) ? $metadata['meta'] : [];
                    $requestPath = (string) ($meta['request_path'] ?? '');
                    return $requestPath === '/admin/users/:id';
                })
            )
            ->willReturn(5);

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
        $gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
        $gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);

        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->method('all')->willReturn(['user' => ['id' => 7]]);

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
        $result = $service->ingest([
            'event_type' => 'frontend.warn_once',
            'severity' => 'warning',
            'message' => 'Problem for user test@example.com token=supersecret1234567890',
            'fingerprint' => 'v1_warn_once_123',
            'meta' => ['request_path' => '/admin/users/123?email=a@b.de'],
            'occurred_at' => '2026-03-05T12:00:00+01:00',
        ]);

        $this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
    }

    public function testIngestBuildsFallbackFingerprintUsingRouteBeforeMessage(): void
    {
        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())
            ->method('record')
            ->with(
                'frontend.warn_once',
                'success',
                $this->callback(static function (array $context): bool {
                    $metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
                    $fingerprintHash = (string) ($metadata['fingerprint_hash'] ?? '');
                    $expectedFingerprint = 'v1_' . substr(hash('sha256', 'frontend.warn_once|/admin/users/:id|Grid init failed'), 0, 24);
                    return $fingerprintHash === RequestContext::hashValue($expectedFingerprint);
                })
            )
            ->willReturn(6);

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
        $gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
        $gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);

        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->method('all')->willReturn(['user' => ['id' => 7]]);

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
        $result = $service->ingest([
            'event_type' => 'frontend.warn_once',
            'severity' => 'warning',
            'message' => 'Grid init failed',
            'meta' => ['location' => '/admin/users/123'],
            'occurred_at' => '2026-03-05T12:00:00+01:00',
        ]);

        $this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
    }

    public function testIngestRejectsInvalidPageRequestIdPattern(): void
    {
        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())
            ->method('record')
            ->with(
                'frontend.warn_once',
                'success',
                $this->callback(static function (array $context): bool {
                    $metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
                    $meta = is_array($metadata['meta'] ?? null) ? $metadata['meta'] : [];
                    return !array_key_exists('page_request_id', $meta);
                })
            )
            ->willReturn(8);

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
        $gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
        $gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);

        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->method('all')->willReturn(['user' => ['id' => 7]]);

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
        $result = $service->ingest([
            'event_type' => 'frontend.warn_once',
            'severity' => 'warning',
            'message' => 'Invalid request id should be dropped',
            'fingerprint' => 'v1_invalid_request_id',
            'meta' => ['page_request_id' => '------------------------------------'],
        ]);

        $this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
    }

    public function testIngestUsesSingleSessionStateReadWriteForGuards(): void
    {
        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())
            ->method('record')
            ->willReturn(7);

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
        $gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
        $gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $rateLimiter->expects($this->once())
            ->method('hit')
            ->willReturn(['allowed' => true, 'retry_after' => 0]);

        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->expects($this->once())->method('all')->willReturn(['user' => ['id' => 7]]);
        $sessionStore->expects($this->once())
            ->method('get')
            ->with('frontend_telemetry_state', [])
            ->willReturn([]);
        $sessionStore->expects($this->once())
            ->method('set')
            ->with(
                'frontend_telemetry_state',
                $this->callback(static function (mixed $value): bool {
                    if (!is_array($value)) {
                        return false;
                    }
                    if (!is_array($value['dedupe'] ?? null)) {
                        return false;
                    }
                    if (!is_array($value['burst'] ?? null)) {
                        return false;
                    }
                    return true;
                })
            );

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
        $result = $service->ingest([
            'event_type' => 'frontend.warn_once',
            'severity' => 'warning',
            'message' => 'single state write',
            'fingerprint' => 'v1_single_state_write',
            'meta' => ['location' => '/admin/test'],
        ]);

        $this->assertSame(['accepted' => true, 'reason' => 'logged'], $result);
    }

    public function testIngestDropsDuplicateFingerprint(): void
    {
        $audit = $this->createMock(SystemAuditService::class);
        $audit->expects($this->once())->method('record')->willReturn(9);

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
        $gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
        $gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $rateLimiter->method('hit')->willReturn(['allowed' => true, 'retry_after' => 0]);

        $sessionData = ['user' => ['id' => 7]];
        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->method('all')->willReturnCallback(static fn (): array => $sessionData);
        $sessionStore->method('get')->willReturnCallback(static function (string $key, mixed $default = null) use (&$sessionData): mixed {
            return $sessionData[$key] ?? $default;
        });
        $sessionStore->method('set')->willReturnCallback(static function (string $key, mixed $value) use (&$sessionData): void {
            $sessionData[$key] = $value;
        });

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);
        $first = $service->ingest([
            'event_type' => 'frontend.ajax_error',
            'message' => 'AJAX request failed',
            'fingerprint' => 'v1_ajax_error_1',
        ]);
        $second = $service->ingest([
            'event_type' => 'frontend.ajax_error',
            'message' => 'AJAX request failed',
            'fingerprint' => 'v1_ajax_error_1',
        ]);

        $this->assertSame(['accepted' => true, 'reason' => 'logged'], $first);
        $this->assertSame(['accepted' => false, 'reason' => 'duplicate'], $second);
    }

    public function testIngestAppliesSessionBurstFastDropBeforeGlobalRateLimit(): void
    {
        $recordCalls = 0;
        $rateLimiterCalls = 0;

        $audit = $this->createMock(SystemAuditService::class);
        $audit->method('record')->willReturnCallback(static function () use (&$recordCalls): int {
            $recordCalls++;
            return $recordCalls;
        });

        $gateway = $this->createMock(SettingsFrontendTelemetryGateway::class);
        $gateway->method('isFrontendTelemetryEnabled')->willReturn(true);
        $gateway->method('getFrontendTelemetryAllowedEvents')->willReturn(['warn_once', 'ajax_error']);
        $gateway->method('getFrontendTelemetrySampleRate')->willReturn(1.0);

        $rateLimiter = $this->createMock(RateLimiterService::class);
        $rateLimiter->method('hit')->willReturnCallback(static function () use (&$rateLimiterCalls): array {
            $rateLimiterCalls++;
            return ['allowed' => true, 'retry_after' => 0];
        });

        $sessionData = ['user' => ['id' => 7]];
        $sessionStore = $this->createMock(SessionStoreInterface::class);
        $sessionStore->method('all')->willReturnCallback(static fn (): array => $sessionData);
        $sessionStore->method('get')->willReturnCallback(static function (string $key, mixed $default = null) use (&$sessionData): mixed {
            return $sessionData[$key] ?? $default;
        });
        $sessionStore->method('set')->willReturnCallback(static function (string $key, mixed $value) use (&$sessionData): void {
            $sessionData[$key] = $value;
        });

        $service = new FrontendTelemetryIngestService($audit, $gateway, $rateLimiter, $sessionStore);

        $lastResult = ['accepted' => true, 'reason' => 'logged'];
        for ($index = 0; $index < 120; $index++) {
            $lastResult = $service->ingest([
                'event_type' => 'frontend.ajax_error',
                'message' => 'AJAX request failed',
                'fingerprint' => 'v1_ajax_error_' . $index,
            ]);
        }

        $this->assertSame(['accepted' => false, 'reason' => 'burst_limited'], $lastResult);
        $this->assertGreaterThan(0, $recordCalls);
        $this->assertLessThan(120, $recordCalls);
        $this->assertSame($recordCalls, $rateLimiterCalls);
    }
}
