<?php

declare(strict_types=1);

use App\Framework\DateTime\FrozenClock;
use App\Framework\Http\Cookies\Cookie;
use App\Framework\Http\Cookies\Cookies;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Request;
use App\Framework\Http\Response;
use App\Framework\Http\ResponseManipulator;
use App\Framework\Http\Session\InMemorySessionStorage;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionCookieConfig;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionManager;
use App\Framework\Http\Session\SimpleSessionIdGenerator;
use App\Framework\Random\TestableRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;

beforeEach(function () {
    $this->clock = new FrozenClock();
    $this->randomGenerator = new TestableRandomGenerator();
    $this->csrfTokenGenerator = new CsrfTokenGenerator($this->randomGenerator);
    $this->storage = new InMemorySessionStorage();
    $this->idGenerator = new SimpleSessionIdGenerator($this->randomGenerator);
    $this->responseManipulator = new ResponseManipulator();
    $this->cookieConfig = new SessionCookieConfig();

    $this->sessionManager = new SessionManager(
        $this->idGenerator,
        $this->responseManipulator,
        $this->clock,
        $this->csrfTokenGenerator,
        $this->storage,
        $this->cookieConfig
    );
});

describe('SessionManager Basic Operations', function () {
    test('creates new session when no cookie exists', function () {
        $request = new HttpRequest(
            path: '/',
            cookies: new Cookies()
        );

        $session = $this->sessionManager->getOrCreateSession($request);

        expect($session)->toBeInstanceOf(Session::class);
        expect($session->isStarted())->toBeTrue();
    });

    test('creates new session explicitly', function () {
        $session = $this->sessionManager->createNewSession();

        expect($session)->toBeInstanceOf(Session::class);
        expect($session->isStarted())->toBeTrue();
    });

    test('loads existing session from cookie', function () {
        // Erst eine Session erstellen und Daten speichern
        $sessionId = SessionId::fromString('existingsessionid1234567890abcdefg');
        $testData = ['user_id' => 123, 'username' => 'testuser'];
        $this->storage->write($sessionId, $testData);

        // Request mit Session-Cookie erstellen
        $cookies = new Cookies(
            new Cookie('ms_context', $sessionId->toString())
        );

        $request = new Request(
            method: 'GET',
            uri: '/',
            cookies: $cookies,
            headers: [],
            body: ''
        );

        $session = $this->sessionManager->getOrCreateSession($request);

        expect($session->id->toString())->toBe($sessionId->toString());
        expect($session->get('user_id'))->toBe(123);
        expect($session->get('username'))->toBe('testuser');
    });

    test('creates new session when existing session data is corrupted', function () {
        // Session-ID existiert, aber keine Daten im Storage
        $sessionId = SessionId::fromString('nonexistentsessionid1234567890abc');

        $cookies = new Cookies(
            new Cookie('ms_context', $sessionId->toString())
        );

        $request = new Request(
            method: 'GET',
            uri: '/',
            cookies: $cookies,
            headers: [],
            body: ''
        );

        $session = $this->sessionManager->getOrCreateSession($request);

        // Sollte eine neue Session erstellen, nicht die alte laden
        expect($session->id->toString())->not->toBe($sessionId->toString());
        expect($session->all())->toBe([]);
    });
});

