<?php

namespace SwaggerBake\Test\TestCase\Lib\Operation;

use Cake\Routing\RouteBuilder;
use Cake\Routing\Router;
use Cake\TestSuite\PHPUnitConsecutiveTrait;
use Cake\TestSuite\TestCase;
use SwaggerBake\Lib\Attribute\OpenApiPaginator;
use SwaggerBake\Lib\Attribute\OpenApiResponse;
use SwaggerBake\Lib\Configuration;
use SwaggerBake\Lib\SwaggerFactory;
use SwaggerBake\Lib\OpenApi\Operation;
use SwaggerBake\Lib\OpenApi\Response;
use SwaggerBake\Lib\OpenApi\Schema;
use SwaggerBake\Lib\Operation\OperationResponse;
use SwaggerBake\Lib\Route\RouteScanner;
use SwaggerBake\Lib\Swagger;
use SwaggerBake\Test\TestCase\Helper\ReflectionAttributeTrait;

class OperationResponseTest extends TestCase
{
    use ReflectionAttributeTrait;
    use PHPUnitConsecutiveTrait;

    /**
     * @var string[]
     */
    public array $fixtures = [
        'plugin.SwaggerBake.Employees',
        'plugin.SwaggerBake.DepartmentEmployees',
    ];

    private Configuration $config;

    private array $routes;

    public function setUp(): void
    {
        parent::setUp(); // TODO: Change the autogenerated stub
        Router::createRouteBuilder('/')->scope('/', function (RouteBuilder $builder) {
            $builder->setExtensions(['json']);
            $builder->resources('Employees', [
                'only' => [
                    'index',
                    'create',
                    'delete',
                    'noResponsesDefined',
                    'textPlain',
                    'options',
                    'list'
                ],
                'map' => [
                    'noResponsesDefined'  => [
                        'method' => 'get',
                        'action' => 'noResponseDefined',
                        'path' => 'no-responses-defined'
                    ],
                    'textPlain'  => [
                        'method' => 'get',
                        'action' => 'textPlain',
                        'path' => 'text-plain'
                    ],
                    'options' => [
                        'method' => ['options'],
                        'action' => 'options',
                        'path' => 'options'
                    ],
                    'list' => [
                        'method' => 'get', 
                        'action' => 'customGet', 
                        'path' => 'custom-get'
                    ]
                ]
            ]);
        });

        $this->config = new Configuration([
            'prefix' => '/',
            'yml' => '/config/swagger-with-generic-collection.yml',
            'json' => '/webroot/swagger.json',
            'webPath' => '/swagger.json',
            'hotReload' => false,
            'exceptionSchema' => 'Exception',
            'requestAccepts' => ['application/x-www-form-urlencoded'],
            'responseContentTypes' => ['application/json'],
            'namespaces' => [
                'controllers' => ['\SwaggerBakeTest\App\\'],
                'entities' => ['\SwaggerBakeTest\App\\'],
                'tables' => ['\SwaggerBakeTest\App\\'],
            ]
        ], SWAGGER_BAKE_TEST_APP);

        $cakeRoute = new RouteScanner(new Router(), $this->config);
        $this->routes = $cakeRoute->getRoutes();
    }

