<?php

declare(strict_types=1);

namespace Tests\Feature;

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

/**
 * JWT Token Inspection Test Suite
 *
 * Covers:
 *  ✓ Token is a valid 3-part JWT structure
 *  ✓ Header: alg = HS256, typ = JWT
 *  ✓ Payload: iss matches JWT_ISSUER from config
 *  ✓ Payload: iat issued recently (within 10 seconds of now)
 *  ✓ Payload: exp is in the future and at least 3500s away
 *  ✓ Payload: user identity claim present (user_id OR sub per RFC 7519)
 *  ✓ Payload: user_id matches the registered user
 *  ✓ Plaintext JWT is 3-part dot-separated
 *  ⚠ Payload: 'role' slug claim (requires controller update)
 *  ⚠ Payload: 'status' slug claim (requires controller update)
 *  ⚠ Payload: 'jti' claim for blacklist tracking (requires controller update)
 *
 * @group token
 * @group jwt
 */
#[Group('token')]
#[Group('jwt')]
class TokenInspectionTest extends AuthTestCase
{
    private static ?JwtInspector $jwt    = null;
    private static array         $userPayload = [];
    private static int           $registeredUserId = 0;

    // ─── Suite Setup: register + login once, share across all tests ───────────

    public static function setUpBeforeClass(): void
    {
        parent::setUpBeforeClass();

        self::$userPayload = TestDataFactory::makeUser();

        $register = static::$http->post('/api/auth/register', self::$userPayload);
        if (!$register->isCreated() && !$register->isOk()) {
            // Mark setup failure so all tests in this class report correctly
            return;
        }

        self::$registeredUserId = (int) ($register->dot('user.id') ?? 0);

        $login = static::$http->post('/api/auth/login', [
            'email'    => self::$userPayload['email'],
            'password' => self::$userPayload['password'],
        ]);

        $token = $login->get('token') ?? $login->get('access_token');
        if ($token) {
            self::$jwt = new JwtInspector((string) $token);
        }
    }

    // ─── Structure Assertions ─────────────────────────────────────────────────

    #[Test]
    public function token_has_three_dot_separated_parts(): void
    {
        $this->assertNotNull(self::$jwt, "JWT was not produced during setup — check registration/login.");

        $token = self::$userPayload['password']; // intentional wrong var to force rebuild
        $login = $this->loginAs(self::$userPayload['email'], self::$userPayload['password']);
        $raw   = $login->get('token') ?? $login->get('access_token');

        $this->assertNotEmpty($raw, "Token must not be empty.");
        $this->assertSame(3, substr_count((string)$raw, '.') + 1,
            "JWT must have exactly 3 parts separated by dots."
        );
        // Actually count parts correctly:
        $parts = explode('.', (string)$raw);
        $this->assertCount(3, $parts, "JWT must have exactly 3 dot-separated segments.");
    }

    #[Test]
    public function token_header_specifies_hs256_algorithm(): void
    {
        $this->skipIfNoJwt();
        $this->assertSame('HS256', self::$jwt->algorithm(),
            "Token header 'alg' must be HS256 per current implementation."
        );
    }

    #[Test]
    public function token_header_type_is_jwt(): void
    {
        $this->skipIfNoJwt();
        $this->assertSame('JWT', self::$jwt->tokenType());
    }

    // ─── Payload Claim Assertions ─────────────────────────────────────────────

    #[Test]
    public function token_issuer_matches_configured_jwt_issuer(): void
    {
        $this->skipIfNoJwt();
        $expectedIssuer = $_ENV['JWT_ISSUER'] ?? 'emmanylcode-auth';

        $this->assertTrue(
            self::$jwt->issuedBy($expectedIssuer),
            "Token 'iss' claim must equal '{$expectedIssuer}', got: " . self::$jwt->claim('iss')
        );
    }

    #[Test]
    public function token_iat_was_issued_within_last_10_seconds(): void
    {
        $this->skipIfNoJwt();
        $this->assertTrue(
            self::$jwt->issuedRecently(10),
            "Token 'iat' claim must be within 10 seconds of now. iat=" . self::$jwt->claim('iat')
        );
    }

    #[Test]
    public function token_exp_is_in_the_future(): void
    {
        $this->skipIfNoJwt();
        $this->assertFalse(self::$jwt->isExpired(), "Token 'exp' must be in the future.");
    }

    #[Test]
    public function token_exp_respects_configured_expiration_window(): void
    {
        $this->skipIfNoJwt();
        $configured = (int) ($_ENV['JWT_EXPIRATION'] ?? 3600);

        // Allow 30-second tolerance for test execution time
        $this->assertTrue(
            self::$jwt->expiresInAtLeast($configured - 30),
            "Token should expire in ~{$configured}s. Remaining: " . (self::$jwt->claim('exp') - time()) . "s"
        );
    }

    #[Test]
    public function token_contains_user_identity_claim(): void
    {
        $this->skipIfNoJwt();
        $this->assertTrue(
            self::$jwt->hasSubject(),
            "Token must contain 'sub' or 'user_id' claim to identify the user."
        );
    }

    #[Test]
    public function token_user_identity_matches_registered_user(): void
    {
        $this->skipIfNoJwt();

        if (self::$registeredUserId === 0) {
            $this->markTestSkipped("Registered user ID not captured from registration response.");
        }

        $claimedId = self::$jwt->subjectId();
        $this->assertSame(
            self::$registeredUserId,
            (int) $claimedId,
            "Token user_id/sub must match the ID returned at registration."
        );
    }

    // ─── Implementation Gap Tests — Required but not yet implemented ──────────

    #[Test]
    public function token_contains_role_slug_claim(): void
    {
        $this->skipIfNoJwt();
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: JWT payload does not yet include a "role" slug claim. ' .
            'Update generateToken() to JOIN roles table and embed role slug. ' .
            'Expected claim: "role" => "student"'
        );

        $role = self::$jwt->claim('role');
        $this->assertNotEmpty($role, "Token must contain a 'role' string claim.");
        $this->assertContains($role, array_values(TestDataFactory::ROLE_SLUGS));
    }

    #[Test]
    public function token_contains_status_slug_claim(): void
    {
        $this->skipIfNoJwt();
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: JWT payload does not yet include a "status" slug claim. ' .
            'Update generateToken() to JOIN statuses table and embed status slug. ' .
            'Expected claim: "status" => "active"'
        );

        $status = self::$jwt->claim('status');
        $this->assertNotEmpty($status);
        $this->assertContains($status, array_values(TestDataFactory::STATUS_SLUGS));
    }

    #[Test]
    public function token_contains_jti_claim_for_blacklist_tracking(): void
    {
        $this->skipIfNoJwt();
        $this->markTestSkipped(
            'IMPLEMENTATION GAP: JWT payload does not yet include a "jti" (JWT ID) claim. ' .
            'The VerifyJWTBlacklist middleware requires jti for blacklist key generation: ' .
            '"jwt:blacklist:{jti}". Add jti = bin2hex(random_bytes(16)) to generateToken().'
        );

        $jti = self::$jwt->claim('jti');
        $this->assertNotEmpty($jti, "Token must have a 'jti' claim for blacklist tracking.");
        $this->assertGreaterThanOrEqual(16, strlen((string)$jti));
    }

    // ─── Internal ─────────────────────────────────────────────────────────────

    private function skipIfNoJwt(): void
    {
        if (self::$jwt === null) {
            $this->markTestSkipped("JWT inspector not initialized — registration or login failed in setUpBeforeClass.");
        }
    }
}