describe('SessionManager Session Persistence', function () {
    test('saves session data correctly', function () {
        $session = $this->sessionManager->createNewSession();
        $session->set('test_key', 'test_value');
        $session->set('user_data', ['id' => 456, 'name' => 'Test User']);

        $response = new Response(200, [], '');
        $responseWithCookie = $this->sessionManager->saveSession($session, $response);

        // Prüfe ob Daten im Storage gespeichert wurden
        $storedData = $this->storage->read($session->id);
        expect($storedData['test_key'])->toBe('test_value');
        expect($storedData['user_data'])->toBe(['id' => 456, 'name' => 'Test User']);

        // Prüfe ob Cookie gesetzt wurde
        $headers = $responseWithCookie->headers;
        expect($headers)->toHaveKey('Set-Cookie');
        expect($headers['Set-Cookie'])->toContain('ms_context=' . $session->id->toString());
    });

    test('session data persists across requests', function () {
        // Erste Request: Session erstellen und Daten speichern
        $session1 = $this->sessionManager->createNewSession();
        $session1->set('persistent_data', 'should_persist');

        $response = new Response(200, [], '');
        $responseWithCookie = $this->sessionManager->saveSession($session1, $response);

        // Simulate Cookie aus Response extrahieren
        $sessionId = $session1->id->toString();

        // Zweite Request: Session mit Cookie laden
        $cookies = new Cookies(
            new Cookie('ms_context', $sessionId)
        );

        $request2 = new Request(
            method: 'GET',
            uri: '/',
            cookies: $cookies,
            headers: [],
            body: ''
        );

        $session2 = $this->sessionManager->getOrCreateSession($request2);

        expect($session2->get('persistent_data'))->toBe('should_persist');
        expect($session2->id->toString())->toBe($sessionId);
    });

    test('complex data structures persist correctly', function () {
        $session = $this->sessionManager->createNewSession();
        $complexData = [
            'user' => [
                'id' => 789,
                'profile' => [
                    'name' => 'Complex User',
                    'preferences' => [
                        'theme' => 'dark',
                        'notifications' => true,
                        'languages' => ['en', 'de', 'fr'],
                    ],
                ],
            ],
            'cart' => [
                'items' => [
                    ['id' => 1, 'quantity' => 2, 'price' => 19.99],
                    ['id' => 2, 'quantity' => 1, 'price' => 39.99],
                ],
                'total' => 79.97,
            ],
        ];

        $session->set('complex_data', $complexData);

        $response = new Response(200, [], '');
        $this->sessionManager->saveSession($session, $response);

        // Session erneut laden
        $cookies = new Cookies(
            new Cookie('ms_context', $session->id->toString())
        );

        $request = new Request(
            method: 'GET',
            uri: '/',
            cookies: $cookies,
            headers: [],
            body: ''
        );

        $reloadedSession = $this->sessionManager->getOrCreateSession($request);

        expect($reloadedSession->get('complex_data'))->toBe($complexData);
    });
});

describe('SessionManager Session Regeneration', function () {
    test('regenerates session correctly', function () {
        $originalSession = $this->sessionManager->createNewSession();
        $originalSession->set('user_id', 123);
        $originalSession->set('data_to_preserve', 'important_data');

        // Session speichern
        $response = new Response(200, [], '');
        $this->sessionManager->saveSession($originalSession, $response);

        $originalId = $originalSession->id->toString();

        // Session regenerieren
        $newSession = $this->sessionManager->regenerateSession($originalSession);

        // Neue Session sollte andere ID haben
        expect($newSession->id->toString())->not->toBe($originalId);

        // Aber die gleichen Daten
        expect($newSession->get('user_id'))->toBe(123);
        expect($newSession->get('data_to_preserve'))->toBe('important_data');

        // Alte Session sollte nicht mehr im Storage existieren
        $oldData = $this->storage->read($originalSession->id);
        expect($oldData)->toBe([]);

        // Neue Session sollte im Storage existieren
        $newData = $this->storage->read($newSession->id);
        expect($newData['user_id'])->toBe(123);
    });

    test('regeneration marks session as regenerated', function () {
        $originalSession = $this->sessionManager->createNewSession();
        $originalSession->fromArray([]); // Initialize components

        $newSession = $this->sessionManager->regenerateSession($originalSession);

        // Security manager sollte die Regeneration registriert haben
        expect($newSession->security)->toBeInstanceOf(\App\Framework\Http\Session\SecurityManager::class);
    });
});