    public function test_get_operation_with_attribute_response(): void
    {
        $route = $this->routes['employees:index'];

        $mockReflectionMethod = $this->mockReflectionMethod(OpenApiResponse::class, [
            'statusCode' => '200',
        ]);

        $operationResponse = new OperationResponse(
            (new SwaggerFactory($this->config, new RouteScanner(new Router(), $this->config)))->create(),
            $this->config,
            new Operation('hello', 'get'),
            $route,
            null,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();

        $this->assertInstanceOf(Response::class, $operation->getResponseByCode('200'));
    }

    public function test_get_operation_with_schema_response(): void
    {
        $route = $this->routes['employees:add'];

        $schema = (new Schema())->setName('Employee')->setType('object');

        $mockReflectionMethod = $this->createPartialMock(\ReflectionMethod::class, ['getAttributes']);
        $mockReflectionMethod->expects($this->once())
            ->method(
                'getAttributes'
            )
            ->with(OpenApiResponse::class)
            ->will(
                $this->returnValue([])
            );

        $operationResponse = new OperationResponse(
            (new SwaggerFactory($this->config, new RouteScanner(new Router(), $this->config)))->create()->build(),
            $this->config,
            new Operation('employees:add', 'post'),
            $route,
            $schema,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();

        $this->assertInstanceOf(Response::class, $operation->getResponseByCode('200'));
    }

    public function test_add_operation_with_open_api_response_of_201(): void
    {
        $route = $this->routes['employees:add'];

        $mockReflectionMethod = $this->mockReflectionMethod(OpenApiResponse::class, [
            'statusCode' => '201',
        ]);

        $schema = (new Schema())->setName('Employee')->setType('object');

        $operationResponse = new OperationResponse(
            (new SwaggerFactory($this->config, new RouteScanner(new Router(), $this->config)))->create()->build(),
            $this->config,
            new Operation('employees:add', 'post'),
            $route,
            $schema,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();
        $response = $operation->getResponseByCode('201');
        $this->assertNotEmpty($response);

        $content = $response->getContentByMimeType('application/json');

        $this->assertNotEmpty($content);
        $this->assertNotEmpty($content->getSchema());
    }

    public function test_add_operation_with_no_response_defined(): void
    {
        $route = $this->routes['employees:add'];

        $mockReflectionMethod = $this->createPartialMock(\ReflectionMethod::class, ['getAttributes']);
        $mockReflectionMethod->expects($this->exactly(2))
            ->method('getAttributes')
            ->with(
                ...$this->withConsecutive(
                    [OpenApiResponse::class],
                    [OpenApiPaginator::class]
                )
            )
            ->willReturnOnConsecutiveCalls(
                [],
                []
            );

        $operationResponse = new OperationResponse(
            (new SwaggerFactory($this->config, new RouteScanner(new Router(), $this->config)))->create()->build(),
            $this->config,
            new Operation('employees:add', 'post'),
            $route,
            null,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();
        $response = $operation->getResponseByCode('200');
        $this->assertNotEmpty($response);

        $content = $response->getContentByMimeType('application/json');

        $this->assertNotEmpty($content);
        $this->assertNotEmpty($content->getSchema());
    }

    public function test_delete_action_response_with_http_204(): void
    {
        $route = $this->routes['employees:delete'];

        $mockReflectionMethod = $this->createPartialMock(\ReflectionMethod::class, ['getAttributes']);
        $mockReflectionMethod->expects($this->once())
            ->method(
                'getAttributes'
            )
            ->with(OpenApiResponse::class)
            ->will(
                $this->returnValue([])
            );

        $operationResponse = new OperationResponse(
            (new SwaggerFactory($this->config, new RouteScanner(new Router(), $this->config)))->create()->build(),
            $this->config,
            new Operation('employees:delete', 'delete'),
            $route,
            null,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();
        $this->assertNotEmpty($operation->getResponseByCode('204'));
    }

    public function test_no_response_defined(): void
    {
        $route = $this->routes['employees:noresponsedefined'];

        $mockReflectionMethod = $this->createPartialMock(\ReflectionMethod::class, ['getAttributes']);
        $mockReflectionMethod->expects($this->exactly(2))
            ->method('getAttributes')
            ->with(
                ...$this->withConsecutive(
                    [OpenApiResponse::class],
                    [OpenApiPaginator::class]
                )
            )
            ->willReturnOnConsecutiveCalls(
                [],
                []
            );

        $operationResponse = new OperationResponse(
            $this->mockSwagger('getSchemaByName', null, null),
            $this->config,
            new Operation('employees:noresponsedefined', 'get'),
            $route,
            null,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();
        $response = $operation->getResponseByCode('200');
        $this->assertNotEmpty($response);

        $content = $response->getContentByMimeType('application/json');
        $this->assertNotEmpty($content);
        $this->assertNotEmpty($content->getSchema());
    }

    public function test_get_operation_with_openapi_response_ref(): void
    {
        $route = $this->routes['employees:index'];

        $mockReflectionMethod = $this->mockReflectionMethod(OpenApiResponse::class, [
            'schemaType' => 'array',
            'ref' => '#/components/schema/Employee',
        ]);

        $operationResponse = new OperationResponse(
            $this->mockSwagger('getSchemaByName', 'Employee'),
            $this->config,
            new Operation('employees:index', 'get'),
            $route,
            null,
            $mockReflectionMethod
        );

        /** @var Schema $schema */
        $schema = $operationResponse
            ->getOperationWithResponses()
            ->getResponseByCode('200')
            ->getContentByMimeType('application/json')
            ->getSchema();

        $this->assertEquals('array', $schema->getType());
        $this->assertEquals('#/components/schema/Employee', $schema->getItems()['$ref']);
    }

    public function test_get_operation_with_openapi_response_text_plain(): void
    {
        $route = $this->routes['employees:textplain'];

        $mockReflectionMethod = $this->mockReflectionMethod(OpenApiResponse::class, [
            'mimeTypes' => ['text/plain'],
            'schemaFormat' => 'date-time',
        ]);

        $operationResponse = new OperationResponse(
            $this->mockSwagger('getSchemaByName', null, null),
            $this->config,
            new Operation('employees:textplain', 'get'),
            $route,
            null,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();

        $content = $operation->getResponseByCode('200')->getContentByMimeType('text/plain');
        /** @var Schema $schema */
        $schema = $content->getSchema();

        $this->assertEquals('string', $schema->getType());
        $this->assertEquals('date-time', $schema->getFormat());
    }

    /**
     * @link https://github.com/cnizzardini/cakephp-swagger-bake/issues/363
     */
    public function test_associations(): void
    {
        $route = $this->routes['employees:index'];

        $mockReflectionMethod = $this->mockReflectionMethod(OpenApiResponse::class, [
            'associations' => ['whiteList' => ['DepartmentEmployees']]
        ]);

        $swagger = (new SwaggerFactory($this->config, new RouteScanner(new Router(), $this->config)))
            ->create()
            ->build();

        $operationResponse = new OperationResponse(
            $swagger,
            $this->config,
            new Operation('hello', 'get'),
            $route,
            null,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();
        $content = $operation->getResponseByCode('200')->getContentByMimeType('application/json');
        $this->assertArrayHasKey('department_employees', $content->getSchema()->getProperties());

        // issue #363 association schema should only modify the operations' response schema and not the main schema.
        $schema = $swagger->getSchemaByName('Employee');
        $this->assertArrayNotHasKey('department_employees', $schema->getProperties());
    }

    public function test_text_plain_mime_type(): void
    {
        $route = $this->routes['employees:index'];

        $mockReflectionMethod = $this->mockReflectionMethod(OpenApiResponse::class, [
            'mimeTypes' => ['text/plain']
        ]);

        $operationResponse = new OperationResponse(
            $this->mockSwagger('getSchemaByName', 'Employee'),
            $this->config,
            new Operation('hello', 'get'),
            $route,
            null,
            $mockReflectionMethod
        );

        $content = $operationResponse
            ->getOperationWithResponses()
            ->getResponseByCode('200')
            ->getContentByMimeType('text/plain');

        $this->assertEquals('text/plain', $content->getMimeType());
    }

    public function test_http_options(): void
    {
        $route = $this->routes['employees:options'];

        $operationResponse = new OperationResponse(
            $this->mockSwagger(),
            $this->config,
            new Operation('hello', 'options'),
            $route,
            null,
            null
        );

        $this->assertNotEmpty($operationResponse->getOperationWithResponses()->getResponseByCode('200'));
    }

    public function test_custom_get_method_with_collection_response(): void
    {
        $route = $this->routes['employees:customget'];

        $mockReflectionMethod = $this->mockReflectionMethod(OpenApiResponse::class, [
            'statusCode' => '200',
            'schemaType' => 'array',
            'ref' => '#/components/schema/Employee',
        ]);

        $operationResponse = new OperationResponse(
            $this->mockSwagger('getSchemaByName', 'Employee'),
            $this->config,
            new Operation('employees:customget', 'get'),
            $route,
            null,
            $mockReflectionMethod
        );

        $operation = $operationResponse->getOperationWithResponses();
        $response = $operation->getResponseByCode('200');
        $this->assertNotEmpty($response);

        $content = $response->getContentByMimeType('application/json');
        $this->assertNotEmpty($content);
        $this->assertNotEmpty($content->getSchema());

        /** @var Schema $schema */
        $schema = $content->getSchema();
        $this->assertEquals('array', $schema->getType());
        $this->assertEquals('#/components/schema/Employee', $schema->getItems()['$ref']);
    }

    /**
     * Builds a partial mock of Swagger.
     *
     * @param string $method The method to mock.
     * @param mixed $withArg Defaults to anything if let null,
     * @param mixed $willReturn Defaults to null.
     * @return Swagger
     */
    private function mockSwagger(?string $method = null , mixed $withArg = null, mixed $willReturn = null): Swagger
    {
        if ($method == null) {
            return $this->createPartialMock(Swagger::class, []);
        }

        $mockSwagger = $this->createPartialMock(Swagger::class, [$method]);
        $mockSwagger->expects($this->any())
            ->method(
                $method
            )
            ->with($withArg ?? $this->anything())
            ->will(
                $this->returnValue($willReturn)
            );

        return $mockSwagger;
    }
}