<?php

namespace Tests\Feature\Middleware;

use App\Facades\Settings;
use App\Master\Models\ApiToken;
use App\Master\Models\ApiUserRoute;
use App\Services\ApiUsageTracker;
use Database\Seeders\Tenant\ApiUserRouteRestrictionSettingSeeder;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Symfony\Component\HttpFoundation\Response;
use App\Http\Middleware\ApiAuthenticatedRequest;
use Tests\DatabaseTestCase;

class ApiAuthenticatedRequestTest extends DatabaseTestCase
{
    #[\Override]
    protected function setUp(): void
    {
        parent::setUp();
        $this->middleware = new ApiAuthenticatedRequest();
    }

    public function testDeniesAccessIfNoAuthorizationHeader(): void
    {
        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $response = $this->getResponse($request);

        $this->assertEquals(401, $response->getStatusCode());
        $this->assertStringContainsString('Access Denied', $response->getContent());
    }

    public function testDeniesAccessWithInvalidToken(): void
    {
        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer invalidtoken');
        $response = $this->getResponse($request);

        $this->assertEquals(401, $response->getStatusCode());
        $this->assertStringContainsString('Access Denied', $response->getContent());
    }

    public function testDeniesAccessWithExpiredToken(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->expired()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);

        $response = $this->getResponse($request);

