<?php

declare(strict_types=1);

namespace Tests\Feature;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\Group;
use Tests\AuthTestCase;
use Tests\Support\TestDataFactory;

/**
 * Advanced Token Lifecycle & Security Test Suite
 *
 * Covers:
 *  ✓ POST /api/auth/validate-token — valid token accepted (200)
 *  ✓ POST /api/auth/validate-token — tampered token rejected (401)
 *  ✓ POST /api/auth/validate-token — missing token field returns 400
 *  ✓ POST /api/auth/validate-token — expired token rejected (401)
 *  ⚠ POST /api/auth/refresh        — refresh token produces new access token (gap: endpoint missing)
 *  ⚠ POST /api/auth/logout         — token added to blacklist (gap: endpoint missing)
 *  ⚠ Blacklisted token rejected on protected endpoint (gap: blacklist middleware)
 *
 * @group token
 * @group lifecycle
 * @group security
 */
#[Group('token')]
#[Group('lifecycle')]
#[Group('security')]
class TokenLifecycleTest extends AuthTestCase
{
    // ─── Token Validation (currently implemented endpoint) ────────────────────

    #[Test]
    public function valid_token_is_accepted_by_validate_endpoint(): void
    {
        ['loginResponse' => $login] = $this->registerAndLogin();
        $token = $this->extractToken($login);

        $response = static::$http->post('/api/auth/validate-token', ['token' => $token]);

        $this->assertTrue(
            $response->isOk(),
            "Expected 200 for valid token, got {$response->status}. Body: " . $response->dump()
        );
    }

    #[Test]
    public function tampered_token_is_rejected_with_401(): void
    {
        ['loginResponse' => $login] = $this->registerAndLogin();
        $token = $this->extractToken($login);

        // Tamper: replace last 10 chars of signature with random bytes
        $tampered = substr($token, 0, -10) . bin2hex(random_bytes(5));

        $response = static::$http->post('/api/auth/validate-token', ['token' => $tampered]);

        $this->assertTrue(
            $response->isUnauth(),
            "Tampered token must return 401. Got {$response->status}: " . $response->dump()
        );
    }

    #[Test]
    public function missing_token_field_returns_400(): void
    {
        $response = static::$http->post('/api/auth/validate-token', []);
        $this->assertTrue($response->isBadRequest(), $response->dump());
    }

    #[Test]
    public function completely_fabricated_token_is_rejected(): void
    {
        $fake = base64_encode('{"alg":"HS256","typ":"JWT"}') . '.' .
                base64_encode('{"user_id":999,"exp":9999999999}') . '.' .
                'fakesignature123';

        $response = static::$http->post('/api/auth/validate-token', ['token' => $fake]);

        $this->assertTrue(
            $response->isUnauth(),
            "Fabricated token must be rejected with 401. Got {$response->status}."
        );
    }