describe('SessionManager Session Destruction', function () {
    test('destroys session completely', function () {
        $session = $this->sessionManager->createNewSession();
        $session->set('data_to_destroy', 'will_be_gone');

        // Session speichern
        $response = new Response(200, [], '');
        $this->sessionManager->saveSession($session, $response);

        // Prüfen dass Daten existieren
        $storedData = $this->storage->read($session->id);
        expect($storedData['data_to_destroy'])->toBe('will_be_gone');

        // Session zerstören
        $destroyResponse = $this->sessionManager->destroySession($session, $response);

        // Daten sollten gelöscht sein
        $deletedData = $this->storage->read($session->id);
        expect($deletedData)->toBe([]);

        // Cookie sollte zum Löschen gesetzt werden (expires in der Vergangenheit)
        $headers = $destroyResponse->headers;
        expect($headers)->toHaveKey('Set-Cookie');
        $cookieHeader = $headers['Set-Cookie'];
        expect($cookieHeader)->toContain('ms_context=');
        expect($cookieHeader)->toContain('expires='); // Should have expiry in past
    });
});

describe('SessionManager Configuration', function () {
    test('returns correct cookie name', function () {
        expect($this->sessionManager->getCookieName())->toBe('ms_context');
    });

    test('returns cookie configuration', function () {
        $config = $this->sessionManager->getCookieConfig();
        expect($config)->toBeInstanceOf(SessionCookieConfig::class);
    });

    test('respects custom cookie configuration', function () {
        $customConfig = new SessionCookieConfig(
            lifetime: 7200,
            path: '/custom',
            domain: 'example.com',
            secure: true,
            httpOnly: true
        );

        $customManager = new SessionManager(
            $this->idGenerator,
            $this->responseManipulator,
            $this->clock,
            $this->csrfTokenGenerator,
            $this->storage,
            $customConfig
        );

        $session = $customManager->createNewSession();
        $response = new Response(200, [], '');
        $responseWithCookie = $customManager->saveSession($session, $response);

        $cookieHeader = $responseWithCookie->headers['Set-Cookie'];
        expect($cookieHeader)->toContain('path=/custom');
        expect($cookieHeader)->toContain('domain=example.com');
        expect($cookieHeader)->toContain('secure');
        expect($cookieHeader)->toContain('httponly');
    });
});

describe('SessionManager Error Handling', function () {
    test('handles invalid session ID gracefully', function () {
        $cookies = new Cookies(
            new Cookie('ms_context', 'invalid-session-id-format')
        );

        $request = new Request(
            method: 'GET',
            uri: '/',
            cookies: $cookies,
            headers: [],
            body: ''
        );

        // Sollte eine neue Session erstellen anstatt zu crashen
        $session = $this->sessionManager->getOrCreateSession($request);
        expect($session)->toBeInstanceOf(Session::class);
        expect($session->isStarted())->toBeTrue();
    });

    test('handles storage read errors gracefully', function () {
        // Mock eines Storage der beim Lesen fehlschlägt
        $failingStorage = new class () implements \App\Framework\Http\Session\SessionStorage {
            public function read(SessionId $id): array
            {
                throw new Exception('Storage read failed');
            }

            public function write(SessionId $id, array $data): void
            {
                // No-op
            }

            public function remove(SessionId $id): void
            {
                // No-op
            }

            public function migrate(SessionId $fromId, SessionId $toId): void
            {
                // No-op
            }
        };

        $failingManager = new SessionManager(
            $this->idGenerator,
            $this->responseManipulator,
            $this->clock,
            $this->csrfTokenGenerator,
            $failingStorage,
            $this->cookieConfig
        );

        $sessionId = SessionId::fromString('existingsessionid1234567890abcdef');
        $cookies = new Cookies(
            new Cookie('ms_context', $sessionId->toString())
        );

        $request = new Request(
            method: 'GET',
            uri: '/',
            cookies: $cookies,
            headers: [],
            body: ''
        );

        // Sollte eine neue Session erstellen wenn das Laden fehlschlägt
        expect(fn () => $failingManager->getOrCreateSession($request))
            ->toThrow(Exception::class, 'Storage read failed');
    });
});
