<?php

declare(strict_types=1);

use App\Framework\Auth\Auth;
use App\Framework\Auth\RouteAuthorizationService;
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;
use App\Framework\Config\TypedConfiguration;
use App\Framework\Http\IpAddress;
use App\Framework\Http\Request;
use App\Framework\Router\Exception\RouteNotFound;
use App\Framework\Router\RouteContext;

describe('RouteAuthorizationService', function () {
    beforeEach(function () {
        // Create stub TypedConfiguration
        $this->config = createStubConfigForUnitTest(debug: false);
    });

    afterEach(function () {
        Mockery::close();
    });

    describe('namespace-based access blocking', function () {
        it('blocks all controllers when namespace is blocked', function () {
            $namespaceConfig = [
                'App\Application\Admin\*' => [
                    'access_policy' => NamespaceAccessPolicy::blocked(),
                ],
            ];

            $service = new RouteAuthorizationService($this->config, $namespaceConfig);

            $request = createMockRequest('127.0.0.1');
            $routeContext = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard'
            );

            expect(fn () => $service->authorize($request, $routeContext))
                ->toThrow(RouteNotFound::class);
        });

        it('allows controllers in allowlist when namespace is blocked', function () {
            $namespaceConfig = [
                'App\Application\Admin\*' => [
                    'access_policy' => NamespaceAccessPolicy::blockedExcept(
                        'App\Application\Admin\LoginController'
                    ),
                ],
            ];

            $service = new RouteAuthorizationService($this->config, $namespaceConfig);

            $request = createMockRequest('127.0.0.1');

            // LoginController should be allowed
            $loginRoute = createMockRouteContext(
                'App\Application\Admin\LoginController',
                '/admin/login'
            );

            expect(fn () => $service->authorize($request, $loginRoute))
                ->not->toThrow(RouteNotFound::class);

            // Dashboard should be blocked
            $dashboardRoute = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard'
            );

            expect(fn () => $service->authorize($request, $dashboardRoute))
                ->toThrow(RouteNotFound::class);
        });

        it('allows all controllers when no access policy is set', function () {
            $namespaceConfig = [
                'App\Application\Admin\*' => [
                    'visibility' => 'public',
                    // No access_policy - should not block
                ],
            ];

            $service = new RouteAuthorizationService($this->config, $namespaceConfig);

            $request = createMockRequest('127.0.0.1');
            $routeContext = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard'
            );

            expect(fn () => $service->authorize($request, $routeContext))
                ->not->toThrow(RouteNotFound::class);
        });
    });

    describe('namespace pattern matching', function () {
        it('matches wildcard patterns', function () {
            $namespaceConfig = [
                'App\Application\Admin\*' => [
                    'access_policy' => NamespaceAccessPolicy::blocked(),
                ],
            ];

            $service = new RouteAuthorizationService($this->config, $namespaceConfig);

            $request = createMockRequest('127.0.0.1');

            // Should match App\Application\Admin\Dashboard
            $route1 = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard'
            );
            expect(fn () => $service->authorize($request, $route1))
                ->toThrow(RouteNotFound::class);

            // Should match App\Application\Admin\Users\UserController
            $route2 = createMockRouteContext(
                'App\Application\Admin\Users\UserController',
                '/admin/users'
            );
            expect(fn () => $service->authorize($request, $route2))
                ->toThrow(RouteNotFound::class);

            // Should NOT match App\Application\Api\UserController
            $route3 = createMockRouteContext(
                'App\Application\Api\UserController',
                '/api/users'
            );
            expect(fn () => $service->authorize($request, $route3))
                ->not->toThrow(RouteNotFound::class);
        });

        it('matches exact namespaces', function () {
            $namespaceConfig = [
                'App\Application\Admin' => [
                    'access_policy' => NamespaceAccessPolicy::blocked(),
                ],
            ];

            $service = new RouteAuthorizationService($this->config, $namespaceConfig);

            $request = createMockRequest('127.0.0.1');

            // Should match exact namespace
            $route1 = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard'
            );
            expect(fn () => $service->authorize($request, $route1))
                ->toThrow(RouteNotFound::class);
        });
    });

    describe('multiple namespace configurations', function () {
        it('applies first matching namespace config', function () {
            $namespaceConfig = [
                'App\Application\Admin\*' => [
                    'access_policy' => NamespaceAccessPolicy::blocked(),
                ],
                'App\Application\Api\*' => [
                    'access_policy' => NamespaceAccessPolicy::blockedExcept(
                        'App\Application\Api\HealthController'
                    ),
                ],
            ];

            $service = new RouteAuthorizationService($this->config, $namespaceConfig);

            $request = createMockRequest('127.0.0.1');

            // Admin should be blocked
            $adminRoute = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard'
            );
            expect(fn () => $service->authorize($request, $adminRoute))
                ->toThrow(RouteNotFound::class);

            // API Health should be allowed
            $healthRoute = createMockRouteContext(
                'App\Application\Api\HealthController',
                '/api/health'
            );
            expect(fn () => $service->authorize($request, $healthRoute))
                ->not->toThrow(RouteNotFound::class);

            // Other API should be blocked
            $apiRoute = createMockRouteContext(
                'App\Application\Api\UsersController',
                '/api/users'
            );
            expect(fn () => $service->authorize($request, $apiRoute))
                ->toThrow(RouteNotFound::class);
        });
    });

    describe('legacy Auth attribute', function () {
        it('blocks non-wireguard IPs when Auth attribute is present', function () {
            $service = new RouteAuthorizationService($this->config, []);

            $request = createMockRequest('192.168.1.100');
            $routeContext = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard',
                attributes: [Auth::class]
            );

            expect(fn () => $service->authorize($request, $routeContext))
                ->toThrow(RouteNotFound::class);
        });

        it('allows wireguard IP when Auth attribute is present', function () {
            $service = new RouteAuthorizationService($this->config, []);

            $request = createMockRequest('172.20.0.1');
            $routeContext = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard',
                attributes: [Auth::class]
            );

            expect(fn () => $service->authorize($request, $routeContext))
                ->not->toThrow(RouteNotFound::class);
        });

        it('skips Auth check in debug mode', function () {
            $debugConfig = createStubConfigForUnitTest(debug: true);

            $service = new RouteAuthorizationService($debugConfig, []);

            $request = createMockRequest('192.168.1.100');
            $routeContext = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard',
                attributes: [Auth::class]
            );

            // Should not throw in debug mode
            expect(fn () => $service->authorize($request, $routeContext))
                ->not->toThrow(RouteNotFound::class);
        });
    });

    describe('factory method', function () {
        it('creates service with namespace config', function () {
            $namespaceConfig = [
                'App\Application\Admin\*' => [
                    'access_policy' => NamespaceAccessPolicy::blocked(),
                ],
            ];

            $service = RouteAuthorizationService::withNamespaceConfig(
                $this->config,
                $namespaceConfig
            );

            expect($service)->toBeInstanceOf(RouteAuthorizationService::class);

            $request = createMockRequest('127.0.0.1');
            $routeContext = createMockRouteContext(
                'App\Application\Admin\Dashboard',
                '/admin/dashboard'
            );

            expect(fn () => $service->authorize($request, $routeContext))
                ->toThrow(RouteNotFound::class);
        });
    });
});

// Helper functions for test setup

function createStubConfigForUnitTest(bool $debug = false): TypedConfiguration
{
    return new class ($debug) extends TypedConfiguration {
        public function __construct(bool $debug)
        {
            $this->app = (object)['debug' => $debug];
        }
    };
}

function createMockRequest(string $ipAddress): Request
{
    $request = Mockery::mock(Request::class);
    $server = Mockery::mock('server');
    $server->shouldReceive('getClientIp')
        ->andReturn(IpAddress::fromString($ipAddress));
    $request->server = $server;

    return $request;
}

function createMockRouteContext(
    string $controllerClass,
    string $path,
    array $attributes = []
): RouteContext {
    $route = (object)[
        'controller' => $controllerClass,
        'action' => 'index',
        'attributes' => $attributes,
    ];

    $match = (object)['route' => $route];

    $routeContext = Mockery::mock(RouteContext::class);
    $routeContext->match = $match;
    $routeContext->path = $path;
    $routeContext->shouldReceive('isSuccess')->andReturn(true);

    return $routeContext;
}
