<?php

namespace App\Tests\Application;

use App\Entity\Access\Access;
use App\Entity\Person\Person;
use App\Entity\Public\FederationStructure;
use App\Enum\Organization\LegalEnum;
use App\Enum\Organization\PrincipalTypeEnum;
use App\Enum\Organization\SettingsProductEnum;
use App\Tests\Fixture\Factory\Access\AccessFactory;
use App\Tests\Fixture\Factory\Organization\OrganizationFactory;
use App\Tests\Fixture\Factory\Organization\SettingsFactory;
use App\Tests\Fixture\Factory\Person\PersonFactory;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DomCrawler\Crawler;
use ApiPlatform\Symfony\Bundle\Test\Client;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Zenstruck\Foundry\Proxy;

/**
 * Base class for applicative tests
 */
abstract class OtWebTestCase extends ApiTestCase
{
    protected EntityManagerInterface $em;
    protected Client $client;
    protected Access | Proxy | null $user = null;
    protected ?string $securityToken = null;

    /**
     * Executed before each test
     *
     * @return void
     * @throws \Exception
     */
    public function setup(): void {

        // Boot le kernel symfony et récupère l'entity manager
        // @see https://symfony.com/doc/current/testing.html#retrieving-services-in-the-test
        self::bootKernel();
        $this->em = static::getContainer()->get(EntityManagerInterface::class);

        // Purge DB before populating new fixtures
        $this->purgeDb();

        // Définit les fixtures et flush
        $this->loadFixture();
        $this->em->flush();

        // Instancie le client qui exécutera les requêtes à l'api
        // @see https://symfony.com/doc/current/testing.html#making-requests
        $this->client = static::createClient();
    }

    /**
     * Delete all DB records before populating fixtures.
     *
     * @return void
     * @throws \Doctrine\DBAL\Exception
     */
    private function purgeDb() {
        if (!preg_match('/.*test.*/', $this->em->getConnection()->getDatabase())) {
            throw new \RuntimeException("The DB name shall contain 'test' in its name to be purge");
        }

        $this->em->getConnection()->exec('SET FOREIGN_KEY_CHECKS = 0;');
        $purger = new ORMPurger($this->em);
        $purger->setPurgeMode(ORMPurger::PURGE_MODE_DELETE);
        $purger->purge();
        $this->em->getConnection()->exec('SET FOREIGN_KEY_CHECKS = 1;');
    }

    /**
     * Create and persist the fixtures (do not flush)
     *
     * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#same-entities-used-in-these-docs
     *
     * @return void
     */
    protected function loadFixture(): void {
        $person = PersonFactory::createOne(
            [
                'username' => 'username',
                'password' => 'password'
            ]
        );

        $organization = OrganizationFactory::createOne([
            'legalStatus' => LegalEnum::ASSOCIATION_LAW_1901()->getValue(),
            'principalType' => PrincipalTypeEnum::ARTISTIC_EDUCATION_ONLY()->getValue(),
            'name' => 'My Organization'
        ]);

        SettingsFactory::createOne([
            'product' => SettingsProductEnum::ARTIST(),
            'organization' => $organization
        ]);

        $this->user = AccessFactory::createOne([
            'person' => $person,
            'organization' => $organization,
            'roles' => ['ROLE_USERS_VIEW']
        ]);
    }

    /**
     * Send a requests, parse the hydra response and return an object or a Collection
     *
     * @param string $method
     * @param string $route
     * @param array<mixed> $data
     * @param array<mixed> $headers
     * @return ResponseInterface
     */
    protected function request(string $method, string $route, array | null $data = null, array $headers = []): ResponseInterface {
        if ($this->user) {
            $headers = array_merge(
                ['x-accessid' => $this->user->getId(), 'authorization' => 'BEARER ' . $this->securityToken],
                $headers
            );
        }

        $parameters = ['headers' => $headers];
        if ($data) {
            $parameters['json'] = $data;
        }

        return $this->client->request(
            $method,
            $route,
            $parameters
        );
    }

    /**
     * Send a GET request and return the response parsed content
     *
     * @param string $route
     * @param array<mixed> $headers
     * @return ResponseInterface
     */
    protected function get(string $route, array $headers = []): ResponseInterface {
        return $this->request(
            Request::METHOD_GET,
            $route,
            null,
            $headers
        );
    }

    /**
     * Send a PUT request and return the response parsed content
     *
     * @param string $route
     * @param array<mixed> $data
     * @param array<mixed> $headers
     * @return ResponseInterface
     */
    protected function put(string $route, array $data, array $headers = []): ResponseInterface {
        return $this->request(
            Request::METHOD_PUT,
            $route,
            $data,
            $headers
        );
    }

    /**
     * Send a POST request and return the response parsed content
     *
     * @param string $route
     * @param array<mixed> $data
     * @param array<mixed> $headers
     * @return ResponseInterface
     */
    protected function post(string $route, array $data, array $headers = []): ResponseInterface {
        return $this->request(
            Request::METHOD_POST,
            $route,
            $data,
            $headers
        );
    }

    /**
     * Send a DELETE request and return the response parsed content
     *
     * @param string $route
     * @param array<mixed> $headers
     * @return ResponseInterface
     */
    protected function delete(string $route, array $headers = []): ResponseInterface {
        return $this->request(
            Request::METHOD_DELETE,
            $route,
            null,
            $headers
        );
    }

    /**
     * Login as the given Access user
     *
     * @param Proxy|Access $access
     * @return void
     * @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
     * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
     * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
     * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
     */
    protected function loginAs(Proxy | Access $access): void {
        $person = $access->getPerson();

        $response = $this->post(
            '/login_check',
            ['username' => $person->getUsername(), 'password' => $person->getPassword()]
        );
        $content = $response->getContent();

        $this->securityToken = json_decode($content)->token;
        $this->user = $access;
    }

    /**
     * Assert that the response has the expected status code and is well formated
     *
     * @param string $resourceClass
     * @param int $expectedStatus
     * @return void
     */
    protected function validateCollectionSchema(string $resourceClass, int $expectedStatus = 200): void {
        $this->assertResponseStatusCodeSame($expectedStatus);

        if ($expectedStatus == 200) {
            $this->assertResponseIsSuccessful();
        }

        // Asserts that the returned content type is JSON-LD (the default)
        $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');

        // Asserts that the returned JSON is validated by the JSON Schema generated for this resource by API Platform
        // >>> Issue with the json typed PublicStructure::addresses properties
        // $this->assertMatchesResourceCollectionJsonSchema($resourceClass);
    }
}
