<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Route;

/**
 * The website and the portal each answer on their own hostname, and nowhere
 * else.
 *
 * A route belongs to a host. One that is not registered for the host being
 * asked cannot answer, and the honest reply is 404 — that is the whole
 * mechanism, and Route::domain() is all of it.
 *
 * The first attempt bound the landing page alone and left everything else
 * host-agnostic. The website duly vanished from the portal, and the portal
 * stayed reachable from the website: www.clupilot.com/dashboard kept working.
 * Half a separation is no separation, which is why these tests check BOTH
 * directions for every route rather than only the one that was reported.
 *
 * Routes are built once at boot, so changing config alone changes nothing.
 * These re-register them against a fresh router — the idiom WelcomeTest already
 * uses for the status host.
 */
function routerWithHosts(string|array|null $siteHosts, ?string $appHost = null): Router
{
    config()->set('admin_access.site_hosts', array_values(array_filter((array) $siteHosts)));
    config()->set('admin_access.app_host', $appHost ?? '');

    $router = new Router(app('events'), app());
    Route::swap($router);
    require base_path('routes/web.php');
    $router->getRoutes()->refreshNameLookups();

    return $router;
}

/**
 * Dispatch through the fresh router, with the request bound where the framework
 * would bind it.
 *
 * Router::dispatch() on a standalone router does not rebind the container's
 * request, so a route that reads one — through the helper OR through an
 * injected parameter, both of which resolve from the container — would be
 * handed the test's own request instead of the one being answered. In a real
 * request the two are the same object; here they are not, and a redirect would
 * silently lose its query string in the test while working in production.
 */
function dispatchOn(Router $router, Request $request)
{
    app()->instance('request', $request);

    return $router->dispatch($request);
}

/** Does this host answer this path at all? */
function answers(Router $router, string $host, string $path): bool
{
    try {
        $router->getRoutes()->match(Request::create("http://{$host}{$path}", 'GET'));

        return true;
    } catch (Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
        return false;
    }
}

it('does not serve the portal on the website’s hostname', function () {
    // The half that was missing, and the one that was reported back.
    $router = routerWithHosts('www.example.test', 'app.example.test');

    foreach (['/dashboard', '/cloud', '/invoices', '/settings', '/login', '/register'] as $path) {
        expect(answers($router, 'app.example.test', $path))->toBeTrue("app should answer {$path}")
            ->and(answers($router, 'www.example.test', $path))->toBeFalse("www must not answer {$path}");
    }
});

it('does not serve the website on the portal’s hostname', function () {
    $router = routerWithHosts('www.example.test', 'app.example.test');

    foreach (['/robots.txt', '/legal/impressum', '/legal/agb'] as $path) {
        expect(answers($router, 'www.example.test', $path))->toBeTrue("www should answer {$path}")
            ->and(answers($router, 'app.example.test', $path))->toBeFalse("app must not answer {$path}");
    }

    expect($router->getRoutes()->match(Request::create('http://www.example.test/', 'GET'))->getActionName())
        ->toContain('LandingController');
});

it('answers "/" on the portal host with the product, never a 404', function () {
    // "/" is what somebody types from memory. Turning that into an error page
    // to make a point about hostnames helps nobody.
    $router = routerWithHosts('www.example.test', 'app.example.test');

    $response = dispatchOn($router, Request::create('http://app.example.test/', 'GET'));

    expect($response->getStatusCode())->toBe(302)
        ->and($response->headers->get('Location'))->toContain('/login');
});

it('sends a signed-in customer on the portal host to their dashboard', function () {
    $this->actingAs(User::factory()->create());

    $router = routerWithHosts('www.example.test', 'app.example.test');
    $response = dispatchOn($router, Request::create('http://app.example.test/', 'GET'));

    expect($response->getStatusCode())->toBe(302)
        ->and($response->headers->get('Location'))->toContain('/dashboard');
});

it('keeps the Stripe webhook reachable on any host', function () {
    // Stripe posts to whichever URL it was configured with, and a hostname
    // mismatch there loses payments rather than showing a 404 to a person.
    $router = routerWithHosts('www.example.test', 'app.example.test');

    $matches = function (string $host) use ($router) {
        try {
            $router->getRoutes()->match(Request::create("http://{$host}/webhooks/stripe", 'POST'));

            return true;
        } catch (Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
            return false;
        }
    };

    expect($matches('www.example.test'))->toBeTrue()
        ->and($matches('app.example.test'))->toBeTrue()
        ->and($matches('anything.example.test'))->toBeTrue();
});

it('binds nothing at all while no hostnames are configured', function () {
    // The installed base, and every development machine reached by a bare IP.
    // Switching this on by default would lock those out of everything at once.
    $router = routerWithHosts(null, null);

    foreach (['/', '/dashboard', '/legal/impressum'] as $path) {
        expect(answers($router, 'anything.example.test', $path))->toBeTrue("everything should answer {$path}");
    }
});

it('sends every other name for the website to the canonical one', function () {
    // An apex domain and its www are two names for one site. Answering on both
    // without picking one splits search rankings and cookies between them.
    $router = routerWithHosts(['www.example.test', 'example.test'], 'app.example.test');

    $response = dispatchOn($router, Request::create('http://example.test/legal/impressum', 'GET'));

    expect($response->getStatusCode())->toBe(301)
        ->and($response->headers->get('Location'))->toBe('https://www.example.test/legal/impressum');
});

it('keeps the path and the query when it redirects to the canonical name', function () {
    // A bookmark or an ad link carries both, and dropping them turns a
    // redirect into a dead end at the front page.
    $router = routerWithHosts(['www.example.test', 'example.test'], 'app.example.test');

    $response = dispatchOn($router, Request::create('http://example.test/legal/agb?ref=mail', 'GET'));

    expect($response->headers->get('Location'))->toBe('https://www.example.test/legal/agb?ref=mail');
});

it('does not let the alias redirect swallow any other host', function () {
    // It is a catch-all, and a catch-all registered without a host would eat
    // every path in the application.
    $router = routerWithHosts(['www.example.test', 'example.test'], 'app.example.test');

    expect(answers($router, 'app.example.test', '/dashboard'))->toBeTrue()
        ->and(answers($router, 'www.example.test', '/legal/impressum'))->toBeTrue();
});
