<?php

namespace NoahVet\Reef\Infrastructure\Phpunit\Builder;

use NoahVet\Reef\Contract\Phpunit\EndpointTestBuilderInterface;
use NoahVet\Reef\Contract\Phpunit\ExampleLoaderInterface;
use NoahVet\Reef\Contract\Phpunit\MultipartBody;
use NoahVet\Reef\Contract\Phpunit\RawBodyInterface;
use NoahVet\Reef\Infrastructure\Phpunit\Response\EndpointTestResponse;
use NoahVet\Reef\Phpunit\Fixture\FixtureRepository;
use NoahVet\Reef\Phpunit\Fixture\PermissionFixture;
use NoahVet\Reef\Phpunit\Fixture\ReefTokenFixture;
use NoahVet\Reef\Security\User\ReefOAuthUser;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class EndpointTestBuilder implements EndpointTestBuilderInterface
{
    private ?string $basePath = null;

    private ?string $requestFilePath = null;

    private ?string $requestComponentName = null;

    private ?string $responseFilePath = null;

    private ?string $responseComponentName = null;

    /** @var PermissionFixture[] */
    private array $permissions = [];

    private bool $superAdmin = false;

    /** @var list<string> */
    private array $blacklist = [
        'id', 'createdDate', 'updatedDate', 'startDate', 'endDate',
        'entryDate', 'exitDate', 'activatedDate',
    ];

    /** @var array<string, string> Server variable headers, e.g. ['HTTP_ACCEPT_LANGUAGE' => 'fr_FR'] */
    private array $headers = [];

    private ?string $multipartType = null;

    private ?string $principalId = null;

    public function __construct(
        private readonly KernelBrowser $client,
        private readonly string $method,
        private readonly string $path,
        private readonly FixtureRepository $fixtureRepository,
        private readonly ExampleLoaderInterface $loader,
    ) {
    }

    #[\Override]
    public function withBasePath(string $basePath): static
    {
        $this->basePath = \rtrim($basePath, '/');

        return $this;
    }

    #[\Override]
    public function withRequestExample(string $filePath, string $componentName): static
    {
        $this->requestFilePath = $filePath;
        $this->requestComponentName = $componentName;

        return $this;
    }

    #[\Override]
    public function withResponseExample(string $filePath, string $componentName): static
    {
        $this->responseFilePath = $filePath;
        $this->responseComponentName = $componentName;

        return $this;
    }

    /**
     * @param PermissionFixture[] $permissions
     */
    #[\Override]
    public function withPermissions(array $permissions): static
    {
        $this->permissions = $permissions;

        return $this;
    }

    #[\Override]
    public function withSuperAdminPermissions(): static
    {
        $this->superAdmin = true;

        return $this;
    }

    /** @param list<string> $fields */
    #[\Override]
    public function withBlacklist(array $fields): static
    {
        $this->blacklist = $fields;

        return $this;
    }

    /**
     * @param array<string, string> $headers Server variable headers (e.g. HTTP_ACCEPT_LANGUAGE)
     */
    #[\Override]
    public function withHeaders(array $headers): static
    {
        $this->headers = $headers;

        return $this;
    }

    #[\Override]
    public function withMultipartType(string $subtype): static
    {
        $this->multipartType = $subtype;

        return $this;
    }

    #[\Override]
    public function withPrincipalId(string $uuid): static
    {
        $this->principalId = $uuid;

        return $this;
    }

    #[\Override]
    public function execute(): EndpointTestResponse
    {
        $bearer = \base64_encode(\random_bytes(32));

        $this->fixtureRepository->addFixture(
            new ReefTokenFixture($bearer, new ReefOAuthUser($this->principalId ?? '622fbb01-98ac-4957-9b72-73132bdb5954', null), $this->permissions, $this->superAdmin),
        );

        $requestBody = null;
        if (null !== $this->requestFilePath && null !== $this->requestComponentName) {
            $requestBody = $this->loader->load($this->resolvePath($this->requestFilePath), $this->requestComponentName);
        }

        $expectedBody = null;
        if (null !== $this->responseFilePath && null !== $this->responseComponentName) {
            $loaded = $this->loader->load($this->resolvePath($this->responseFilePath), $this->responseComponentName);
            if ($loaded instanceof RawBodyInterface) {
                throw new \LogicException('Multipart examples cannot be used as response examples for body comparison.');
            }
            $expectedBody = $loaded;
        }

        $serverHeaders = \array_merge(
            ['HTTP_AUTHORIZATION' => 'Bearer '.$bearer],
            $this->headers,
        );

        if ($requestBody instanceof MultipartBody && null !== $this->multipartType) {
            $requestBody = new MultipartBody($requestBody->boundary, $requestBody->rawBody, $this->multipartType);
        }

        $parameters = [];
        $files = [];
        $content = null;

        if ($requestBody instanceof MultipartBody && 'form-data' === $requestBody->subtype) {
            $serverHeaders['CONTENT_TYPE'] = $requestBody->contentType();
            $content = $requestBody->getRawBody();
            $parameters = $requestBody->getParameters();
            foreach ($requestBody->getFileParts() as $part) {
                $tmp = \tempnam(\sys_get_temp_dir(), 'reef_upload_');
                if (false === $tmp) {
                    throw new \RuntimeException('Failed to create temp file for multipart upload.');
                }
                \file_put_contents($tmp, $part['content']);
                $files[$part['name']] = new UploadedFile($tmp, $part['filename'], $part['mimeType'], null, true);
            }
        } elseif ($requestBody instanceof RawBodyInterface) {
            $serverHeaders['CONTENT_TYPE'] = $requestBody->contentType();
            $content = $requestBody->getRawBody();
        } else {
            $content = null !== $requestBody ? \json_encode($requestBody, \JSON_THROW_ON_ERROR) : null;
        }

        $this->client->request(
            $this->method,
            $this->path,
            $parameters,
            $files,
            $serverHeaders,
            $content,
        );

        return new EndpointTestResponse(
            $this->client->getResponse(),
            $expectedBody,
            $this->blacklist,
        );
    }

    private function resolvePath(string $filePath): string
    {
        return null !== $this->basePath ? $this->basePath.'/'.$filePath : $filePath;
    }
}