    #[Test]
    public function expired_token_is_rejected_with_401(): void
    {
        // Craft a token that expired 1 hour ago using the known JWT secret
        $secret  = $_ENV['JWT_SECRET'] ?? 'auth-service-secret-key-change-in-production-2024';
        $issuer  = $_ENV['JWT_ISSUER'] ?? 'emmanylcode-auth';

        $header  = rtrim(strtr(base64_encode(json_encode(['typ' => 'JWT', 'alg' => 'HS256'])), '+/', '-_'), '=');
        $payload = rtrim(strtr(base64_encode(json_encode([
            'user_id' => 1,
            'iat'     => time() - 7200, // issued 2 hours ago
            'exp'     => time() - 3600, // expired 1 hour ago
            'iss'     => $issuer,
        ])), '+/', '-_'), '=');

        $sig = rtrim(strtr(base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true)), '+/', '-_'), '=');
        $expiredToken = "$header.$payload.$sig";

        $response = static::$http->post('/api/auth/validate-token', ['token' => $expiredToken]);

        $this->assertTrue(
            $response->isUnauth(),
            "Expired token must return 401. Got {$response->status}: " . $response->dump()
        );
    }

    // ─── Refresh Token Flow (Implementation Gap) ──────────────────────────────

    #[Test]
    public function refresh_token_produces_new_access_token(): void
    {
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: POST /api/auth/refresh endpoint does not yet exist. ' .
            'Add to index.php route table and implement in AuthServiceController::refresh(). ' .
            'Expected flow: POST /api/auth/refresh {refresh_token: "..."} → 200 {token, refresh_token}'
        );

        // ── Body: execute once endpoint is implemented ──
        ['payload' => $user, 'loginResponse' => $login] = $this->registerAndLogin();
        $refreshToken = $this->extractRefreshToken($login);
        $this->assertNotNull($refreshToken, "Login response must include a refresh_token.");

        // Store & send refresh token to obtain new access token
        $response = static::$http->post('/api/auth/refresh', ['refresh_token' => $refreshToken]);

        $this->assertTrue($response->isOk(), $response->dump());

        $newAccessToken = $response->get('token') ?? $response->get('access_token');
        $this->assertNotEmpty($newAccessToken, "Refresh must return a new access token.");

        // New token must be different from the original
        $originalToken = $this->extractToken($login);
        $this->assertNotSame($originalToken, $newAccessToken, "Refreshed token must be newly generated.");
    }

    #[Test]
    public function used_or_invalid_refresh_token_returns_401(): void
    {
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: /api/auth/refresh endpoint not yet implemented.'
        );

        $response = static::$http->post('/api/auth/refresh', [
            'refresh_token' => 'invalid.refresh.token.value',
        ]);
        $this->assertTrue($response->isUnauth(), $response->dump());
    }

    // ─── Logout / Token Blacklisting (Implementation Gap) ────────────────────

    #[Test]
    public function logout_adds_token_to_blacklist_and_returns_200(): void
    {
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: POST /api/auth/logout endpoint does not yet exist. ' .
            'Implement AuthServiceController::logout() to: ' .
            '1. Extract jti from Bearer token. ' .
            '2. Write to file cache: "jwt:blacklist:{jti}" with TTL = remaining exp time. ' .
            '3. Return 200 {message: "Logged out successfully"}.'
        );

        ['loginResponse' => $login] = $this->registerAndLogin();
        $token = $this->extractToken($login);

        $response = static::$http->post(
            '/api/auth/logout',
            [],
            static::$http->bearerHeader($token)
        );

        $this->assertTrue($response->isOk(), "Logout must return 200. Got: " . $response->dump());
    }

    #[Test]
    public function blacklisted_token_is_rejected_on_subsequent_requests(): void
    {
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: Requires /api/auth/logout + jti claim + blacklist middleware. ' .
            'Full chain: 1) login → get token with jti, 2) logout → token blacklisted, ' .
            '3) validate-token with same token → 401 TOKEN_REVOKED.'
        );

        ['loginResponse' => $login] = $this->registerAndLogin();
        $token = $this->extractToken($login);

        // Step 1: Logout — adds token to blacklist
        static::$http->post('/api/auth/logout', [], static::$http->bearerHeader($token));

        // Step 2: Attempt to use the blacklisted token
        $retry = static::$http->post('/api/auth/validate-token', ['token' => $token]);

        $this->assertTrue(
            $retry->isUnauth(),
            "Blacklisted token must return 401. Got {$retry->status}: " . $retry->dump()
        );

        $this->assertStringContainsStringIgnoringCase(
            'revoked',
            $retry->rawBody,
            "Response must indicate token has been revoked."
        );
    }

    #[Test]
    public function accessing_protected_route_without_token_returns_401(): void
    {
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: No protected routes exist yet. ' .
            'Once middleware-guarded endpoints are added, remove this skip.'
        );

        $response = static::$http->get('/api/auth/me');
        $this->assertTrue($response->isUnauth(), $response->dump());
    }
}
