<?php

declare(strict_types=1);

namespace App\Tests\Integration\Auth;

use App\Domain\Auth\Role;
use App\Domain\Auth\TokenKind;
use App\Tests\Integration\Support\AppTestCase;

/**
 * Covers the auth matrix from M03 task §7. Each test corresponds to one
 * row of the table:
 *
 *  | Token kind   | X-Acting-User-Id | User exists | Required | Expected |
 *  | ------------ | ---------------- | ----------- | -------- | -------- |
 *  | none         | -                | -           | viewer   | 401      |
 *  | bad token    | -                | -           | viewer   | 401      |
 *  | reporter     | -                | -           | viewer   | 401      |
 *  | admin/viewer | -                | -           | viewer   | 200      |
 *  | admin/viewer | -                | -           | operator | 403      |
 *  | admin/admin  | -                | -           | admin    | 200      |
 *  | service      | no               | -           | viewer   | 400      |
 *  | service      | yes              | no          | viewer   | 403      |
 *  | service      | yes (viewer)     | yes         | viewer   | 200      |
 *  | service      | yes (viewer)     | yes         | operator | 403      |
 *  | service      | yes (admin)      | yes         | admin    | 200      |
 *
 * /admin/me requires Viewer; /auth/users/upsert-local requires Admin.
 * Together they exercise both required-role rungs.
 */
final class AuthMatrixTest extends AppTestCase
{
    public function testNoTokenReturns401(): void
    {
        $response = $this->request('GET', '/api/v1/admin/me');
        self::assertSame(401, $response->getStatusCode());
        self::assertSame('unauthorized', $this->decode($response)['error']);
    }

    public function testBadlyFormattedTokenReturns401(): void
    {
        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer not_a_real_token',
        ]);
        self::assertSame(401, $response->getStatusCode());
    }

    public function testWellFormedButUnknownTokenReturns401(): void
    {
        // Right format, never persisted → no DB hit on lookup.
        $raw = 'irdb_adm_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $raw,
        ]);
        self::assertSame(401, $response->getStatusCode());
    }

    public function testReporterTokenOnAdminRouteReturns401(): void
    {
        $reporterId = $this->createReporter();
        $token = $this->createToken(TokenKind::Reporter, reporterId: $reporterId);

        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(401, $response->getStatusCode());
    }

    public function testAdminTokenWithViewerRoleSatisfiesViewerRoute(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
        ]);

        self::assertSame(200, $response->getStatusCode());
        $body = $this->decode($response);
        self::assertSame('viewer', $body['role']);
        self::assertSame('admin-token', $body['source']);
        self::assertNull($body['user_id']);
    }

    public function testAdminTokenWithAdminRoleSatisfiesAdminRoute(): void
    {
        $token = $this->createToken(TokenKind::Admin, role: Role::Admin);

        // /admin/me requires Viewer; an Admin role satisfies it.
        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(200, $response->getStatusCode());
        self::assertSame('admin', $this->decode($response)['role']);
    }

    public function testServiceTokenWithoutImpersonationHeaderReturns400(): void
    {
        $token = $this->createToken(TokenKind::Service);

        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
        ]);
        self::assertSame(400, $response->getStatusCode());
        self::assertSame('missing X-Acting-User-Id', $this->decode($response)['error']);
    }

    public function testServiceTokenWithUnknownUserReturns403(): void
    {
        $token = $this->createToken(TokenKind::Service);

        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
            'X-Acting-User-Id' => '99999',
        ]);
        self::assertSame(403, $response->getStatusCode());
    }

    public function testServiceTokenWithMalformedImpersonationHeaderReturns400(): void
    {
        $token = $this->createToken(TokenKind::Service);

        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
            'X-Acting-User-Id' => 'not-an-int',
        ]);
        self::assertSame(400, $response->getStatusCode());
    }

    public function testServiceTokenImpersonatingViewerReachesViewerRoute(): void
    {
        $token = $this->createToken(TokenKind::Service);
        $userId = $this->createUser(Role::Viewer, isLocal: false, subject: 'sub-viewer');

        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
            'X-Acting-User-Id' => (string) $userId,
        ]);
        self::assertSame(200, $response->getStatusCode());

        $body = $this->decode($response);
        self::assertSame($userId, $body['user_id']);
        self::assertSame('viewer', $body['role']);
        self::assertSame('oidc', $body['source']);
        self::assertFalse($body['is_local']);
    }

    public function testServiceTokenImpersonatingAdminReachesAdminRoute(): void
    {
        $token = $this->createToken(TokenKind::Service);
        $userId = $this->createUser(Role::Admin, isLocal: true);

        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
            'X-Acting-User-Id' => (string) $userId,
        ]);
        self::assertSame(200, $response->getStatusCode());

        $body = $this->decode($response);
        self::assertSame('admin', $body['role']);
        self::assertTrue($body['is_local']);
        self::assertSame('local', $body['source']);
    }

    public function testActingUserHeaderIgnoredForAdminToken(): void
    {
        // Per SPEC §8: X-Acting-User-Id is *only* trusted with the service token.
        // For an admin token it must not be 400'd, just ignored.
        $token = $this->createToken(TokenKind::Admin, role: Role::Viewer);
        $response = $this->request('GET', '/api/v1/admin/me', [
            'Authorization' => 'Bearer ' . $token,
            'X-Acting-User-Id' => '99999',
        ]);

        self::assertSame(200, $response->getStatusCode());
        self::assertSame('admin-token', $this->decode($response)['source']);
    }
}
