<?php

declare(strict_types=1);

namespace apexl\Io\TestInfrastructure;

use apexl\Io\Client;
use apexl\Io\enums\HttpMethod;
use apexl\Io\factories\ClientFactory;
use apexl\Io\factories\ContainerFactory;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\services\FilePaths;
use apexl\Vault\exceptions\ExecutionException;
use apexl\Vault\Vault;
use DI\Bridge\Slim\Bridge;
use DI\Container;
use Dotenv\Dotenv;
use Faker\Factory as FakerFactory;
use Phinx\Console\PhinxApplication;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Slim\App;
use Slim\Psr7\Factory\RequestFactory;
use Slim\Psr7\Request;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\NullOutput;
use function apexl\Io\config;

abstract class FeatureTest extends TestCase
{
    protected const bool USES_DATABASE = true;
    const string BASE_DIR = __DIR__ . '/../../../../../..';

    protected static ?Vault $db;
    protected static ?App $application;
    private array $spoofedServerVars = [];

    private RequestFactory $requestFactory;

    public static function setUpBeforeClass(): void
    {
        Dotenv::createImmutable(self::BASE_DIR)->load();
        parent::setUpBeforeClass(); // TODO: Change the autogenerated stub
        self::bootstrapApplication();
    }

    protected static function bootstrapApplication(): void
    {
        $application = Bridge::create(new ContainerFactory()->make(self::BASE_DIR));
        self::$application = $application;
        /** @var Container $container */
        $container = $application->getContainer();
        $GLOBALS['container'] = $container;

        $container->set(FilePaths::class, new FilePaths(self::BASE_DIR));

        $client = $container->get(ClientFactory::class)->make();
        $container->set(Client::class, $client);

        if (static::USES_DATABASE) {
            self::$db = $container->get(Vault::class);
            self::migrateDatabase();
        }

        //start the app (bootstrap), provide the primary config file.
        $client->bootstrap();
    }

    protected function get(string $uri, array $headers = [], ?int $expectCode = 200): array
    {
        return $this->executeRequest(
            method: HttpMethod::GET,
            uri: $uri,
            headers: $headers,
            expectCode: $expectCode,
        );
    }

    protected function executeRequest(HttpMethod $method, string $uri, array $body = [], array $headers = [], ?int $expectCode = 200): array
    {
        $request = $this->request($method, $uri, $body, $headers);

        $response = self::$application->handle($request);

        return $this->responseToData($response, $expectCode);
    }

    protected function request(HttpMethod $method, string $uri, array $body = [], array $headers = []): Request
    {
        /** @var Request $request */
        $request = $this->requestFactory
            ->createRequest($method->name, $uri)
            ->withHeader('accept', 'application/json');

        foreach ($headers as $key => $value) {
            $request = $request->withHeader($key, $value);
        }

        if ($body) {
            $request = $request->withHeader('content-type', 'application/json');
            $request->getBody()->write(json_encode($body));
        }

        return $request;

    }

    private function responseToData(ResponseInterface $response, ?int $expectCode = 200): array
    {
        if ($expectCode !== null) {
            $this->assertEquals($expectCode, $response->getStatusCode());
        }
        $response->getBody()->rewind();

        return json_decode($response->getBody()->getContents(), true);

    }

    private static function migrateDatabase(): void
    {
        self::$db->executeSql('drop database test');
        self::$db->executeSql('create database test');
        self::$db->executeSql('use test');

        $output = new ConsoleOutput();
        $output = new NullOutput(); // Comment this line to see the output of the migration

        $app = new PhinxApplication();
        $app->setAutoExit(false);
        $app->run(
            new StringInput(
                <<<'CLI'
                migrate 
                    --configuration ./api/config/phinx.php
                CLI,
            ),
            $output
        );

        $app->run(
            new StringInput(
                <<<'CLI'
                seed:run 
                    --configuration ./api/config/phinx.php
                CLI,
            ),
            $output
        );
    }

    public static function tearDownAfterClass(): void
    {

        if (static::USES_DATABASE) {

            try {
                self::$db->executeSql('KILL CONNECTION_ID()');
            } catch (ExecutionException) {
                // Silently swallow expected exception so as not to upset PHPUnit
            } finally {
                self::$db = null;
            }
        }
        self::$application = null;
        $GLOBALS['container'] = null;
    }

    protected function setUp(): void
    {
        if (static::USES_DATABASE) {
            self::$db->executeSql('START TRANSACTION');
        }

        $this->faker = FakerFactory::create();
        $this->requestFactory = new RequestFactory();
        $this->spoofServerVars();
    }

    private function spoofServerVars(): void
    {
        $backendDomain = config('app.site.backend_domain');
        $https = parse_url($backendDomain, PHP_URL_SCHEME) === 'https' ? 'on' : 'off';
        $host = parse_url($backendDomain, PHP_URL_HOST);

        foreach ([
                     'HTTPS' => $https,
                     'HTTP_HOST' => $host,

                 ] as $key => $value) {
            $this->spoofedServerVars[$key] = $_SERVER[$key] ?? null;
            $_SERVER[$key] = $value;
        }
    }

    protected function tearDown(): void
    {
        if (static::USES_DATABASE) {
            self::$db->executeSql('COMMIT');
//            self::$db->executeSql('ROLLBACK');
        }
        parent::tearDown();

        $this->unspoofServerVars();
    }

    private function unspoofServerVars(): void
    {
        foreach ($this->spoofedServerVars as $key => $value) {
            if ($value === null) {
                unset($_SERVER[$key]);
            } else {
                $_SERVER[$key] = $value;
            }

            unset($this->spoofedServerVars[$key]);
        }
    }

    protected function getAuthenticated(string $uri, array $headers = [], ?int $expectCode = 200, ?userEntity $user = null): array
    {
        return $this->executeAuthenticatedRequest(
            user: $user ?? $this->defaultAdminUser(),
            method: HttpMethod::GET,
            uri: $uri,
            headers: $headers,
            expectCode: $expectCode,
        );
    }

    protected function executeAuthenticatedRequest(
        userEntity $user,
        HttpMethod $method,
        string $uri,
        array $body = [],
        array $headers = [],
        ?int $expectCode = 200
    ): array {
        $request = $this->request($method, $uri, $body, $headers)
            ->withAttribute('user', $user->id)
            ->withAttribute('authenticated', true);

        $response = self::$application->handle($request);

        return $this->responseToData($response, $expectCode);
    }

    /**
     * @TODO Create user factory in io-user
     */
    private function defaultAdminUser(): userEntity
    {
        $user = new userEntity();
        $user->first_name = $this->faker->firstName();
        $user->last_name = $this->faker->lastName();
        $user->active = true;
        $user->email = $this->faker->email();
        $user->password = $this->faker->password();
        $user->salt = $this->faker->regexify('[A-Za-z0-9]{128}');
        $user->store();
        $user->addRole('administrator');

        return $user;
    }

    protected function post(string $uri, array $body = [], array $headers = [], ?int $expectCode = 200): array
    {
        return $this->executeRequest(
            method: HttpMethod::POST,
            uri: $uri,
            body: $body,
            headers: $headers,
            expectCode: $expectCode
        );
    }
}
