<?php

namespace NoahVet\Reef\Test\A_Unit\Infrastructure\Phpunit\Builder;

use NoahVet\Reef\Contract\Phpunit\ExampleLoaderInterface;
use NoahVet\Reef\Contract\Phpunit\MultipartBody;
use NoahVet\Reef\Infrastructure\Phpunit\Builder\EndpointTestBuilder;
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\IAM\Model\Resource;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;

class EndpointTestBuilderTest extends TestCase
{
    /** @var KernelBrowser&MockObject */
    private MockObject $client;

    /** @var FixtureRepository&MockObject */
    private MockObject $fixtureRepository;

    /** @var ExampleLoaderInterface&MockObject */
    private MockObject $loader;

    protected function setUp(): void
    {
        $this->client = $this->createMock(KernelBrowser::class);
        $this->fixtureRepository = $this->createMock(FixtureRepository::class);
        $this->loader = $this->createMock(ExampleLoaderInterface::class);
    }

    public function testExecuteRegistersFixtureAndMakesRequest(): void
    {
        $this->fixtureRepository
            ->expects($this->once())
            ->method('addFixture')
            ->with($this->isInstanceOf(ReefTokenFixture::class))
            ->willReturnSelf()
        ;

        $this->client
            ->expects($this->once())
            ->method('request')
            ->with(
                'POST',
                '/workspaces',
                [],
                [],
                $this->callback(static fn (array $headers) => \str_starts_with($headers['HTTP_AUTHORIZATION'] ?? '', 'Bearer ')),
                null,
            )
        ;
        $this->client->method('getResponse')->willReturn(new Response('{}', 201));

        $result = $this->makeBuilder('POST', '/workspaces')->execute();

        self::assertInstanceOf(EndpointTestResponse::class, $result);
    }

    public function testExecuteLoadsRequestBodyFromOasFile(): void
    {
        $requestBody = ['name' => 'My workspace'];

        $this->loader
            ->expects($this->once())
            ->method('load')
            ->with('/path/to/request.yaml', 'MyRequestExample')
            ->willReturn($requestBody)
        ;

        $this->fixtureRepository->method('addFixture')->willReturnSelf();
        $this->client
            ->expects($this->once())
            ->method('request')
            ->with('POST', '/workspaces', [], [], $this->anything(), \json_encode($requestBody))
        ;
        $this->client->method('getResponse')->willReturn(new Response('{}', 201));

        $this->makeBuilder('POST', '/workspaces')
            ->withRequestExample('/path/to/request.yaml', 'MyRequestExample')
            ->execute()
        ;
    }

    public function testExecuteLoadsExpectedResponseFromOasFile(): void
    {
        $expectedBody = ['id' => '123', 'name' => 'My workspace'];

        $this->loader
            ->method('load')
            ->with('/path/to/response.yaml', 'MyResponseExample')
            ->willReturn($expectedBody)
        ;

        $this->fixtureRepository->method('addFixture')->willReturnSelf();
        $this->client->method('request');
        $this->client->method('getResponse')
            ->willReturn(new Response(\json_encode($expectedBody, \JSON_THROW_ON_ERROR), 201))
        ;

        $response = $this->makeBuilder('POST', '/workspaces')
            ->withResponseExample('/path/to/response.yaml', 'MyResponseExample')
            ->withBlacklist(['id'])
            ->execute()
        ;

        $response->assertCreated()->assertMatchesExpectedBody();
    }

    public function testWithPermissionsPassesPermissionsToFixture(): void
    {
        $permission = new PermissionFixture(
            Resource::fromString('reef:test:resource', 'resource-1'),
            ['reef:test:resource:read'],
        );

        $this->fixtureRepository
            ->expects($this->once())
            ->method('addFixture')
            ->with($this->callback(static fn (ReefTokenFixture $fixture) => $fixture->getAllPermissions() === [$permission]))
            ->willReturnSelf()
        ;

        $this->client->method('request');
        $this->client->method('getResponse')->willReturn(new Response('{}', 200));

        $this->makeBuilder('GET', '/workspaces')
            ->withPermissions([$permission])
            ->execute()
        ;
    }

