<?php

namespace Tests\Feature\Api;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route as LaravelRoute;
use Illuminate\Support\Facades\Route;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;

class ApiAuthenticationAndAuthorizationTest extends TestCase
{
    use CreatesApiTestUsers;
    use RefreshDatabase;


    public function test_api_login_returns_token_and_user_object(): void
    {
        $user = $this->createApiUserWithRole('teacher', [
            'firstname' => 'Login',
            'lastname' => 'Tester',
            'email' => 'api.login@example.test',
        ]);

        $this->postJson('/api/v1/auth/login', [
            'email' => strtoupper($user->email),
            'password' => 'password',
        ])
            ->assertOk()
            ->assertJsonPath('status', true)
            ->assertJsonStructure([
                'token',
                'access_token',
                'token_type',
                'expires_in',
                'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'],
            ])
            ->assertJsonPath('user.email', 'api.login@example.test');
    }

    public function test_session_login_also_returns_token_and_user_object_for_spa_clients(): void
    {
        $user = $this->createApiUserWithRole('teacher', [
            'firstname' => 'Session',
            'lastname' => 'Tester',
            'email' => 'session.login@example.test',
        ]);

        $this->postJson('/user/login', [
            'email' => $user->email,
            'password' => 'password',
        ])
            ->assertOk()
            ->assertJsonPath('status', true)
            ->assertJsonStructure([
                'token',
                'access_token',
                'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'],
                'next_url',
            ])
            ->assertJsonPath('user.email', 'session.login@example.test');
    }

    public function test_auth_me_requires_authentication(): void
    {
        $this->getJson('/api/v1/auth/me')->assertUnauthorized();
    }

    public function test_auth_me_returns_the_current_authenticated_user(): void
    {
        $user = $this->createApiUserWithRole('teacher', [
            'firstname' => 'Teacher',
            'lastname' => 'Tester',
            'email' => 'teacher.me@example.test',
        ]);

        $this->actingAs($user, 'api')
            ->getJson('/api/v1/auth/me')
            ->assertOk()
            ->assertJsonPath('status', true)
            ->assertJsonPath('data.email', 'teacher.me@example.test');
    }

    public function test_admin_only_routes_reject_non_admin_users(): void
    {
        $parent = $this->createApiUserWithRole('parent');
        $this->actingAs($parent, 'api');

        foreach ($this->adminOnlyEndpoints() as [$method, $uri]) {
            $response = $this->json($method, $uri);

            $this->assertContains(
                $response->getStatusCode(),
                [401, 403],
                "$method $uri should reject a non-admin user; got {$response->getStatusCode()}"
            );
        }
    }

    public function test_protected_api_routes_reject_anonymous_requests_before_business_logic(): void
    {
        $failures = [];

        foreach ($this->sampleProtectedRoutes() as $route) {
            $method = $this->primaryMethod($route);
            $uri = '/' . $this->uriWithPlaceholders($route->uri());

            $response = $this->json($method, $uri);
            $status = $response->getStatusCode();

            if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
                $failures[] = "$method $uri returned $status";
            }
        }

        $this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n" . implode("\n", $failures));
    }

    /**
     * @return list<array{0: string, 1: string}>
     */
    private function adminOnlyEndpoints(): array
    {
        return [
            ['GET', '/api/v1/users'],
            ['POST', '/api/v1/users'],
            ['GET', '/api/v1/administrator/dashboard/metrics'],
            ['GET', '/api/v1/administrator/staff'],
            ['GET', '/api/v1/school-years'],
            ['GET', '/api/v1/administrator/role-permission/roles'],
        ];
    }

    /**
     * @return list<LaravelRoute>
     */
    private function sampleProtectedRoutes(): array
    {
        return collect(Route::getRoutes()->getRoutes())
            ->filter(fn (LaravelRoute $route): bool => str_starts_with($route->uri(), 'api/'))
            ->filter(function (LaravelRoute $route): bool {
                return collect($route->gatherMiddleware())->contains(function (string $middleware): bool {
                    return str_contains($middleware, 'auth:api') || str_contains($middleware, 'jwt.auth');
                });
            })
            ->reject(function (LaravelRoute $route): bool {
                $uri = $route->uri();

                return str_contains($uri, 'files/')
                    || str_contains($uri, 'receipts/')
                    || str_contains($uri, 'reimbursements/')
                    || str_contains($uri, 'attachments/');
            })
            ->take(80)
            ->values()
            ->all();
    }

    private function primaryMethod(LaravelRoute $route): string
    {
        foreach ($route->methods() as $method) {
            if (! in_array($method, ['HEAD', 'OPTIONS'], true)) {
                return $method;
            }
        }

        return 'GET';
    }

    private function uriWithPlaceholders(string $uri): string
    {
        return preg_replace('/\{[^}]+\}/', '1', $uri) ?? $uri;
    }
}
