<?php

declare(strict_types=1);

namespace App\Tests\Integration\Support;

use App\App\AppFactory;
use App\App\Container;
use App\Auth\OidcAuthenticator;
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Monolog\Handler\NullHandler;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Slim\App;
use Slim\Psr7\Factory\ServerRequestFactory;
use Slim\Psr7\Factory\StreamFactory;

/**
 * Boots the UI Slim app with a Guzzle MockHandler swapped in for the
 * api-side calls. Sessions run in CLI-fallback mode (no headers); each
 * test gets a fresh `$_SESSION` superglobal.
 *
 * The mocked responses are queued via `enqueueApiResponse(...)` before
 * the request under test fires.
 */
abstract class AppTestCase extends TestCase
{
    protected ContainerInterface $container;
    protected App $app;
    protected MockHandler $mock;
    /** @var list<array{request: \Psr\Http\Message\RequestInterface, response: \Psr\Http\Message\ResponseInterface, error: mixed, options: array<string, mixed>}> */
    protected array $apiHistory = [];
    private ?string $throttlePath = null;

    protected function tearDown(): void
    {
        parent::tearDown();
        if ($this->throttlePath !== null) {
            @unlink($this->throttlePath);
            @unlink($this->throttlePath . '.lock');
            $this->throttlePath = null;
        }
    }

    /**
     * @param array<string, mixed> $overrides
     */
    protected function bootApp(array $overrides = []): void
    {
        // Reset the global session bag between tests.
        $_SESSION = [];

        $this->throttlePath = sys_get_temp_dir() . '/irdb_login_throttle_test_'
            . bin2hex(random_bytes(8)) . '.json';

        $defaults = [
            'app_env' => 'development',
            'log_level' => \Monolog\Level::Warning,
            'public_url' => 'http://localhost:8080',
            'api_base_url' => 'http://api:8081',
            'ui_service_token' => 'irdb_svc_TESTTOKEN',
            'api_timeout_seconds' => 1.0,
            'oidc_enabled' => false,
            'oidc_issuer' => '',
            'oidc_client_id' => '',
            'oidc_client_secret' => '',
            'oidc_redirect_uri' => '',
            'local_admin_enabled' => true,
            'local_admin_username' => 'admin',
            'local_admin_password_hash' => password_hash('test1234', PASSWORD_ARGON2ID),
            'session_idle_seconds' => 28800,
            'session_absolute_seconds' => 86400,
            'login_throttle_path' => $this->throttlePath,
        ];
        $settings = array_replace($defaults, $overrides);

        $this->container = Container::build($settings);
        $this->mock = new MockHandler();
        $this->apiHistory = [];

        if (method_exists($this->container, 'set')) {
            /** @var \DI\Container $c */
            $c = $this->container;
            $handler = HandlerStack::create($this->mock);
            $handler->push(Middleware::history($this->apiHistory));
            $c->set(GuzzleClientInterface::class, new \GuzzleHttp\Client(['handler' => $handler]));
            // ApiClient closure-builds from the container; refresh the
            // bound instance with our mocked Guzzle client.
            $c->set(\App\ApiClient\ApiClient::class, new \App\ApiClient\ApiClient(
                http: $c->get(GuzzleClientInterface::class),
                serviceToken: (string) $c->get('settings.ui_service_token'),
                health: $c->get(\App\ApiClient\ApiHealth::class),
            ));
            // Quiet the logger so test output stays clean.
            $nullLogger = new Logger('test');
            $nullLogger->pushHandler(new NullHandler());
            $c->set(LoggerInterface::class, $nullLogger);
        }

        $this->app = AppFactory::build($this->container);
    }

    /**
     * @param array<string, mixed> $body
     */
    protected function enqueueApiResponse(int $status, array $body, string $contentType = 'application/json'): void
    {
        $this->mock->append(new \GuzzleHttp\Psr7\Response(
            $status,
            ['Content-Type' => $contentType],
            (string) json_encode($body),
        ));
    }

    protected function enqueueApiException(\Throwable $e): void
    {
        $this->mock->append($e);
    }

    /**
     * @param array<string, string> $headers
     * @param array<string, mixed>  $serverParams
     */
    protected function request(
        string $method,
        string $path,
        array $headers = [],
        ?string $body = null,
        ?string $contentType = null,
        array $serverParams = [],
    ): \Psr\Http\Message\ResponseInterface {
        $factory = new ServerRequestFactory();
        $request = $factory->createServerRequest($method, $path, $serverParams);
        foreach ($headers as $name => $value) {
            $request = $request->withHeader($name, $value);
        }
        if ($contentType !== null) {
            $request = $request->withHeader('Content-Type', $contentType);
        }
        if ($body !== null) {
            $stream = (new StreamFactory())->createStream($body);
            $request = $request->withBody($stream);
        }

        return $this->app->handle($request);
    }

    /**
     * Swap in a Monolog `TestHandler` so a test can assert what reached
     * the logger. Re-builds controllers that captured the old logger
     * (currently just `OidcController`) so they pick up the new binding.
     */
    protected function captureLogs(): TestHandler
    {
        $handler = new TestHandler();
        $logger = new Logger('test');
        $logger->pushHandler($handler);
        if (method_exists($this->container, 'set')) {
            /** @var \DI\Container $c */
            $c = $this->container;
            $c->set(LoggerInterface::class, $logger);
            $c->set(\App\Auth\OidcController::class, new \App\Auth\OidcController(
                authenticator: $c->get(OidcAuthenticator::class),
                auth: $c->get(\App\ApiClient\AuthClient::class),
                sessions: $c->get(\App\Auth\SessionManager::class),
                logger: $logger,
                enabled: (bool) $c->get('settings.oidc_enabled'),
            ));
            $this->app = AppFactory::build($c);
        }

        return $handler;
    }

    /**
     * Replace the `OidcAuthenticator` binding with a fake callback that
     * the test controls. Lets us drive the OIDC controller's success +
     * failure paths without a real IdP.
     */
    protected function bindOidcAuthenticator(OidcAuthenticator $stub): void
    {
        if (method_exists($this->container, 'set')) {
            /** @var \DI\Container $c */
            $c = $this->container;
            $c->set(OidcAuthenticator::class, $stub);
            // Re-build the OidcController so it picks up the new binding.
            $c->set(\App\Auth\OidcController::class, new \App\Auth\OidcController(
                authenticator: $stub,
                auth: $c->get(\App\ApiClient\AuthClient::class),
                sessions: $c->get(\App\Auth\SessionManager::class),
                logger: $c->get(LoggerInterface::class),
                enabled: (bool) $c->get('settings.oidc_enabled'),
            ));
            $this->app = AppFactory::build($c);
        }
    }
}
