<?php

use App\Models\Application;
use App\Models\AuthenticationToken;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token\Parser;
use Tests\Support\ManagesTestKeys;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class, ManagesTestKeys::class);

beforeEach(function () {
    // Ensure test keys exist for each test
    $this->ensureTestKeysExist();
});

describe('OIDC Authorization Endpoint', function () {
    test('requires authentication', function () {
        $this->get(route('auth.authorize'))
            ->assertRedirect(route('login'));
    });

    test('validates client_id parameter', function () {
        $user = User::factory()->create();
        
        $this->actingAs($user)
            ->get(route('auth.authorize') . '?response_type=code&redirect_uri=http://example.com')
            ->assertStatus(404);
    });

    test('validates redirect_uri matches client configuration', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create([
            'redirect_uri' => 'http://example.com/callback'
        ]);

        $this->actingAs($user)
            ->get(route('auth.authorize') . '?' . http_build_query([
                'client_id' => $app->client_id,
                'response_type' => 'code',
                'redirect_uri' => 'http://malicious.com/callback',
            ]))
            ->assertStatus(403);
    });

    test('successful authorization flow with required parameters', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create([
            'redirect_uri' => 'http://example.com/callback'
        ]);

        $response = $this->actingAs($user)
            ->get(route('auth.authorize') . '?' . http_build_query([
                'client_id' => $app->client_id,
                'response_type' => 'code',
                'redirect_uri' => $app->redirect_uri,
                'state' => 'test-state-123',
                'scope' => 'openid profile email',
            ]));

        $response->assertRedirect(route('auth.confirm'));
        expect(session('app_id'))->toBe($app->id);
        expect(session('redirect_on_confirm'))->toContain('http://example.com/callback');
        expect(session('redirect_on_confirm'))->toContain('state=test-state-123');
    });

    test('supports PKCE flow with S256 code challenge', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create([
            'redirect_uri' => 'http://example.com/callback'
        ]);

        $codeVerifier = base64url_encode(random_bytes(32));
        $codeChallenge = base64url_encode(hash('sha256', $codeVerifier, true));

        $response = $this->actingAs($user)
            ->get(route('auth.authorize') . '?' . http_build_query([
                'client_id' => $app->client_id,
                'response_type' => 'code',
                'redirect_uri' => $app->redirect_uri,
                'code_challenge' => $codeChallenge,
                'code_challenge_method' => 'S256',
                'state' => 'test-state',
            ]));

        $response->assertRedirect(route('auth.confirm'));
        
        // Verify PKCE parameters are cached
        $redirectUrl = session('redirect_on_confirm');
        preg_match('/code=([^&]+)/', $redirectUrl, $matches);
        $authCode = $matches[1];
        
        $cachedData = Cache::get("auth_code:$authCode");
        expect($cachedData)->not->toBeNull();
        expect($cachedData['code_challenge'])->toBe($codeChallenge);
        expect($cachedData['code_challenge_method'])->toBe('S256');
    });

    test('supports PKCE flow with plain code challenge', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create([
            'redirect_uri' => 'http://example.com/callback'
        ]);

        $codeVerifier = 'test-code-verifier';

        $response = $this->actingAs($user)
            ->get(route('auth.authorize') . '?' . http_build_query([
                'client_id' => $app->client_id,
                'response_type' => 'code',
                'redirect_uri' => $app->redirect_uri,
                'code_challenge' => $codeVerifier,
                'code_challenge_method' => 'plain',
            ]));

        $response->assertRedirect(route('auth.confirm'));
        
        $redirectUrl = session('redirect_on_confirm');
        preg_match('/code=([^&]+)/', $redirectUrl, $matches);
        $authCode = $matches[1];
        
        $cachedData = Cache::get("auth_code:$authCode");
        expect($cachedData['code_challenge'])->toBe($codeVerifier);
        expect($cachedData['code_challenge_method'])->toBe('plain');
    });

    test('preserves nonce parameter for ID token', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create([
            'redirect_uri' => 'http://example.com/callback'
        ]);

        $nonce = 'test-nonce-123';

        $this->actingAs($user)
            ->get(route('auth.authorize') . '?' . http_build_query([
                'client_id' => $app->client_id,
                'response_type' => 'code',
                'redirect_uri' => $app->redirect_uri,
                'nonce' => $nonce,
            ]));

        $redirectUrl = session('redirect_on_confirm');
        preg_match('/code=([^&]+)/', $redirectUrl, $matches);
        $authCode = $matches[1];
        
        $cachedData = Cache::get("auth_code:$authCode");
        expect($cachedData['nonce'])->toBe($nonce);
    });
});

