<?php

declare(strict_types=1);

use App\Framework\Database\Exception\ConnectionFailedException;
use App\Framework\Database\Exception\ConstraintViolationException;
use App\Framework\Database\Exception\DeadlockException;
use App\Framework\Database\Exception\PoolExhaustedException;
use App\Framework\Database\Exception\QueryExecutionException;
use App\Framework\Database\Exception\QuerySyntaxException;
use App\Framework\Database\ValueObjects\SqlState;
use App\Framework\Exception\Core\DatabaseErrorCode;

describe('SQLSTATE Exception Classes', function () {
    describe('ConnectionFailedException', function () {
        it('creates exception for connection failure', function () {
            $sqlState = new SqlState('08001');
            $exception = ConnectionFailedException::cannotConnect(
                'localhost',
                'testdb',
                $sqlState,
                'Connection refused'
            );

            expect($exception->getMessage())->toContain('localhost')
                ->and($exception->getMessage())->toContain('testdb')
                ->and($exception->getSqlState())->toBe($sqlState)
                ->and($exception->getErrorCode())->toBe(DatabaseErrorCode::CONNECTION_FAILED)
                ->and($exception->getContext()->data['host'])->toBe('localhost')
                ->and($exception->getContext()->data['database'])->toBe('testdb');
        });

        it('creates exception for server rejection', function () {
            $sqlState = new SqlState('08004');
            $exception = ConnectionFailedException::serverRejectedConnection(
                'db.example.com',
                'dbuser',
                $sqlState,
                'Invalid credentials'
            );

            expect($exception->getMessage())->toContain('db.example.com')
                ->and($exception->getMessage())->toContain('dbuser')
                ->and($exception->getSqlState()->code)->toBe('08004')
                ->and($exception->getContext()->data['host'])->toBe('db.example.com')
                ->and($exception->getContext()->data['username'])->toBe('dbuser');
        });

        it('creates exception for connection timeout', function () {
            $sqlState = new SqlState('HY000');
            $exception = ConnectionFailedException::connectionTimeout(
                'remote-db.com',
                30,
                $sqlState
            );

            expect($exception->getMessage())->toContain('30 seconds')
                ->and($exception->getContext()->data['timeout_seconds'])->toBe(30)
                ->and($exception->getContext()->data['host'])->toBe('remote-db.com');
        });

        it('creates exception for too many connections', function () {
            $sqlState = new SqlState('HY000');
            $exception = ConnectionFailedException::tooManyConnections(
                'localhost',
                100,
                $sqlState
            );

            expect($exception->getMessage())->toContain('100')
                ->and($exception->getContext()->data['max_connections'])->toBe(100)
                ->and($exception->getContext()->data['host'])->toBe('localhost');
        });
    });

    describe('ConstraintViolationException', function () {
        it('creates exception for unique violation', function () {
            $sqlState = new SqlState('23505');
            $exception = ConstraintViolationException::uniqueViolation(
                'users',
                'email',
                'test@example.com',
                $sqlState
            );

            expect($exception->getMessage())->toContain('email')
                ->and($exception->getMessage())->toContain('users')
                ->and($exception->getSqlState())->toBe($sqlState)
                ->and($exception->getErrorCode())->toBe(DatabaseErrorCode::CONSTRAINT_VIOLATION)
                ->and($exception->getContext()->data['table'])->toBe('users')
                ->and($exception->getContext()->data['column'])->toBe('email');
        });

        it('creates exception for foreign key violation', function () {
            $sqlState = new SqlState('23503');
            $exception = ConstraintViolationException::foreignKeyViolation(
                'orders',
                'user_id',
                'users',
                999,
                $sqlState
            );

            expect($exception->getMessage())->toContain('orders')
                ->and($exception->getMessage())->toContain('users')
                ->and($exception->getContext()->data['table'])->toBe('orders')
                ->and($exception->getContext()->data['foreign_key'])->toBe('user_id')
                ->and($exception->getContext()->data['referenced_table'])->toBe('users');
        });

        it('creates exception for not null violation', function () {
            $sqlState = new SqlState('23502');
            $exception = ConstraintViolationException::notNullViolation(
                'products',
                'price',
                $sqlState
            );

            expect($exception->getMessage())->toContain('price')
                ->and($exception->getMessage())->toContain('products')
                ->and($exception->getContext()->data['table'])->toBe('products')
                ->and($exception->getContext()->data['column'])->toBe('price');
        });

        it('creates exception for composite unique violation', function () {
            $sqlState = new SqlState('23505');
            $exception = ConstraintViolationException::compositeUniqueViolation(
                'user_permissions',
                ['user_id', 'permission_id'],
                [123, 456],
                $sqlState
            );

            expect($exception->getMessage())->toContain('user_id')
                ->and($exception->getMessage())->toContain('permission_id')
                ->and($exception->getContext()->data['columns'])->toBe(['user_id', 'permission_id'])
                ->and($exception->getContext()->debug['duplicate_values'])->toBe(['123', '456']);
        });
    });

    describe('DeadlockException', function () {
        it('creates exception for basic deadlock detection', function () {
            $sqlState = new SqlState('40001');
            $exception = DeadlockException::detected($sqlState);

            expect($exception->getMessage())->toContain('Deadlock detected')
                ->and($exception->getSqlState())->toBe($sqlState)
                ->and($exception->getErrorCode())->toBe(DatabaseErrorCode::DEADLOCK_DETECTED)
                ->and($exception->getContext()->data['recoverable'])->toBeTrue()
                ->and($exception->getContext()->data['retry_recommended'])->toBeTrue();
        });

        it('creates exception with transaction details', function () {
            $sqlState = new SqlState('40001');
            $exception = DeadlockException::inTransaction(
                'tx_12345',
                $sqlState,
                attemptNumber: 2
            );

            expect($exception->getMessage())->toContain('tx_12345')
                ->and($exception->getMessage())->toContain('attempt 2')
                ->and($exception->getContext()->data['transaction_id'])->toBe('tx_12345')
                ->and($exception->getContext()->data['attempt_number'])->toBe(2);
        });

        it('creates exception for max retries exceeded', function () {
            $sqlState = new SqlState('40001');
            $exception = DeadlockException::maxRetriesExceeded(
                3,
                $sqlState,
                'tx_99999'
            );

            expect($exception->getMessage())->toContain('3 retry attempts')
                ->and($exception->getContext()->data['recoverable'])->toBeFalse()
                ->and($exception->getContext()->data['retry_recommended'])->toBeFalse()
                ->and($exception->getContext()->data['max_attempts'])->toBe(3);
        });

        it('calculates retry delay with exponential backoff', function () {
            $delay1 = DeadlockException::getRetryDelay(1);
            $delay2 = DeadlockException::getRetryDelay(2);
            $delay3 = DeadlockException::getRetryDelay(3);

            // Test basic properties of exponential backoff (jitter makes exact comparisons unreliable)
            expect($delay1)->toBeGreaterThan(0)
                ->and($delay2)->toBeGreaterThan(0)
                ->and($delay3)->toBeGreaterThan(0)
                ->and($delay3)->toBeLessThanOrEqual(5000); // Max 5 seconds
        });

        it('determines retry eligibility', function () {
            expect(DeadlockException::shouldRetry(1, 3))->toBeTrue()
                ->and(DeadlockException::shouldRetry(2, 3))->toBeTrue()
                ->and(DeadlockException::shouldRetry(3, 3))->toBeFalse()
                ->and(DeadlockException::shouldRetry(4, 3))->toBeFalse();
        });
    });

    describe('QuerySyntaxException', function () {
        it('creates exception for basic syntax error', function () {
            $sqlState = new SqlState('42601');
            $exception = QuerySyntaxException::forQuery(
                'SELCT * FROM users',
                $sqlState,
                'Syntax error near "SELCT"'
            );

            expect($exception->getMessage())->toContain('Syntax error')
                ->and($exception->getSqlState())->toBe($sqlState)
                ->and($exception->getErrorCode())->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR);
        });

        it('creates exception for table not found', function () {
            $sqlState = new SqlState('42P01');
            $exception = QuerySyntaxException::tableNotFound(
                'nonexistent_table',
                $sqlState
            );

            expect($exception->getMessage())->toContain('nonexistent_table')
                ->and($exception->getContext()->data['table_name'])->toBe('nonexistent_table');
        });

        it('creates exception for column not found', function () {
            $sqlState = new SqlState('42703');
            $exception = QuerySyntaxException::columnNotFound(
                'invalid_column',
                'users',
                $sqlState
            );

            expect($exception->getMessage())->toContain('users.invalid_column')
                ->and($exception->getContext()->data['column_name'])->toBe('invalid_column')
                ->and($exception->getContext()->data['table_name'])->toBe('users');
        });

        it('creates exception for insufficient privileges', function () {
            $sqlState = new SqlState('42501');
            $exception = QuerySyntaxException::insufficientPrivileges(
                'DELETE',
                'sensitive_data',
                $sqlState
            );

            expect($exception->getMessage())->toContain('DELETE')
                ->and($exception->getMessage())->toContain('sensitive_data')
                ->and($exception->getContext()->data['operation'])->toBe('DELETE')
                ->and($exception->getContext()->data['object_name'])->toBe('sensitive_data');
        });

        it('truncates long queries for safety', function () {
            $longQuery = str_repeat('SELECT * FROM users WHERE id = 1 AND ', 20) . 'name = "test"';
            $sqlState = new SqlState('42601');
            $exception = QuerySyntaxException::forQuery($longQuery, $sqlState);

            expect(strlen($exception->getContext()->data['query']))->toBeLessThanOrEqual(220); // 200 + " (truncated)"
        });
    });

    describe('QueryExecutionException', function () {
        it('creates exception for data type mismatch', function () {
            $sqlState = new SqlState('22P02');
            $exception = QueryExecutionException::forDataTypeMismatch(
                'age',
                'integer',
                'not_a_number',
                $sqlState
            );

            expect($exception->getMessage())->toContain('age')
                ->and($exception->getMessage())->toContain('integer')
                ->and($exception->getContext()->data['column'])->toBe('age')
                ->and($exception->getContext()->data['expected_type'])->toBe('integer');
        });

        it('creates exception for data truncation', function () {
            $sqlState = new SqlState('22001');
            $exception = QueryExecutionException::forDataTruncation(
                'description',
                255,
                500,
                $sqlState
            );

            expect($exception->getMessage())->toContain('description')
                ->and($exception->getMessage())->toContain('255')
                ->and($exception->getContext()->data['max_length'])->toBe(255)
                ->and($exception->getContext()->data['actual_length'])->toBe(500);
        });

        it('creates exception for division by zero', function () {
            $sqlState = new SqlState('22012');
            $exception = QueryExecutionException::forDivisionByZero(
                'total / count',
                $sqlState
            );

            expect($exception->getMessage())->toContain('Division by zero')
                ->and($exception->getContext()->data['expression'])->toBe('total / count');
        });

        it('sanitizes sensitive values in context', function () {
            $sqlState = new SqlState('22P02');
            $exception = QueryExecutionException::forDataTypeMismatch(
                'password',
                'string',
                'my-secret-password-123',
                $sqlState
            );

            // Value is stored in debug (not data) for security
            // Note: sanitizeValue only truncates strings > 100 chars
            expect($exception->getContext()->debug)->toHaveKey('actual_value');
            expect($exception->getContext()->debug['actual_value'])->toBeString();
        });
    });

    describe('PoolExhaustedException', function () {
        it('creates exception when no connections available', function () {
            $exception = PoolExhaustedException::noConnectionsAvailable(
                currentConnections: 10,
                maxConnections: 10,
                connectionsInUse: 10
            );

            expect($exception->getMessage())->toContain('10/10')
                ->and($exception->getErrorCode())->toBe(DatabaseErrorCode::POOL_EXHAUSTED)
                ->and($exception->getContext()->data['current_connections'])->toBe(10)
                ->and($exception->getContext()->data['max_connections'])->toBe(10)
                ->and($exception->getContext()->data['connections_in_use'])->toBe(10)
                ->and($exception->getContext()->data['free_connections'])->toBe(0)
                ->and($exception->getContext()->data['utilization_percent'])->toBe(100.0);
        });

        it('creates exception when all connections unhealthy', function () {
            $exception = PoolExhaustedException::allConnectionsUnhealthy(
                maxConnections: 5,
                unhealthyConnections: 5
            );

            expect($exception->getMessage())->toContain('unhealthy')
                ->and($exception->getContext()->data['health_status'])->toBe('critical');
        });

        it('provides retry recommendations', function () {
            $exception = PoolExhaustedException::withRetryRecommendation(
                currentConnections: 8,
                maxConnections: 10,
                recommendedRetryDelayMs: 500
            );

            expect($exception->getMessage())->toContain('retry after 500ms')
                ->and($exception->getRetryAfter())->toBe(1) // Rounded up to 1 second
                ->and($exception->getContext()->data['recommended_retry_delay_ms'])->toBe(500);
        });

        it('provides context-aware recommended actions', function () {
            $highUtilization = PoolExhaustedException::noConnectionsAvailable(
                currentConnections: 100,
                maxConnections: 100,
                connectionsInUse: 100
            );

            $unhealthyConnections = PoolExhaustedException::allConnectionsUnhealthy(
                maxConnections: 10,
                unhealthyConnections: 10
            );

            expect($highUtilization->getRecommendedAction())->toContain('Increase max_connections');
            expect($unhealthyConnections->getRecommendedAction())->toContain('Check database server health');
        });
    });

    describe('Exception Integration', function () {
        it('maintains SQLSTATE through exception hierarchy', function () {
            $sqlState = new SqlState('23505');
            $exception = ConstraintViolationException::uniqueViolation(
                'users',
                'email',
                'test@example.com',
                $sqlState
            );

            expect($exception->getSqlState())->toBe($sqlState)
                ->and($exception->getSqlState()->code)->toBe('23505')
                ->and($exception->getSqlState()->isUniqueViolation())->toBeTrue()
                ->and($exception->getSqlState()->isConstraintViolation())->toBeTrue();
        });

        it('includes proper error categorization', function () {
            $connectionError = ConnectionFailedException::cannotConnect(
                'localhost',
                'testdb',
                new SqlState('08001')
            );

            $syntaxError = QuerySyntaxException::tableNotFound(
                'missing_table',
                new SqlState('42P01')
            );

            $deadlock = DeadlockException::detected(new SqlState('40001'));

            expect($connectionError->isCategory('DB'))->toBeTrue();
            expect($syntaxError->isCategory('DB'))->toBeTrue();
            expect($deadlock->isCategory('DB'))->toBeTrue();

            expect($connectionError->getErrorCode())->toBe(DatabaseErrorCode::CONNECTION_FAILED);
            expect($syntaxError->getErrorCode())->toBe(DatabaseErrorCode::QUERY_SYNTAX_ERROR);
            expect($deadlock->getErrorCode())->toBe(DatabaseErrorCode::DEADLOCK_DETECTED);
        });

        it('preserves previous exception chain', function () {
            $pdoException = new \PDOException('Original PDO error');
            $sqlState = new SqlState('08001');

            $exception = ConnectionFailedException::cannotConnect(
                'localhost',
                'testdb',
                $sqlState,
                'Connection failed',
                $pdoException
            );

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