<?php

declare(strict_types=1);

use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\ExceptionMetadata;
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\Core\AuthErrorCode;
use App\Framework\Exception\Core\ValidationErrorCode;
use App\Framework\Exception\Core\ErrorSeverity;

describe('FrameworkException', function () {
    describe('Factory Methods', function () {
        it('creates simple exception without error code', function () {
            $exception = FrameworkException::simple('Test error');

            expect($exception->getMessage())->toBe('Test error');
            expect($exception->getErrorCode())->toBeNull();
            expect($exception->getContext())->toBeInstanceOf(ExceptionContext::class);
        });

        it('creates exception with error code', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::CONNECTION_FAILED,
                'Database connection failed'
            );

            expect($exception->getMessage())->toBe('Database connection failed');
            expect($exception->getErrorCode())->toBe(DatabaseErrorCode::CONNECTION_FAILED);
            expect($exception->getSeverity())->toBe(ErrorSeverity::CRITICAL);
        });

        it('uses error code description as default message', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::QUERY_FAILED
            );

            expect($exception->getMessage())->toBe('Database query execution failed');
        });

        it('creates exception for operation', function () {
            $exception = FrameworkException::forOperation(
                'database.query',
                'UserRepository',
                'Query execution failed',
                DatabaseErrorCode::QUERY_FAILED
            );

            expect($exception->getMessage())->toBe('Query execution failed');
            expect($exception->getContext()->operation)->toBe('database.query');
            expect($exception->getContext()->component)->toBe('UserRepository');
        });

        it('creates exception from context', function () {
            $context = ExceptionContext::forOperation('test.operation', 'TestComponent')
                ->withData(['key' => 'value']);

            $exception = FrameworkException::fromContext(
                'Test message',
                $context,
                ValidationErrorCode::INVALID_INPUT
            );

            expect($exception->getMessage())->toBe('Test message');
            expect($exception->getContext())->toBe($context);
            expect($exception->getErrorCode())->toBe(ValidationErrorCode::INVALID_INPUT);
        });
    });

    describe('ErrorCode Integration', function () {
        it('provides severity from error code', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::CONNECTION_FAILED
            );

            expect($exception->getSeverity())->toBe(ErrorSeverity::CRITICAL);
        });

        it('provides recovery hint from error code', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::CONNECTION_FAILED
            );

            expect($exception->getRecoveryHint())
                ->toBe('Check database server status and connection settings');
        });

        it('identifies recoverable exceptions', function () {
            $recoverable = FrameworkException::create(
                DatabaseErrorCode::TIMEOUT
            );
            $nonRecoverable = FrameworkException::create(
                DatabaseErrorCode::MIGRATION_ROLLBACK_FAILED
            );

            expect($recoverable->isRecoverable())->toBeTrue();
            expect($nonRecoverable->isRecoverable())->toBeFalse();
        });

        it('provides retry after seconds from error code', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::CONNECTION_FAILED
            );

            expect($exception->getRetryAfter())->toBe(30);
        });

        it('checks error code identity', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::QUERY_FAILED
            );

            expect($exception->isErrorCode(DatabaseErrorCode::QUERY_FAILED))->toBeTrue();
            expect($exception->isErrorCode(DatabaseErrorCode::CONNECTION_FAILED))->toBeFalse();
        });

        it('checks error code category', function () {
            $dbException = FrameworkException::create(DatabaseErrorCode::QUERY_FAILED);
            $authException = FrameworkException::create(AuthErrorCode::TOKEN_EXPIRED);

            expect($dbException->isCategory('DB'))->toBeTrue();
            expect($dbException->isCategory('AUTH'))->toBeFalse();
            expect($authException->isCategory('AUTH'))->toBeTrue();
        });
    });

    describe('ExceptionMetadata Integration', function () {
        it('initializes with default metadata', function () {
            $exception = FrameworkException::simple('Test');

            expect($exception->getMetadata())->toBeInstanceOf(ExceptionMetadata::class);
            expect($exception->shouldAggregate())->toBeTrue();
            expect($exception->shouldReport())->toBeTrue();
            expect($exception->wasReported())->toBeFalse();
        });

        it('uses retry after from error code', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::CONNECTION_FAILED
            );

            expect($exception->getRetryAfter())->toBe(30);
        });

        it('allows custom retry after override', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::QUERY_FAILED
            )->withRetryAfter(60);

            expect($exception->getRetryAfter())->toBe(60);
        });

        it('can skip aggregation', function () {
            $exception = FrameworkException::simple('Test')
                ->skipAggregation();

            expect($exception->shouldAggregate())->toBeFalse();
            expect($exception->shouldReport())->toBeTrue();
        });

        it('can skip reporting', function () {
            $exception = FrameworkException::simple('Test')
                ->skipReporting();

            expect($exception->shouldAggregate())->toBeTrue();
            expect($exception->shouldReport())->toBeFalse();
        });

        it('can skip both aggregation and reporting', function () {
            $exception = FrameworkException::simple('Test')
                ->skipAll();

            expect($exception->shouldAggregate())->toBeFalse();
            expect($exception->shouldReport())->toBeFalse();
        });

        it('tracks reported status', function () {
            $exception = FrameworkException::simple('Test');
            expect($exception->wasReported())->toBeFalse();

            $exception->markAsReported();
            expect($exception->wasReported())->toBeTrue();
            expect($exception->shouldReport())->toBeFalse();
        });
    });

    describe('Context Enrichment', function () {
        it('enriches exception with operation context', function () {
            $exception = FrameworkException::simple('Test')
                ->withOperation('database.query', 'UserRepository');

            expect($exception->getContext()->operation)->toBe('database.query');
            expect($exception->getContext()->component)->toBe('UserRepository');
        });

        it('enriches exception with data', function () {
            $exception = FrameworkException::simple('Test')
                ->withData(['user_id' => 123, 'action' => 'login']);

            expect($exception->getContext()->data)->toBe(['user_id' => 123, 'action' => 'login']);
        });

        it('enriches exception with debug info', function () {
            $exception = FrameworkException::simple('Test')
                ->withDebug(['query' => 'SELECT * FROM users', 'params' => [1]]);

            expect($exception->getContext()->debug)->toHaveKey('query');
        });

        it('enriches exception with context metadata', function () {
            $exception = FrameworkException::simple('Test')
                ->withContextMetadata(['request_id' => 'abc123']);

            expect($exception->getContext()->metadata)->toBe(['request_id' => 'abc123']);
        });

        it('chains context enrichment methods', function () {
            $exception = FrameworkException::simple('Test')
                ->withOperation('auth.login', 'AuthService')
                ->withData(['username' => 'john'])
                ->withDebug(['ip' => '127.0.0.1'])
                ->skipAggregation();

            expect($exception->getContext()->operation)->toBe('auth.login');
            expect($exception->getContext()->component)->toBe('AuthService');
            expect($exception->getContext()->data['username'])->toBe('john');
            expect($exception->shouldAggregate())->toBeFalse();
        });
    });

    describe('Immutability', function () {
        it('creates new instance when modifying context', function () {
            $original = FrameworkException::simple('Test');
            $modified = $original->withData(['key' => 'value']);

            expect($original)->not->toBe($modified);
            expect($original->getContext()->data)->toBe([]);
            expect($modified->getContext()->data)->toBe(['key' => 'value']);
        });

        it('creates new instance when modifying metadata', function () {
            $original = FrameworkException::simple('Test');
            $modified = $original->skipAggregation();

            expect($original)->not->toBe($modified);
            expect($original->shouldAggregate())->toBeTrue();
            expect($modified->shouldAggregate())->toBeFalse();
        });

        it('creates new instance when setting error code', function () {
            $original = FrameworkException::simple('Test');
            $modified = $original->withErrorCode(DatabaseErrorCode::QUERY_FAILED);

            expect($original)->not->toBe($modified);
            expect($original->getErrorCode())->toBeNull();
            expect($modified->getErrorCode())->toBe(DatabaseErrorCode::QUERY_FAILED);
        });
    });

    describe('Array Serialization', function () {
        it('serializes to array with error code info', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::QUERY_FAILED,
                'Test query failed'
            )->withData(['table' => 'users']);

            $array = $exception->toArray();

            expect($array)->toHaveKey('class');
            expect($array)->toHaveKey('message');
            expect($array)->toHaveKey('error_code');
            expect($array)->toHaveKey('error_category');
            expect($array)->toHaveKey('description');
            expect($array)->toHaveKey('recovery_hint');
            expect($array)->toHaveKey('is_recoverable');
            expect($array['error_code'])->toBe('DB002');
            expect($array['error_category'])->toBe('DB');
        });

        it('serializes without error code', function () {
            $exception = FrameworkException::simple('Test error');
            $array = $exception->toArray();

            expect($array)->toHaveKey('message');
            expect($array)->not->toHaveKey('error_code');
        });
    });

    describe('String Representation', function () {
        it('includes error code in string representation', function () {
            $exception = FrameworkException::create(
                DatabaseErrorCode::QUERY_FAILED,
                'Test query failed'
            );

            $string = (string) $exception;

            expect($string)->toContain('[DB002]');
            expect($string)->toContain('Test query failed');
        });

        it('works without error code', function () {
            $exception = FrameworkException::simple('Test error');
            $string = (string) $exception;

            expect($string)->toContain('Test error');
            expect($string)->not->toContain('[');
        });
    });

    describe('Previous Exception Chain', function () {
        it('wraps previous exception', function () {
            $previous = new \RuntimeException('Original error');
            $exception = FrameworkException::simple('Wrapped error', $previous);

            expect($exception->getPrevious())->toBe($previous);
        });

        it('chains with error code', function () {
            $previous = new \PDOException('Connection refused');
            $exception = FrameworkException::create(
                DatabaseErrorCode::CONNECTION_FAILED,
                'Database unavailable',
                previous: $previous
            );

            expect($exception->getPrevious())->toBe($previous);
            expect($exception->getErrorCode())->toBe(DatabaseErrorCode::CONNECTION_FAILED);
        });
    });

    describe('Multiple Error Code Categories', function () {
        it('handles database errors', function () {
            $exception = FrameworkException::create(DatabaseErrorCode::TIMEOUT);

            expect($exception->isCategory('DB'))->toBeTrue();
            expect($exception->getSeverity())->toBe(ErrorSeverity::ERROR);
            expect($exception->getRetryAfter())->toBe(60);
        });

        it('handles authentication errors', function () {
            $exception = FrameworkException::create(AuthErrorCode::TOKEN_EXPIRED);

            expect($exception->isCategory('AUTH'))->toBeTrue();
            expect($exception->getSeverity())->toBe(ErrorSeverity::WARNING);
            expect($exception->getRetryAfter())->toBe(0);
        });

        it('handles validation errors', function () {
            $exception = FrameworkException::create(ValidationErrorCode::BUSINESS_RULE_VIOLATION);

            expect($exception->isCategory('VAL'))->toBeTrue();
            expect($exception->getSeverity())->toBe(ErrorSeverity::ERROR);
            expect($exception->isRecoverable())->toBeTrue();
        });
    });
});