describe('OIDC Token Endpoint', function () {
    test('rejects invalid authorization codes', function () {
        $app = Application::factory()->create();

        $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => 'invalid-code',
            'redirect_uri' => $app->redirect_uri,
            'client_id' => $app->client_id,
            'client_secret' => $app->client_secret,
        ])->assertStatus(403);
    });

    test('rejects expired authorization codes', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create();

        // Create expired code
        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
        ], now()->subMinutes(10)); // Expired

        $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
            'client_id' => $app->client_id,
            'client_secret' => $app->client_secret,
        ])->assertStatus(403);
    });

    test('validates client credentials', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create();

        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
        ], now()->addMinutes(5));

        $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
            'client_id' => $app->client_id,
            'client_secret' => 'wrong-secret',
        ])->assertStatus(403);
    });

    test('validates redirect_uri matches original request', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create([
            'redirect_uri' => 'http://example.com/callback'
        ]);

        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
        ], now()->addMinutes(5));

        $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => 'http://malicious.com/callback',
            'client_id' => $app->client_id,
            'client_secret' => $app->client_secret,
        ])->assertStatus(403);
    });

    test('successful token exchange with client credentials', function () {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'name' => 'Test User',
            'preferred_username' => 'testuser',
        ]);
        $app = Application::factory()->create();

        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
            'scope' => 'openid profile email',
        ], now()->addMinutes(5));

        $response = $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
            'client_id' => $app->client_id,
            'client_secret' => $app->client_secret,
        ]);

        $response->assertStatus(200);
        $data = $response->json();
        
        expect($data)->toHaveKeys(['access_token', 'token_type', 'expires_in', 'id_token']);
        expect($data['token_type'])->toBe('Bearer');
        expect($data['expires_in'])->toBe(3600);
        
        // Verify access token is stored
        $this->assertDatabaseHas('authentication_tokens', [
            'token' => $data['access_token'],
            'user_id' => $user->id,
            'application_id' => $app->id,
        ]);

        // Verify ID token is valid JWT
        $parser = new Parser(new JoseEncoder());
        $idToken = $parser->parse($data['id_token']);
        expect($idToken->claims()->get('sub'))->toBe((string) $user->uuid);
        expect($idToken->claims()->get('email'))->toBe($user->email);
    });

    test('successful PKCE token exchange with S256', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create();

        $codeVerifier = base64url_encode(random_bytes(32));
        $codeChallenge = base64url_encode(hash('sha256', $codeVerifier, true));

        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
            'code_challenge' => $codeChallenge,
            'code_challenge_method' => 'S256',
        ], now()->addMinutes(5));

        $response = $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
            'code_verifier' => $codeVerifier,
        ]);

        $response->assertStatus(200);
    });

    test('successful PKCE token exchange with plain method', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create();

        $codeVerifier = 'test-code-verifier';

        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
            'code_challenge' => $codeVerifier,
            'code_challenge_method' => 'plain',
        ], now()->addMinutes(5));

        $response = $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
            'code_verifier' => $codeVerifier,
        ]);

        $response->assertStatus(200);
    });

    test('rejects invalid PKCE code verifier', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create();

        $codeVerifier = base64url_encode(random_bytes(32));
        $codeChallenge = base64url_encode(hash('sha256', $codeVerifier, true));

        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
            'code_challenge' => $codeChallenge,
            'code_challenge_method' => 'S256',
        ], now()->addMinutes(5));

        $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
            'code_verifier' => 'wrong-verifier',
        ])->assertStatus(403);
    });

    test('includes nonce in ID token when provided', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create();

        $nonce = 'test-nonce-123';
        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
            'nonce' => $nonce,
        ], now()->addMinutes(5));

        $response = $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
            'client_id' => $app->client_id,
            'client_secret' => $app->client_secret,
        ]);

        $data = $response->json();
        $parser = new Parser(new JoseEncoder());
        $idToken = $parser->parse($data['id_token']);
        expect($idToken->claims()->get('nonce'))->toBe($nonce);
    });

    test('requires authentication method when no PKCE', function () {
        $user = User::factory()->create();
        $app = Application::factory()->create();

        $code = 'test-code';
        Cache::put("auth_code:$code", [
            'user_id' => $user->id,
            'client_id' => $app->id,
        ], now()->addMinutes(5));

        $this->post(route('auth.token'), [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $app->redirect_uri,
        ])->assertStatus(403);
    });
});