    public function testWithHeadersMergesIntoServerVars(): void
    {
        $this->fixtureRepository->method('addFixture')->willReturnSelf();

        $this->client
            ->expects($this->once())
            ->method('request')
            ->with(
                'GET',
                '/workspaces',
                [],
                [],
                $this->callback(static fn (array $headers) => ($headers['HTTP_ACCEPT_LANGUAGE'] ?? null) === 'fr_FR'),
                null,
            )
        ;
        $this->client->method('getResponse')->willReturn(new Response('[]', 200));

        $this->makeBuilder('GET', '/workspaces')
            ->withHeaders(['HTTP_ACCEPT_LANGUAGE' => 'fr_FR'])
            ->execute()
        ;
    }

    public function testWithSuperAdminPermissionsSetsSuperAdminFlag(): void
    {
        $this->fixtureRepository
            ->expects($this->once())
            ->method('addFixture')
            ->with($this->callback(static fn (ReefTokenFixture $fixture) => true === $fixture->isSuperAdmin()))
            ->willReturnSelf()
        ;

        $this->client->method('request');
        $this->client->method('getResponse')->willReturn(new Response('{}', 200));

        $this->makeBuilder('GET', '/workspaces')
            ->withSuperAdminPermissions()
            ->execute()
        ;
    }

    public function testWithBasePathPrependsToRequestAndResponsePaths(): void
    {
        $loadCalls = 0;

        $this->loader
            ->expects($this->exactly(2))
            ->method('load')
            ->willReturnCallback(static function (string $path, string $example) use (&$loadCalls): array {
                ++$loadCalls;

                if (1 === $loadCalls) {
                    self::assertSame('doc/OpenAPI/Example/request.yaml', $path);
                    self::assertSame('MyRequest', $example);
                }

                if (2 === $loadCalls) {
                    self::assertSame('doc/OpenAPI/Example/response.yaml', $path);
                    self::assertSame('MyResponse', $example);
                }

                return [];
            })
        ;

        $this->fixtureRepository->method('addFixture')->willReturnSelf();
        $this->client->method('request');
        $this->client->method('getResponse')->willReturn(new Response('[]', 200));

        $this->makeBuilder('POST', '/workspaces')
            ->withBasePath('doc/OpenAPI/Example')
            ->withRequestExample('request.yaml', 'MyRequest')
            ->withResponseExample('response.yaml', 'MyResponse')
            ->execute()
        ;
    }

    public function testExecuteSendsMultipartBodyWithCorrectContentType(): void
    {
        $rawBody = "--reef-boundary\r\nContent-Type: application/json\r\n\r\n{}\r\n--reef-boundary--";
        $multipartBody = new MultipartBody('reef-boundary', $rawBody);

        $this->loader
            ->expects($this->once())
            ->method('load')
            ->willReturn($multipartBody)
        ;

        $this->fixtureRepository->method('addFixture')->willReturnSelf();
        $this->client
            ->expects($this->once())
            ->method('request')
            ->with(
                'POST',
                '/documents',
                [],
                [],
                $this->callback(static fn (array $headers) => ($headers['CONTENT_TYPE'] ?? null) === 'multipart/related; boundary=reef-boundary'),
                $rawBody,
            )
        ;
        $this->client->method('getResponse')->willReturn(new Response('{}', 201));

        $this->makeBuilder('POST', '/documents')
            ->withRequestExample('/path/to/doc.yaml', 'WithMaximumDataRequestExample')
            ->execute()
        ;
    }

    public function testWithMultipartTypeOverridesSubtype(): void
    {
        // name="file" without filename → treated as scalar parameter, not UploadedFile
        $rawBody = "--reef-boundary\r\nContent-Disposition: form-data; name=\"file\"\r\n\r\nbinary\r\n--reef-boundary--";
        $multipartBody = new MultipartBody('reef-boundary', $rawBody);

        $this->loader
            ->expects($this->once())
            ->method('load')
            ->willReturn($multipartBody)
        ;

        $this->fixtureRepository->method('addFixture')->willReturnSelf();
        $this->client
            ->expects($this->once())
            ->method('request')
            ->with(
                'POST',
                '/uploads',
                ['file' => 'binary'],
                [],
                $this->callback(static fn (array $headers) => ($headers['CONTENT_TYPE'] ?? null) === 'multipart/form-data; boundary=reef-boundary'),
                $rawBody,
            )
        ;
        $this->client->method('getResponse')->willReturn(new Response('{}', 201));

        $this->makeBuilder('POST', '/uploads')
            ->withMultipartType('form-data')
            ->withRequestExample('/path/to/upload.yaml', 'UploadRequestExample')
            ->execute()
        ;
    }

