<?php

declare(strict_types=1);

namespace App\Tests\Functional\Administration\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Administration\Domain\Model\RefreshToken\TokenFamilyId;
use App\Administration\Domain\Model\Session\DeviceInfo;
use App\Administration\Domain\Model\Session\Location;
use App\Administration\Domain\Model\Session\Session;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\SessionRepository;
use App\Shared\Domain\Tenant\TenantId;

use function count;

use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;

/**
 * Tests for sessions endpoints.
 *
 * @see Story 1.6 - Gestion des sessions
 */
final class SessionsEndpointsTest extends ApiTestCase
{
    /**
     * Opt-in for API Platform 5.0 behavior where kernel boot is explicit.
     *
     * @see https://github.com/api-platform/core/issues/6971
     */
    protected static ?bool $alwaysBootKernel = true;

    private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
    private const string USER_ID = '550e8400-e29b-41d4-a716-446655440000';

    // =========================================================================
    // Logout endpoint tests
    // =========================================================================

    #[Test]
    public function logoutEndpointAcceptsPostWithoutAuthentication(): void
    {
        $client = static::createClient();

        // Logout should work even without a valid token (graceful logout)
        $client->request('POST', '/api/token/logout', [
            'headers' => [
                'Host' => 'localhost',
                'Accept' => 'application/json',
            ],
        ]);

        // Should return 200 OK (graceful logout even without token)
        self::assertResponseStatusCodeSame(200);
    }

    #[Test]
    public function logoutEndpointReturnsSuccessMessage(): void
    {
        $client = static::createClient();

        $client->request('POST', '/api/token/logout', [
            'headers' => [
                'Host' => 'localhost',
                'Accept' => 'application/json',
            ],
        ]);

        self::assertResponseIsSuccessful();
        self::assertJsonContains(['message' => 'Logout successful']);
    }

    #[Test]
    public function logoutEndpointDeletesRefreshTokenCookie(): void
    {
        $client = static::createClient();

        $response = $client->request('POST', '/api/token/logout', [
            'headers' => [
                'Host' => 'localhost',
                'Accept' => 'application/json',
            ],
        ]);

        // Check that Set-Cookie header is present (to delete the cookie)
        $headers = $response->getHeaders(false);
        self::assertArrayHasKey('set-cookie', $headers);

        // The cookie should be set to empty with past expiration
        $setCookieHeader = $headers['set-cookie'][0];
        self::assertStringContainsString('refresh_token=', $setCookieHeader);
    }

    #[Test]
    public function logoutEndpointDeletesRefreshTokenCookieOnApiAndLegacyPaths(): void
    {
        $client = static::createClient();

        $response = $client->request('POST', '/api/token/logout', [
            'headers' => [
                'Host' => 'localhost',
                'Accept' => 'application/json',
            ],
        ]);

        $headers = $response->getHeaders(false);
        self::assertArrayHasKey('set-cookie', $headers);

        // Should have 2 Set-Cookie headers for refresh_token deletion
        $setCookieHeaders = $headers['set-cookie'];
        self::assertGreaterThanOrEqual(2, count($setCookieHeaders), 'Expected at least 2 Set-Cookie headers');

        // Collect all refresh_token cookie paths
        $paths = [];
        foreach ($setCookieHeaders as $header) {
            if (stripos($header, 'refresh_token=') !== false) {
                // Extract path with case-insensitive matching
                if (preg_match('/path=([^;]+)/i', $header, $matches)) {
                    $paths[] = trim($matches[1]);
                }
            }
        }

        // Must clear both /api (current) and /api/token (legacy) paths
        self::assertContains('/api', $paths, 'Missing Set-Cookie for Path=/api');
        self::assertContains('/api/token', $paths, 'Missing Set-Cookie for Path=/api/token (legacy)');
    }

    // =========================================================================
    // Sessions list endpoint tests - Security
    // =========================================================================

    /**
     * Without a valid tenant subdomain, the endpoint returns 404.
     * This is correct security behavior: don't reveal endpoint existence.
     */
    #[Test]
    public function listSessionsReturns404WithoutTenant(): void
    {
        $client = static::createClient();

        $client->request('GET', '/api/me/sessions', [
            'headers' => [
                'Host' => 'localhost',
                'Accept' => 'application/json',
            ],
        ]);

        // Tenant middleware intercepts and returns 404 (not revealing endpoint)
        self::assertResponseStatusCodeSame(404);
        self::assertJsonContains(['message' => 'Resource not found']);
    }

    // =========================================================================
    // Revoke single session endpoint tests - Security
    // =========================================================================

    /**
     * Without a valid tenant subdomain, the endpoint returns 404.
     * This is correct security behavior: don't reveal endpoint existence.
     */
    #[Test]
    public function revokeSessionReturns404WithoutTenant(): void
    {
        $client = static::createClient();

        $client->request('DELETE', '/api/me/sessions/00000000-0000-0000-0000-000000000001', [
            'headers' => [
                'Host' => 'localhost',
                'Accept' => 'application/json',
            ],
        ]);

        // Tenant middleware intercepts and returns 404 (not revealing endpoint)
        self::assertResponseStatusCodeSame(404);
        self::assertJsonContains(['message' => 'Resource not found']);
    }

