<?php

declare(strict_types=1);

use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
use App\Framework\Logging\Processors\ExceptionProcessor;
use App\Framework\Logging\ValueObjects\LogContext;

describe('ExceptionProcessor', function () {
    beforeEach(function () {
        $this->timestamp = new DateTimeImmutable('2024-01-15 10:30:45', new DateTimeZone('Europe/Berlin'));
        $this->processor = new ExceptionProcessor();
    });

    describe('constructor', function () {
        it('can be instantiated with default config', function () {
            $processor = new ExceptionProcessor();

            expect($processor instanceof ExceptionProcessor)->toBeTrue();
        });

        it('can be instantiated with custom config', function () {
            $processor = new ExceptionProcessor(
                includeStackTraces: false,
                traceDepth: 5
            );

            expect($processor instanceof ExceptionProcessor)->toBeTrue();
        });
    });

    describe('getPriority()', function () {
        it('returns priority 15', function () {
            expect($this->processor->getPriority())->toBe(15);
        });
    });

    describe('getName()', function () {
        it('returns name exception', function () {
            expect($this->processor->getName())->toBe('exception');
        });
    });

    describe('processRecord()', function () {
        it('returns record unchanged when no exception present', function () {
            $record = new LogRecord(
                message: 'Test message',
                context: LogContext::empty(),
                level: LogLevel::INFO,
                timestamp: $this->timestamp
            );

            $processed = $this->processor->processRecord($record);

            expect($processed->getExtras())->toBeEmpty();
        });

        it('formats basic exception information', function () {
            $exception = new RuntimeException('Test error', 123);

            $record = new LogRecord(
                message: 'Error occurred',
                context: LogContext::withData(['exception' => $exception]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $this->processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');

            expect($exceptionData)->toBeArray();
            expect($exceptionData['class'])->toBe('RuntimeException');
            expect($exceptionData['message'])->toBe('Test error');
            expect($exceptionData['code'])->toBe(123);
            expect(isset($exceptionData['file']))->toBeTrue();
            expect(isset($exceptionData['line']))->toBeTrue();
        });

        it('includes stack trace by default', function () {
            $exception = new Exception('Test exception');

            $record = new LogRecord(
                message: 'Error with trace',
                context: LogContext::withData(['exception' => $exception]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $this->processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');

            expect(isset($exceptionData['trace']))->toBeTrue();
            expect($exceptionData['trace'])->toBeArray();
        });

        it('excludes stack trace when disabled', function () {
            $processor = new ExceptionProcessor(includeStackTraces: false);
            $exception = new Exception('Test exception');

            $record = new LogRecord(
                message: 'Error without trace',
                context: LogContext::withData(['exception' => $exception]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');

            expect(isset($exceptionData['trace']))->toBeFalse();
        });

        it('handles nested exceptions', function () {
            $innerException = new InvalidArgumentException('Inner error');
            $outerException = new RuntimeException('Outer error', 0, $innerException);

            $record = new LogRecord(
                message: 'Nested exception',
                context: LogContext::withData(['exception' => $outerException]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $this->processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');

            expect($exceptionData['class'])->toBe('RuntimeException');
            expect(isset($exceptionData['previous']))->toBeTrue();
            expect($exceptionData['previous']['class'])->toBe('InvalidArgumentException');
            expect($exceptionData['previous']['message'])->toBe('Inner error');
        });

        it('limits stack trace depth', function () {
            $processor = new ExceptionProcessor(traceDepth: 3);
            $exception = new Exception('Deep exception');

            $record = new LogRecord(
                message: 'Deep trace',
                context: LogContext::withData(['exception' => $exception]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');

            expect(isset($exceptionData['trace']))->toBeTrue();
            expect(count($exceptionData['trace']))->toBeLessThanOrEqual(3);
        });

        it('formats stack trace entries correctly', function () {
            $exception = new Exception('Test exception');

            $record = new LogRecord(
                message: 'Error with trace',
                context: LogContext::withData(['exception' => $exception]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $this->processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');
            $trace = $exceptionData['trace'];

            expect($trace)->toBeArray();
            if (count($trace) > 0) {
                $firstFrame = $trace[0];
                expect(isset($firstFrame['file']))->toBeTrue();
                expect(isset($firstFrame['line']))->toBeTrue();
            }
        });

        it('includes function information in stack trace', function () {
            $exception = new Exception('Test exception');

            $record = new LogRecord(
                message: 'Error',
                context: LogContext::withData(['exception' => $exception]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $this->processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');
            $trace = $exceptionData['trace'];

            // At least one frame should have function info
            $hasFunctionInfo = false;
            foreach ($trace as $frame) {
                if (isset($frame['function'])) {
                    $hasFunctionInfo = true;
                    break;
                }
            }

            expect($hasFunctionInfo)->toBeTrue();
        });

        it('handles exception without previous exception', function () {
            $exception = new Exception('Single exception');

            $record = new LogRecord(
                message: 'Single error',
                context: LogContext::withData(['exception' => $exception]),
                level: LogLevel::ERROR,
                timestamp: $this->timestamp
            );

            $processed = $this->processor->processRecord($record);

            $exceptionData = $processed->getExtra('exception');

            expect(isset($exceptionData['previous']))->toBeFalse();
        });
    });

    describe('readonly behavior', function () {
        it('is a final class', function () {
            $reflection = new ReflectionClass(ExceptionProcessor::class);

            expect($reflection->isFinal())->toBeTrue();
        });
    });
});