    public function testWithMultipartTypeHasPriorityOverOasAnnotation(): void
    {
        // Part has no Content-Disposition → name is null → skipped; parameters and files remain empty
        $rawBody = "--reef-boundary\r\n\r\nbinary\r\n--reef-boundary--";
        $multipartBody = new MultipartBody('reef-boundary', $rawBody, 'mixed');

        $this->loader
            ->expects($this->once())
            ->method('load')
            ->willReturn($multipartBody)
        ;

        $this->fixtureRepository->method('addFixture')->willReturnSelf();
        $this->client
            ->expects($this->once())
            ->method('request')
            ->with(
                'POST',
                '/uploads',
                [],
                [],
                $this->callback(static fn (array $headers) => ($headers['CONTENT_TYPE'] ?? null) === 'multipart/form-data; boundary=reef-boundary'),
                $rawBody,
            )
        ;
        $this->client->method('getResponse')->willReturn(new Response('{}', 201));

        $this->makeBuilder('POST', '/uploads')
            ->withMultipartType('form-data')
            ->withRequestExample('/path/to/upload.yaml', 'UploadRequestExample')
            ->execute()
        ;
    }

    public function testExecuteFormDataWithFilePartPassesUploadedFile(): void
    {
        $rawBody = "--reef-boundary\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"avatar.jpg\"\r\nContent-Type: image/jpeg\r\n\r\nJPEGDATA\r\n--reef-boundary--";
        $multipartBody = new MultipartBody('reef-boundary', $rawBody, 'form-data');

        $this->loader->expects($this->once())->method('load')->willReturn($multipartBody);
        $this->fixtureRepository->method('addFixture')->willReturnSelf();

        $this->client
            ->expects($this->once())
            ->method('request')
            ->with(
                'POST',
                '/uploads',
                [],
                $this->callback(static fn (array $files) => isset($files['photo']) && $files['photo'] instanceof UploadedFile
                    && 'avatar.jpg' === $files['photo']->getClientOriginalName()
                    && 'image/jpeg' === $files['photo']->getClientMimeType()),
                $this->callback(static fn (array $headers) => ($headers['CONTENT_TYPE'] ?? null) === 'multipart/form-data; boundary=reef-boundary'),
                $rawBody,
            )
        ;
        $this->client->method('getResponse')->willReturn(new Response('{}', 201));

        $this->makeBuilder('POST', '/uploads')
            ->withRequestExample('/path/to/upload.yaml', 'UploadRequestExample')
            ->execute()
        ;
    }

    public function testExecuteFormDataWithScalarFieldPassesParameter(): void
    {
        $rawBody = "--reef-boundary\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\nHello World\r\n--reef-boundary--";
        $multipartBody = new MultipartBody('reef-boundary', $rawBody, 'form-data');

        $this->loader->expects($this->once())->method('load')->willReturn($multipartBody);
        $this->fixtureRepository->method('addFixture')->willReturnSelf();

        $this->client
            ->expects($this->once())
            ->method('request')
            ->with(
                'POST',
                '/uploads',
                ['title' => 'Hello World'],
                [],
                $this->callback(static fn (array $headers) => ($headers['CONTENT_TYPE'] ?? null) === 'multipart/form-data; boundary=reef-boundary'),
                $rawBody,
            )
        ;
        $this->client->method('getResponse')->willReturn(new Response('{}', 201));

        $this->makeBuilder('POST', '/uploads')
            ->withRequestExample('/path/to/upload.yaml', 'UploadRequestExample')
            ->execute()
        ;
    }

    private function makeBuilder(string $method, string $path): EndpointTestBuilder
    {
        return new EndpointTestBuilder(
            $this->client,
            $method,
            $path,
            $this->fixtureRepository,
            $this->loader,
        );
    }
}
