<?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\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('Complete Session Lifecycle', function () {
    test('full session lifecycle: create -> use -> persist -> reload -> destroy', function () {
        // Phase 1: Session Creation
        $request1 = new Request(
            method: 'GET',
            uri: '/login',
            cookies: new Cookies([]),
            headers: [],
            body: ''
        );

        $session = $this->sessionManager->getOrCreateSession($request1);
        expect($session)->toBeInstanceOf(Session::class);
        expect($session->isStarted())->toBeTrue();

        $originalSessionId = $session->id->toString();

        // Phase 2: Session Usage (simulate login process)
        $session->set('user_id', 123);
        $session->set('username', 'testuser');
        $session->set('login_time', time());
        $session->set('user_permissions', ['read', 'write', 'admin']);

        // Initialize components
        $session->fromArray($session->all());

        // Use session components
        $session->flash->add('success', 'Login successful!');
        $session->csrf->generateToken('login_form');

        expect($session->get('user_id'))->toBe(123);
        expect($session->flash)->toBeInstanceOf(\App\Framework\Http\Session\FlashBag::class);

        // Phase 3: Session Persistence
        $response1 = new Response(200, [], 'Login successful');
        $responseWithCookie = $this->sessionManager->saveSession($session, $response1);

        // Check cookie is set
        expect($responseWithCookie->headers)->toHaveKey('Set-Cookie');
        expect($responseWithCookie->headers['Set-Cookie'])->toContain($originalSessionId);

        // Verify data is stored
        $storedData = $this->storage->read($session->id);
        expect($storedData['user_id'])->toBe(123);
        expect($storedData['username'])->toBe('testuser');

        // Phase 4: Session Reload (simulate new request)
        $cookies = new Cookies([
            new Cookie('ms_context', $originalSessionId),
        ]);

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

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

        // Verify session data persisted
        expect($reloadedSession->id->toString())->toBe($originalSessionId);
        expect($reloadedSession->get('user_id'))->toBe(123);
        expect($reloadedSession->get('username'))->toBe('testuser');
        expect($reloadedSession->get('user_permissions'))->toBe(['read', 'write', 'admin']);

        // Verify components are working
        expect($reloadedSession->flash)->toBeInstanceOf(\App\Framework\Http\Session\FlashBag::class);
        expect($reloadedSession->csrf)->toBeInstanceOf(\App\Framework\Http\Session\CsrfProtection::class);

        // Phase 5: Session Modification
        $reloadedSession->set('last_activity', time());
        $reloadedSession->remove('login_time');
        $reloadedSession->flash->add('info', 'Dashboard loaded');

        $response2 = new Response(200, [], 'Dashboard');
        $this->sessionManager->saveSession($reloadedSession, $response2);

        // Phase 6: Session Destruction (logout)
        $request3 = new Request(
            method: 'POST',
            uri: '/logout',
            cookies: $cookies,
            headers: [],
            body: ''
        );

        $sessionToDestroy = $this->sessionManager->getOrCreateSession($request3);
        expect($sessionToDestroy->get('user_id'))->toBe(123); // Data still there
        expect($sessionToDestroy->has('login_time'))->toBeFalse(); // But modifications persisted
        expect($sessionToDestroy->has('last_activity'))->toBeTrue();

        $response3 = new Response(200, [], 'Logged out');
        $logoutResponse = $this->sessionManager->destroySession($sessionToDestroy, $response3);

        // Verify session is destroyed
        $destroyedData = $this->storage->read($sessionToDestroy->id);
        expect($destroyedData)->toBe([]);

        // Verify cookie is set to expire
        expect($logoutResponse->headers['Set-Cookie'])->toContain('ms_context=');

        // Phase 7: Verify new request creates new session
        $request4 = new Request(
            method: 'GET',
            uri: '/',
            cookies: new Cookies([]), // No session cookie
            headers: [],
            body: ''
        );

        $newSession = $this->sessionManager->getOrCreateSession($request4);
        expect($newSession->id->toString())->not->toBe($originalSessionId);
        expect($newSession->get('user_id'))->toBeNull();
        expect($newSession->all())->toBe([]);
    });

    test('session regeneration during lifecycle', function () {
        // Create initial session
        $session = $this->sessionManager->createNewSession();
        $session->set('sensitive_data', 'important');
        $session->set('user_role', 'user');

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

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

        // Simulate privilege escalation requiring session regeneration
        $session->set('user_role', 'admin');

        // Regenerate session for security
        $newSession = $this->sessionManager->regenerateSession($session);

        expect($newSession->id->toString())->not->toBe($originalId);
        expect($newSession->get('sensitive_data'))->toBe('important');
        expect($newSession->get('user_role'))->toBe('admin');

        // Old session should not exist
        $oldData = $this->storage->read($session->id);
        expect($oldData)->toBe([]);

        // New session should exist
        $newData = $this->storage->read($newSession->id);
        expect($newData['sensitive_data'])->toBe('important');
        expect($newData['user_role'])->toBe('admin');
    });

    test('concurrent session handling', function () {
        // Simulate multiple concurrent requests with same session
        $sessionId = SessionId::fromString('concurrentsessionid1234567890abcd');
        $initialData = ['user_id' => 456, 'concurrent_test' => true];
        $this->storage->write($sessionId, $initialData);

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

        // Request 1: Load session and modify
        $request1 = new Request('GET', '/api/data', $cookies, [], '');
        $session1 = $this->sessionManager->getOrCreateSession($request1);
        $session1->set('request_1_data', 'modified_by_request_1');

        // Request 2: Load same session and modify differently
        $request2 = new Request('GET', '/api/other', $cookies, [], '');
        $session2 = $this->sessionManager->getOrCreateSession($request2);
        $session2->set('request_2_data', 'modified_by_request_2');

        // Both sessions should have the same ID but different local state
        expect($session1->id->toString())->toBe($session2->id->toString());
        expect($session1->get('request_1_data'))->toBe('modified_by_request_1');
        expect($session1->get('request_2_data'))->toBeNull();
        expect($session2->get('request_1_data'))->toBeNull();
        expect($session2->get('request_2_data'))->toBe('modified_by_request_2');

        // Save both sessions (last one wins)
        $response1 = new Response(200, [], '');
        $response2 = new Response(200, [], '');

        $this->sessionManager->saveSession($session1, $response1);
        $this->sessionManager->saveSession($session2, $response2);

        // Reload session to see final state
        $request3 = new Request('GET', '/verify', $cookies, [], '');
        $finalSession = $this->sessionManager->getOrCreateSession($request3);

        // Should have data from the last saved session (session2)
        expect($finalSession->get('user_id'))->toBe(456);
        expect($finalSession->get('request_1_data'))->toBeNull();
        expect($finalSession->get('request_2_data'))->toBe('modified_by_request_2');
    });

    test('session data integrity throughout lifecycle', function () {
        // Test with complex data that could be corrupted
        $complexData = [
            'user' => [
                'id' => 789,
                'profile' => [
                    'name' => 'Test User with üñíçødé',
                    'email' => 'test@example.com',
                    'preferences' => [
                        'language' => 'de-DE',
                        'timezone' => 'Europe/Berlin',
                        'notifications' => [
                            'email' => true,
                            'push' => false,
                            'sms' => null,
                        ],
                    ],
                ],
            ],
            'shopping_cart' => [
                'items' => [
                    [
                        'id' => 1,
                        'name' => 'Product with "quotes" and \'apostrophes\'',
                        'price' => 19.99,
                        'quantity' => 2,
                        'metadata' => [
                            'color' => 'red',
                            'size' => 'large',
                            'custom_data' => '{"json": "inside", "json": true}',
                        ],
                    ],
                ],
                'total' => 39.98,
                'currency' => 'EUR',
                'discount_codes' => ['SAVE10', 'WELCOME'],
            ],
            'session_metadata' => [
                'created_at' => time(),
                'ip_address' => '192.168.1.1',
                'user_agent' => 'Mozilla/5.0 Test Browser',
                'csrf_tokens' => [
                    'form_1' => 'token_abc123',
                    'form_2' => 'token_def456',
                ],
            ],
        ];

        $session = $this->sessionManager->createNewSession();

        // Set complex data
        foreach ($complexData as $key => $value) {
            $session->set($key, $value);
        }

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

        // Extract session ID from cookie for next request
        $sessionId = $session->id->toString();
        $cookies = new Cookies([
            new Cookie('ms_context', $sessionId),
        ]);

        // Reload session multiple times to test persistence
        for ($i = 0; $i < 3; $i++) {
            $request = new Request('GET', "/request-{$i}", $cookies, [], '');
            $reloadedSession = $this->sessionManager->getOrCreateSession($request);

            // Verify all complex data is intact
            expect($reloadedSession->get('user'))->toBe($complexData['user']);
            expect($reloadedSession->get('shopping_cart'))->toBe($complexData['shopping_cart']);
            expect($reloadedSession->get('session_metadata'))->toBe($complexData['session_metadata']);

            // Test deep nested access
            $user = $reloadedSession->get('user');
            expect($user['profile']['preferences']['notifications']['email'])->toBeTrue();
            expect($user['profile']['preferences']['notifications']['push'])->toBeFalse();
            expect($user['profile']['preferences']['notifications']['sms'])->toBeNull();

            // Modify and save again
            $cart = $reloadedSession->get('shopping_cart');
            $cart['items'][0]['quantity'] = $i + 3;
            $reloadedSession->set('shopping_cart', $cart);
            $reloadedSession->set("request_{$i}_timestamp", time());

            $this->sessionManager->saveSession($reloadedSession, $response);
        }

        // Final verification
        $finalRequest = new Request('GET', '/final', $cookies, [], '');
        $finalSession = $this->sessionManager->getOrCreateSession($finalRequest);

        $finalCart = $finalSession->get('shopping_cart');
        expect($finalCart['items'][0]['quantity'])->toBe(5); // 2 + 3 from last iteration
        expect($finalSession->has('request_0_timestamp'))->toBeTrue();
        expect($finalSession->has('request_1_timestamp'))->toBeTrue();
        expect($finalSession->has('request_2_timestamp'))->toBeTrue();
    });

    test('session lifecycle error recovery', function () {
        // Test recovery from various error conditions

        // 1. Corrupted session data
        $sessionId = SessionId::fromString('corruptedsessionid1234567890abcde');
        $session = $this->sessionManager->createNewSession();
        $session->set('valid_data', 'should_persist');

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

        // Simulate corrupted storage (storage returns invalid data)
        $corruptedStorage = new class ($this->storage) implements \App\Framework\Http\Session\SessionStorage {
            private $originalStorage;

            private $corruptOnRead = false;

            public function __construct($storage)
            {
                $this->originalStorage = $storage;
            }

            public function enableCorruption()
            {
                $this->corruptOnRead = true;
            }

            public function read(SessionId $id): array
            {
                if ($this->corruptOnRead) {
                    return ['corrupted' => 'data', 'invalid' => null];
                }

                return $this->originalStorage->read($id);
            }

            public function write(SessionId $id, array $data): void
            {
                $this->originalStorage->write($id, $data);
            }

            public function remove(SessionId $id): void
            {
                $this->originalStorage->remove($id);
            }

            public function migrate(SessionId $fromId, SessionId $toId): void
            {
                $this->originalStorage->migrate($fromId, $toId);
            }
        };

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

        // Normal read should work
        $cookies = new Cookies([
            new Cookie('ms_context', $session->id->toString()),
        ]);

        $request = new Request('GET', '/test', $cookies, [], '');
        $loadedSession = $corruptedManager->getOrCreateSession($request);
        expect($loadedSession->get('valid_data'))->toBe('should_persist');

        // Enable corruption and try to read
        $corruptedStorage->enableCorruption();
        $corruptedSession = $corruptedManager->getOrCreateSession($request);

        // Should handle corrupted data gracefully
        expect($corruptedSession->get('corrupted'))->toBe('data');
        expect($corruptedSession->get('valid_data'))->toBeNull(); // Original data lost due to corruption
    });
});