describe('OIDC UserInfo Endpoint', function () {
    test('requires authorization header', function () {
        $this->get(route('auth.userinfo'))
            ->assertStatus(400)
            ->assertJson(['error' => 'invalid_request']);
    });

    test('requires bearer token format', function () {
        $this->get(route('auth.userinfo'), [
            'Authorization' => 'Basic invalid'
        ])->assertStatus(400)
            ->assertJson(['error' => 'invalid_request']);
    });

    test('rejects invalid access tokens', function () {
        $this->get(route('auth.userinfo'), [
            'Authorization' => 'Bearer invalid-token'
        ])->assertStatus(401)
            ->assertJson(['error' => 'invalid_token']);
    });

    test('returns user info for valid access token', function () {
        $user = User::factory()->create([
            'email' => 'test@example.com',
            'name' => 'Test User',
            'preferred_username' => 'testuser',
            'avatar' => 'avatar.jpg',
        ]);
        $app = Application::factory()->create();
        $token = AuthenticationToken::factory()->create([
            'user_id' => $user->id,
            'application_id' => $app->id,
            'token' => 'valid-access-token',
        ]);

        $response = $this->get(route('auth.userinfo'), [
            'Authorization' => 'Bearer valid-access-token'
        ]);

        $response->assertStatus(200)
            ->assertJson([
                'sub' => (string) $user->uuid,
                'email' => $user->email,
                'name' => $user->name,
                'preferred_username' => $user->preferred_username,
                'picture' => $user->avatarUrl(),
            ]);
    });

    test('handles user without avatar', function () {
        $user = User::factory()->create([
            'avatar' => null,
        ]);
        $app = Application::factory()->create();
        $token = AuthenticationToken::factory()->create([
            'user_id' => $user->id,
            'application_id' => $app->id,
            'token' => 'valid-access-token',
        ]);

        $response = $this->get(route('auth.userinfo'), [
            'Authorization' => 'Bearer valid-access-token'
        ]);

        $response->assertStatus(200);
        $data = $response->json();
        expect($data['picture'])->toBeNull();
    });

});

describe('OIDC Discovery and JWKS', function () {
    test('openid configuration endpoint returns correct metadata', function () {
        $response = $this->get(route('auth.openid-configuration'));

        $response->assertStatus(200)
            ->assertJson([
                'issuer' => config('app.url'),
                'authorization_endpoint' => route('auth.authorize'),
                'token_endpoint' => route('auth.token'),
                'userinfo_endpoint' => route('auth.userinfo'),
                'jwks_uri' => route('auth.keys'),
                'scopes_supported' => ['openid', 'profile', 'email'],
                'response_types_supported' => ['code'],
                'id_token_signing_alg_values_supported' => ['RS256'],
                'claims_supported' => [
                    'sub',
                    'email',
                    'name',
                    'preferred_username',
                    'picture'
                ],
            ]);
    });

    test('jwks endpoint returns valid key set', function () {
        $response = $this->get(route('auth.keys'));

        $response->assertStatus(200);
        $data = $response->json();
        
        expect($data)->toHaveKey('keys');
        expect($data['keys'])->toBeArray();
        expect($data['keys'][0])->toHaveKeys(['kty', 'use', 'alg', 'kid', 'n', 'e']);
        expect($data['keys'][0]['kty'])->toBe('RSA');
        expect($data['keys'][0]['use'])->toBe('sig');
        expect($data['keys'][0]['alg'])->toBe('RS256');
    });
});

// Helper function for base64url encoding
function base64url_encode($data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}