<?php

namespace App\Providers;

use App\Development\Manifest;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;

/**
 * Centralised HTTP Mock Provider
 *
 * This exists to provide a central space to register mocks for the application.
 * This decomplicates code broadly speaking, and decreases the overall surface area
 * for a mock to end up accidentally firing in production.
 *
 * This protects against unmapped outbound calls.
 *
 * Enable with CREATIA_MOCK_HTTP=true in the environment file. Only works if the
 * environment is set to development.
 *
 * Routes live in App\Development\Manifest::register(); each maps a URL pattern
 * to a [class, method] handler in app/Development/. Handlers receive the request
 * params (merged query + body) and the raw request, and return either an
 * Http::response(...) or a bare array/string which is wrapped for them.
 */
class HttpMockServiceProvider extends ServiceProvider
{
    /**
     * Base namespace that manifest handler class names are resolved against.
     */
    protected const HANDLER_NAMESPACE = 'App\\Development\\';

    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        if (! $this->app->environment('development') || ! config('creatia.mock.http')) {
            return;
        }

        Log::info('Running HTTP Mock Service Provider');

        Http::preventStrayRequests();
        Http::fake($this->stubs());
    }

    /**
     * Build the Http::fake() map from the development manifest, turning each
     * [class, method] entry into a closure that dispatches the live request.
     *
     * @return array<string, \Closure>
     */
    protected function stubs(): array
    {
        $stubs = [];

        foreach ($this->app->make(Manifest::class)->register() as $pattern => $handler) {
            $stubs[$pattern] = $this->dispatcher($handler);
        }

        return $stubs;
    }

    /**
     * Wrap a manifest handler in a closure suitable for Http::fake().
     *
     * @param  array{0: string, 1: string}|string  $handler
     */
    protected function dispatcher($handler): \Closure
    {
        [$class, $method] = $this->resolve($handler);

        return function (Request $request) use ($class, $method) {
            $result = $this->app->make($class)->{$method}($this->params($request), $request);

            return $result instanceof PromiseInterface ? $result : Http::response($result);
        };
    }

    /**
     * Normalise a manifest handler reference into a [class, method] pair.
     * Accepts a ['Class', 'method'] tuple or a 'Class@method' string, and
     * resolves bare/sub-namespaced class names against App\Development.
     *
     * @param  array{0: string, 1: string}|string  $handler
     * @return array{0: string, 1: string}
     */
    protected function resolve($handler): array
    {
        [$class, $method] = is_string($handler) ? explode('@', $handler, 2) : $handler;

        if (! class_exists($class)) {
            $class = self::HANDLER_NAMESPACE . ltrim($class, '\\');
        }

        return [$class, $method];
    }

    /**
     * The parameters actually being sent: query string merged over body data.
     *
     * @return array<string, mixed>
     */
    protected function params(Request $request): array
    {
        parse_str((string) parse_url($request->url(), PHP_URL_QUERY), $query);

        return array_merge($query, $request->data() ?: []);
    }
}