        $this->assertEquals(401, $response->getStatusCode());
        $this->assertStringContainsString('Access Denied', $response->getContent());
    }

    public function testDeniesAccessIfTokenHasNoTenantAccess(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach('some-other-tenant');

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);

        $response = $this->getResponse($request);

        $this->assertEquals(401, $response->getStatusCode());
    }

    public function testAllowsAccessWithValidTokenAndAllowedRoute(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'test.route',
        ]);

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $response = $this->getResponse($request);

        $this->assertEquals(200, $response->getStatusCode());
    }

    public function testDeniesAccessWhenRouteNotInAllowedList(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'some.other.route',
        ]);

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $response = $this->getResponse($request);

        $this->assertEquals(403, $response->getStatusCode());
        $this->assertStringContainsString('Forbidden', $response->getContent());
    }

    public function testDeniesAccessWhenUserHasNoAllowedRoutes(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $response = $this->getResponse($request);

        $this->assertEquals(403, $response->getStatusCode());
        $this->assertStringContainsString('Forbidden', $response->getContent());
    }

    public function testAllowsDisallowedRouteWhenRestrictionSettingDisabled(): void
    {
        tenant()->saveQuietly();
        (new ApiUserRouteRestrictionSettingSeeder())->run();
        Settings::update('api-users-restrict-routes', false);

        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'some.other.route',
        ]);

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $response = $this->getResponse($request);

        $this->assertEquals(200, $response->getStatusCode());
    }

    public function testRestrictsRoutesWhenRestrictionSettingEnabled(): void
    {
        tenant()->saveQuietly();
        (new ApiUserRouteRestrictionSettingSeeder())->run();

        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'some.other.route',
        ]);

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $response = $this->getResponse($request);

        $this->assertEquals(403, $response->getStatusCode());
    }

    public function testAllowsAccessToMultipleAllowedRoutes(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'route.one',
        ]);
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'route.two',
        ]);

        $request = $this->createRequestWithRoute('/one', 'GET', 'route.one');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $this->assertEquals(200, $this->getResponse($request)->getStatusCode());

        $request = $this->createRequestWithRoute('/two', 'GET', 'route.two');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $this->assertEquals(200, $this->getResponse($request)->getStatusCode());
    }

    public function testRecordsApiUsageForAuthenticatedRequest(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'test.route',
        ]);

        $tracker = \Mockery::mock(ApiUsageTracker::class);
        $tracker->shouldReceive('record')->once();
        $this->app->instance(ApiUsageTracker::class, $tracker);

        $request = $this->createRequestWithRoute('/dummy', 'POST', 'test.route', ['foo' => 'bar']);
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);

        (new ApiAuthenticatedRequest())->handle($request, fn () => response('ok'));
    }

    public function testAllowsExportMapAccessWithQueryParameterKey(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'tenant.exports.download',
        ]);

        $request = $this->createRequestWithRoute(
            '/dummy?key=' . $apiToken->token,
            'GET',
            'tenant.exports.download'
        );
        $response = $this->getResponse($request, 'export-map');

        $this->assertEquals(200, $response->getStatusCode());
    }

    public function testAllowsExportMapAccessWithBearerToken(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'tenant.exports.download',
        ]);

        $request = $this->createRequestWithRoute('/dummy', 'GET', 'tenant.exports.download');
        $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
        $response = $this->getResponse($request, 'export-map');

        $this->assertEquals(200, $response->getStatusCode());
    }

    public function testDeniesExportMapAccessWithInvalidKey(): void
    {
        $request = Request::create('/dummy?key=invalidtoken', 'GET');
        $response = $this->getResponse($request, 'export-map');

        $this->assertEquals(401, $response->getStatusCode());
    }

    public function testCachesSuccessfulTokenLookupToAvoidRepeatQueries(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'test.route',
        ]);

        $makeRequest = function () use ($apiToken) {
            $request = $this->createRequestWithRoute('/dummy', 'GET', 'test.route');
            $request->headers->set('Authorization', 'Bearer ' . $apiToken->token);
            return $this->getResponse($request);
        };

        // First call performs the lookup; subsequent calls within the TTL are served
        // from cache and run no further token queries. The per-request route-access
        // check (api_user_routes) is a separate, intentionally-uncached concern, so it
        // is excluded from this assertion.
        $this->assertEquals(200, $makeRequest()->getStatusCode());

        \DB::enableQueryLog();
        $this->assertEquals(200, $makeRequest()->getStatusCode());
        $queries = collect(\DB::getQueryLog())
            ->filter(fn ($q) => str_contains($q['query'], 'api_tokens') || str_contains($q['query'], 'api_user'))
            ->reject(fn ($q) => str_contains($q['query'], 'api_user_routes'));
        \DB::disableQueryLog();

        $this->assertCount(0, $queries, 'Cached token check should issue no token/tenant queries');
    }

    public function testAutoImagingAllowsAccessWithValidConfigKey(): void
    {
        config(['services.image-sync.auto-imaging.key' => 'test-imaging-key']);

        $request = $this->createRequestWithRoute('/dummy', 'POST', 'auto-imaging.store', ['key' => 'test-imaging-key']);
        $response = $this->middleware->handle($request, fn () => response('ok'), 'auto-imaging');

        $this->assertEquals(200, $response->getStatusCode());
    }

    public function testAutoImagingDeniesAccessWithInvalidKey(): void
    {
        config(['services.image-sync.auto-imaging.key' => 'test-imaging-key']);

        $request = $this->createRequestWithRoute('/dummy', 'POST', 'auto-imaging.store', ['key' => 'wrong-key']);
        $response = $this->middleware->handle($request, fn () => response('ok'), 'auto-imaging');

        $this->assertEquals(401, $response->getStatusCode());
    }

    public function testAutoImagingAllowsAccessWithValidApiTokenAndAllowedRoute(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'auto-imaging.store',
        ]);

        $request = $this->createRequestWithRoute('/dummy', 'POST', 'auto-imaging.store', ['key' => $apiToken->token]);
        $response = $this->middleware->handle($request, fn () => response('ok'), 'auto-imaging');

        $this->assertEquals(200, $response->getStatusCode());
    }

    public function testAutoImagingDeniesAccessWithValidApiTokenButDisallowedRoute(): void
    {
        tenant()->saveQuietly();
        $apiToken = ApiToken::factory()->create();
        $apiToken->apiUser->tenants()->attach(tenant('id'));
        ApiUserRoute::create([
            'api_user_id' => $apiToken->apiUser->id,
            'route_name' => 'some.other.route',
        ]);

        $request = $this->createRequestWithRoute('/dummy', 'POST', 'auto-imaging.store', ['key' => $apiToken->token]);
        $response = $this->middleware->handle($request, fn () => response('ok'), 'auto-imaging');

        $this->assertEquals(403, $response->getStatusCode());
    }

    private function createRequestWithRoute(string $uri, string $method, string $routeName, array $data = []): Request
    {
        $request = Request::create($uri, $method, $data);
        $route = new Route($method, $uri, fn () => response('ok'));
        $route->name($routeName);
        $request->setRouteResolver(fn () => $route);

        return $request;
    }

    private function getResponse($request, ?string $legacyScenario = null): Response
    {
        return $this->middleware->handle($request, fn () => response('ok'), $legacyScenario);
    }
}