    // =========================================================================
    // Revoke all sessions endpoint tests - Security
    // =========================================================================

    /**
     * Without a valid tenant subdomain, the endpoint returns 404.
     * This is correct security behavior: don't reveal endpoint existence.
     */
    #[Test]
    public function revokeAllSessionsReturns404WithoutTenant(): void
    {
        $client = static::createClient();

        $client->request('DELETE', '/api/me/sessions', [
            'headers' => [
                'Host' => 'localhost',
                'Accept' => 'application/json',
            ],
        ]);

        // Tenant middleware intercepts and returns 404 (not revealing endpoint)
        self::assertResponseStatusCodeSame(404);
        self::assertJsonContains(['message' => 'Resource not found']);
    }

    // =========================================================================
    // Authenticated session list tests
    // =========================================================================

    /**
     * Test that listing sessions requires authentication.
     * Returns 401 when no JWT token is provided.
     */
    #[Test]
    public function listSessionsReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();

        $client->request('GET', 'http://ecole-alpha.classeo.local/api/me/sessions', [
            'headers' => [
                'Accept' => 'application/json',
            ],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    /**
     * Test that revoking sessions requires authentication.
     * Returns 401 when no JWT token is provided.
     */
    #[Test]
    public function revokeSessionReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();

        $familyId = TokenFamilyId::generate();

        $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/me/sessions/' . $familyId, [
            'headers' => [
                'Accept' => 'application/json',
            ],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    /**
     * Test that revoking all sessions requires authentication.
     * Returns 401 when no JWT token is provided.
     */
    #[Test]
    public function revokeAllSessionsReturns401WithoutAuthentication(): void
    {
        $client = static::createClient();

        $client->request('DELETE', 'http://ecole-alpha.classeo.local/api/me/sessions', [
            'headers' => [
                'Accept' => 'application/json',
            ],
        ]);

        self::assertResponseStatusCodeSame(401);
    }

    /**
     * Test that the session repository correctly stores and retrieves sessions.
     * This is an integration test of the repository without HTTP layer.
     */
    #[Test]
    public function sessionRepositoryCanStoreAndRetrieveSessions(): void
    {
        $container = static::getContainer();
        $sessionRepository = $container->get(SessionRepository::class);

        $familyId = TokenFamilyId::generate();
        $userId = UserId::fromString(self::USER_ID);
        $tenantId = TenantId::fromString(self::TENANT_ID);

        $session = Session::create(
            familyId: $familyId,
            userId: $userId,
            tenantId: $tenantId,
            deviceInfo: DeviceInfo::fromUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'),
            location: Location::fromIp('192.168.1.1', 'France', 'Paris'),
            createdAt: new DateTimeImmutable(),
        );
        $sessionRepository->save($session, 86400);

        // Retrieve by family ID
        $retrieved = $sessionRepository->findByFamilyId($familyId);
        self::assertNotNull($retrieved);
        self::assertTrue($retrieved->familyId->equals($familyId));
        self::assertSame('Desktop', $retrieved->deviceInfo->device);
        self::assertSame('Chrome 120', $retrieved->deviceInfo->browser);

        // Retrieve by user ID
        $userSessions = $sessionRepository->findAllByUserId($userId);
        self::assertNotEmpty($userSessions);

        // Cleanup
        $sessionRepository->delete($familyId);
        self::assertNull($sessionRepository->findByFamilyId($familyId));
    }

    /**
     * Test that deleteAllExcept removes other sessions but keeps the specified one.
     */
    #[Test]
    public function sessionRepositoryDeleteAllExceptKeepsSpecifiedSession(): void
    {
        $container = static::getContainer();
        $sessionRepository = $container->get(SessionRepository::class);

        $userId = UserId::fromString(self::USER_ID);
        $tenantId = TenantId::fromString(self::TENANT_ID);

        // Clean up any existing sessions for this user (from previous test runs)
        $existingSessions = $sessionRepository->findAllByUserId($userId);
        foreach ($existingSessions as $existingSession) {
            $sessionRepository->delete($existingSession->familyId);
        }

        // Create multiple sessions
        $keepFamilyId = TokenFamilyId::generate();
        $deleteFamilyId1 = TokenFamilyId::generate();
        $deleteFamilyId2 = TokenFamilyId::generate();

        foreach ([$keepFamilyId, $deleteFamilyId1, $deleteFamilyId2] as $familyId) {
            $session = Session::create(
                familyId: $familyId,
                userId: $userId,
                tenantId: $tenantId,
                deviceInfo: DeviceInfo::fromUserAgent('Test Browser'),
                location: Location::unknown(),
                createdAt: new DateTimeImmutable(),
            );
            $sessionRepository->save($session, 86400);
        }

        // Delete all except one
        $deletedIds = $sessionRepository->deleteAllExcept($userId, $keepFamilyId);

        self::assertCount(2, $deletedIds);
        self::assertNotNull($sessionRepository->findByFamilyId($keepFamilyId));
        self::assertNull($sessionRepository->findByFamilyId($deleteFamilyId1));
        self::assertNull($sessionRepository->findByFamilyId($deleteFamilyId2));

        // Cleanup
        $sessionRepository->delete($keepFamilyId);
    }
}
