<?php

namespace Illuminate\Tests\Integration\Http;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Exceptions\PostTooLargeException;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\ConditionallyLoadsAttributes;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\MergeValue;
use Illuminate\Http\Resources\MissingValue;
use Illuminate\Pagination\Cursor;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Route;
use Illuminate\Tests\Integration\Http\Fixtures\Author;
use Illuminate\Tests\Integration\Http\Fixtures\AuthorResourceWithOptionalRelationship;
use Illuminate\Tests\Integration\Http\Fixtures\EmptyPostCollectionResource;
use Illuminate\Tests\Integration\Http\Fixtures\ObjectResource;
use Illuminate\Tests\Integration\Http\Fixtures\Post;
use Illuminate\Tests\Integration\Http\Fixtures\PostCollectionResource;
use Illuminate\Tests\Integration\Http\Fixtures\PostCollectionResourceWithPaginationInformation;
use Illuminate\Tests\Integration\Http\Fixtures\PostModelCollectionResource;
use Illuminate\Tests\Integration\Http\Fixtures\PostResource;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithAnonymousResourceCollectionWithPaginationInformation;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithExtraData;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithJsonOptions;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithJsonOptionsAndTypeHints;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalAppendedAttributes;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalAttributes;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalData;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalHasAttributes;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalMerging;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalPivotRelationship;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalRelationship;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalRelationshipAggregates;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalRelationshipCounts;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalRelationshipExists;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalRelationshipUsingNamedParameters;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithoutWrap;
use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithUnlessOptionalData;
use Illuminate\Tests\Integration\Http\Fixtures\ReallyEmptyPostResource;
use Illuminate\Tests\Integration\Http\Fixtures\ResourceWithPreservedKeys;
use Illuminate\Tests\Integration\Http\Fixtures\SerializablePostResource;
use Illuminate\Tests\Integration\Http\Fixtures\Subscription;
use LogicException;
use Orchestra\Testbench\TestCase;

class ResourceTest extends TestCase
{
    public function testResourcesMayBeConvertedToJson()
    {
        Route::get('/', function () {
            return new PostResource(new Post([
                'id' => 5,
                'title' => 'Test Title',
                'abstract' => 'Test abstract',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
                'title' => 'Test Title',
            ],
        ]);
    }

    public function testResourcesMayBeConvertedToJsonWithToJsonMethod()
    {
        $resource = new PostResource(new Post([
            'id' => 5,
            'title' => 'Test Title',
            'abstract' => 'Test abstract',
        ]));

        $this->assertSame('{"id":5,"title":"Test Title","custom":true}', $resource->toJson());
    }

    public function testAnObjectsMayBeConvertedToJson()
    {
        Route::get('/', function () {
            return ObjectResource::make(
                (object) ['first_name' => 'Bob', 'age' => 40]
            );
        });

        $this->withoutExceptionHandling()
            ->get('/', ['Accept' => 'application/json'])
            ->assertStatus(200)
            ->assertExactJson([
                'data' => [
                    'name' => 'Bob',
                    'age' => 40,
                ],
            ]);
    }

    public function testArraysWithObjectsMayBeConvertedToJson()
    {
        Route::get('/', function () {
            $objects = [
                (object) ['first_name' => 'Bob', 'age' => 40],
                (object) ['first_name' => 'Jack', 'age' => 25],
            ];

            return ObjectResource::collection($objects);
        });

        $this->withoutExceptionHandling()
            ->get('/', ['Accept' => 'application/json'])
            ->assertStatus(200)
            ->assertExactJson([
                'data' => [
                    ['name' => 'Bob', 'age' => 40],
                    ['name' => 'Jack', 'age' => 25],
                ],
            ]);
    }

    public function testResourcesMayHaveNoWrap()
    {
        Route::get('/', function () {
            return new PostResourceWithoutWrap(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertJson([
            'id' => 5,
            'title' => 'Test Title',
        ]);
    }

    public function testResourcesMayHaveOptionalValues()
    {
        Route::get('/', function () {
            return new PostResourceWithOptionalData(new Post([
                'id' => 5,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
                'second' => 'value',
                'third' => 'value',
                'fourth' => 'default',
                'fifth' => 'default',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalValuesUsingUnless()
    {
        Route::get('/', function () {
            return new PostResourceWithUnlessOptionalData(new Post([
                'id' => 5,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/',
            ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
                'first' => 'value',
                'fourth' => 'value',
                'fifth' => 'value',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalSelectedAttributes()
    {
        Route::get('/', function () {
            return new PostResourceWithOptionalAttributes(new Post([
                'id' => 5,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
                'title' => 'no title',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalHasAttributes()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'is_published' => true,
            ]);

            return new PostResourceWithOptionalHasAttributes($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/',
            ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
                'first' => true,
                'second' => 'override value',
                'third' => 'override value',
                'fourth' => true,
                'fifth' => true,
            ],
        ]);
    }

    public function testResourcesWithOptionalHasAttributesReturnDefaultValuesAndNotMissingValues()
    {
        Route::get('/', function () {
            return new PostResourceWithOptionalHasAttributes(new Post([
                'id' => 5,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/',
            ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'fourth' => 'default',
                'fifth' => 'default',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalAppendedAttributes()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
            ]);

            $post->append('is_published');

            return new PostResourceWithOptionalAppendedAttributes($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
                'first' => true,
                'second' => 'override value',
                'third' => 'override value',
                'fourth' => true,
                'fifth' => true,
            ],
        ]);
    }

    public function testResourcesWithOptionalAppendedAttributesReturnDefaultValuesAndNotMissingValues()
    {
        Route::get('/', function () {
            return new PostResourceWithOptionalAppendedAttributes(new Post([
                'id' => 5,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'fourth' => 'default',
                'fifth' => 'default',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalMerges()
    {
        Route::get('/', function () {
            return new PostResourceWithOptionalMerging(new Post([
                'id' => 5,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'second' => 'value',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalRelationships()
    {
        Route::get('/', function () {
            return new PostResourceWithOptionalRelationship(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalRelationshipCounts()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]);

            return new PostResourceWithOptionalRelationshipCounts($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'comments' => 'None',
            ],
        ]);
    }

    public function testResourcesMayLoadOptionalRelationshipCounts()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
                'authors_count' => 2,
                'comments_count' => 5,
                'favourited_posts_count' => 1,
            ]);

            return new PostResourceWithOptionalRelationshipCounts($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'authors' => 2,
                'favourite_posts' => 1,
                'comments' => '5 comments',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalRelationshipExists()
    {
        Route::get('/', function () {
            return new PostResourceWithOptionalRelationshipExists(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'has_favourited_posts' => 'No',
            ],
        ]);
    }

    public function testResourcesMayLoadOptionalRelationshipExists()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
                'authors_exists' => true,
                'favourited_posts_exists' => true,
                'comments_exists' => false,
            ]);

            return new PostResourceWithOptionalRelationshipExists($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'has_authors' => true,
                'has_favourited_posts' => 'Yes',
                'comment_exists' => false,
            ],
        ]);
    }

    public function testResourcesMayLoadOptionalRelationships()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]);

            $post->setRelation('author', new Author(['name' => 'jrrmartin']));

            return new PostResourceWithOptionalRelationship($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'author' => ['name' => 'jrrmartin'],
                'author_name' => 'jrrmartin',
            ],
        ]);
    }

    public function testResourcesMayLoadOptionalRelationshipAggregates()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
                'comments_avg_rating' => 3.8,
                'comments_min_rating' => 2,
                'comments_max_rating' => 5,
            ]);

            return new PostResourceWithOptionalRelationshipAggregates($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'title' => 'Test Title',
                'average_rating' => 3.8,
                'minimum_rating' => 2,
                'maximum_rating' => '5 ratings',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalRelationshipAggregates()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]);

            return new PostResourceWithOptionalRelationshipAggregates($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'title' => 'Test Title',
                'maximum_rating' => 'Default Value',
            ],
        ]);
    }

    public function testResourcesMayShowsNullForLoadedRelationshipWithValueNull()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]);

            $post->setRelation('author', null);

            return new PostResourceWithOptionalRelationship($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'author' => null,
                'author_name' => null,
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalRelationshipsWithDefaultValues()
    {
        Route::get('/', function () {
            return new AuthorResourceWithOptionalRelationship(new Author([
                'name' => 'jrrmartin',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'name' => 'jrrmartin',
                'posts_count' => 'not loaded',
                'latest_post_title' => 'not loaded',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalPivotRelationships()
    {
        Route::get('/', function () {
            $post = new Post(['id' => 5]);
            $post->setRelation('pivot', new Subscription);

            return new PostResourceWithOptionalPivotRelationship($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'subscription' => [
                    'foo' => 'bar',
                ],
            ],
        ]);
    }

    public function testResourceDoesNotThrowErrorWhenUsingEloquentStrictModeAndCheckingOptionalPivotRelationship()
    {
        Model::shouldBeStrict(true);

        Route::get('/', function () {
            $post = new Post(['id' => 5]);
            (function () {
                $this->exists = true;
                $this->wasRecentlyCreated = false;
            })->bindTo($post)();

            return new PostResourceWithOptionalPivotRelationship($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
            ],
        ]);
    }

    public function testWhenLoadedUsingNamedDefaultParameterOnMissingRelation()
    {
        Route::get('/', function () {
            $post = new Post(['id' => 1]);

            return new PostResourceWithOptionalRelationshipUsingNamedParameters($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 1,
                'author_defaulting_to_null' => null,
                'author_name' => 'Anonymous',
            ],
        ]);
    }

    public function testWhenLoadedUsingNamedDefaultParameterOnLoadedRelation()
    {
        Route::get('/', function () {
            $post = new Post(['id' => 1]);
            $post->setRelation('author', new Author(['name' => 'jrrmartin']));

            return new PostResourceWithOptionalRelationshipUsingNamedParameters($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 1,
                'author' => ['name' => 'jrrmartin'],
                'author_defaulting_to_null' => ['name' => 'jrrmartin'],
                'author_name' => 'jrrmartin',
            ],
        ]);
    }

    public function testResourcesMayHaveOptionalPivotRelationshipsWithCustomAccessor()
    {
        Route::get('/', function () {
            $post = new Post(['id' => 5]);
            $post->setRelation('accessor', new Subscription);

            return new PostResourceWithOptionalPivotRelationship($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertExactJson([
            'data' => [
                'id' => 5,
                'custom_subscription' => [
                    'foo' => 'bar',
                ],
            ],
        ]);
    }

    public function testResourceIsUrlRoutable()
    {
        $post = new PostResource(new Post([
            'id' => 5,
            'title' => 'Test Title',
        ]));

        $this->assertSame('http://localhost/post/5', url('/post', $post));
    }

    public function testNamedRoutesAreUrlRoutable()
    {
        $post = new PostResource(new Post([
            'id' => 5,
            'title' => 'Test Title',
        ]));

        Route::get('/post/{id}', function () use ($post) {
            return route('post.show', $post);
        })->name('post.show');

        $response = $this->withoutExceptionHandling()->get('/post/1');

        $this->assertSame('http://localhost/post/5', $response->original);
    }

    public function testResourcesMayBeSerializable()
    {
        Route::get('/', function () {
            return new SerializablePostResource(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
            ],
        ]);
    }

    public function testResourcesMayCustomizeResponses()
    {
        Route::get('/', function () {
            return new PostResource(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);
        $response->assertHeader('X-Resource', 'True');
    }

    public function testResourcesMayCustomizeExtraData()
    {
        Route::get('/', function () {
            return new PostResourceWithExtraData(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertJson([
            'data' => [
                'id' => 5,
                'title' => 'Test Title',
            ],
            'foo' => 'bar',
        ]);
    }

    public function testResourcesMayCustomizeExtraDataWhenBuildingResponse()
    {
        Route::get('/', function () {
            return (new PostResourceWithExtraData(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ])))->additional(['baz' => 'qux']);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertJson([
            'data' => [
                'id' => 5,
                'title' => 'Test Title',
            ],
            'foo' => 'bar',
            'baz' => 'qux',
        ]);
    }

    public function testResourcesMayCustomizeJsonOptions()
    {
        Route::get('/', function () {
            return new PostResourceWithJsonOptions(new Post([
                'id' => 5,
                'title' => 'Test Title',
                'reading_time' => 3.0,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $this->assertEquals(
            '{"data":{"id":5,"title":"Test Title","reading_time":3.0}}',
            $response->baseResponse->content()
        );
    }

    public function testCollectionResourcesMayCustomizeJsonOptions()
    {
        Route::get('/', function () {
            return PostResourceWithJsonOptions::collection(collect([
                new Post(['id' => 5, 'title' => 'Test Title', 'reading_time' => 3.0]),
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $this->assertEquals(
            '{"data":[{"id":5,"title":"Test Title","reading_time":3.0}]}',
            $response->baseResponse->content()
        );
    }

    public function testResourcesMayCustomizeJsonOptionsOnPaginatedResponse()
    {
        Route::get('/', function () {
            $paginator = new LengthAwarePaginator(
                collect([new Post(['id' => 5, 'title' => 'Test Title', 'reading_time' => 3.0])]),
                10, 15, 1
            );

            return PostResourceWithJsonOptions::collection($paginator);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $this->assertEquals(
            '{"data":[{"id":5,"title":"Test Title","reading_time":3.0}],"links":{"first":"\/?page=1","last":"\/?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"&laquo; Previous","page":null,"active":false},{"url":"\/?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Next &raquo;","page":null,"active":false}],"path":"\/","per_page":15,"to":1,"total":10}}',
            $response->baseResponse->content()
        );
    }

    public function testResourcesMayCustomizeJsonOptionsWithTypeHintedConstructor()
    {
        Route::get('/', function () {
            return new PostResourceWithJsonOptionsAndTypeHints(new Post([
                'id' => 5,
                'title' => 'Test Title',
                'reading_time' => 3.0,
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $this->assertEquals(
            '{"data":{"id":5,"title":"Test Title","reading_time":3.0}}',
            $response->baseResponse->content()
        );
    }

    public function testCustomHeadersMayBeSetOnResponses()
    {
        Route::get('/', function () {
            return (new PostResource(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ])))->response()->setStatusCode(202)->header('X-Custom', 'True');
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(202);
        $response->assertHeader('X-Custom', 'True');
    }

    public function testResourcesMayReceiveProperStatusCodeForFreshModels()
    {
        Route::get('/', function () {
            $post = new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]);

            $post->wasRecentlyCreated = true;

            return new PostResource($post);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(201);
    }

    public function testCollectionsAreNotDoubledWrapped()
    {
        Route::get('/', function () {
            return new PostCollectionResource(collect([new Post([
                'id' => 5,
                'title' => 'Test Title',
            ])]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                ],
            ],
        ]);
    }

    public function testPaginatorsReceiveLinks()
    {
        Route::get('/', function () {
            $paginator = new LengthAwarePaginator(
                collect([new Post(['id' => 5, 'title' => 'Test Title'])]),
                10, 15, 1
            );

            return new PostCollectionResource($paginator);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                ],
            ],
            'links' => [
                'first' => '/?page=1',
                'last' => '/?page=1',
                'prev' => null,
                'next' => null,
            ],
            'meta' => [
                'current_page' => 1,
                'from' => 1,
                'last_page' => 1,
                'path' => '/',
                'per_page' => 15,
                'to' => 1,
                'total' => 10,
            ],
        ]);
    }

    public function testPaginatorResourceCanPreserveQueryParameters()
    {
        Route::get('/', function () {
            $collection = collect([new Post(['id' => 2, 'title' => 'Laravel Nova'])]);
            $paginator = new LengthAwarePaginator(
                $collection, 3, 1, 2
            );

            return PostCollectionResource::make($paginator)->preserveQuery();
        });

        $response = $this->withoutExceptionHandling()->get(
            '/?framework=laravel&author=Otwell&page=2', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 2,
                    'title' => 'Laravel Nova',
                ],
            ],
            'links' => [
                'first' => '/?framework=laravel&author=Otwell&page=1',
                'last' => '/?framework=laravel&author=Otwell&page=3',
                'prev' => '/?framework=laravel&author=Otwell&page=1',
                'next' => '/?framework=laravel&author=Otwell&page=3',
            ],
            'meta' => [
                'current_page' => 2,
                'from' => 2,
                'last_page' => 3,
                'path' => '/',
                'per_page' => 1,
                'to' => 2,
                'total' => 3,
            ],
        ]);
    }

    public function testPaginatorResourceCanReceiveQueryParameters()
    {
        Route::get('/', function () {
            $collection = collect([new Post(['id' => 2, 'title' => 'Laravel Nova'])]);
            $paginator = new LengthAwarePaginator(
                $collection, 3, 1, 2
            );

            return PostCollectionResource::make($paginator)->withQuery(['author' => 'Taylor']);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/?framework=laravel&author=Otwell&page=2', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 2,
                    'title' => 'Laravel Nova',
                ],
            ],
            'links' => [
                'first' => '/?author=Taylor&page=1',
                'last' => '/?author=Taylor&page=3',
                'prev' => '/?author=Taylor&page=1',
                'next' => '/?author=Taylor&page=3',
            ],
            'meta' => [
                'current_page' => 2,
                'from' => 2,
                'last_page' => 3,
                'path' => '/',
                'per_page' => 1,
                'to' => 2,
                'total' => 3,
            ],
        ]);
    }

    public function testCursorPaginatorReceiveLinks()
    {
        Route::get('/', function () {
            $paginator = new CursorPaginator(
                collect([new Post(['id' => 5, 'title' => 'Test Title']), new Post(['id' => 6, 'title' => 'Hello'])]),
                1, null, ['parameters' => ['id']]
            );

            return new PostCollectionResource($paginator);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                ],
            ],
            'links' => [
                'first' => null,
                'last' => null,
                'prev' => null,
                'next' => '/?cursor='.(new Cursor(['id' => 5]))->encode(),
            ],
            'meta' => [
                'path' => '/',
                'per_page' => 1,
                'next_cursor' => (new Cursor(['id' => 5]))->encode(),
                'prev_cursor' => null,
            ],
        ]);
    }

    public function testCursorPaginatorResourceCanPreserveQueryParameters()
    {
        Route::get('/', function () {
            $collection = collect([new Post(['id' => 5, 'title' => 'Test Title']), new Post(['id' => 6, 'title' => 'Hello'])]);
            $paginator = new CursorPaginator(
                $collection, 1, null, ['parameters' => ['id']]
            );

            return PostCollectionResource::make($paginator)->preserveQuery();
        });

        $response = $this->withoutExceptionHandling()->get(
            '/?framework=laravel&author=Otwell', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                ],
            ],
            'links' => [
                'first' => null,
                'last' => null,
                'prev' => null,
                'next' => '/?framework=laravel&author=Otwell&cursor='.(new Cursor(['id' => 5]))->encode(),
            ],
            'meta' => [
                'path' => '/',
                'per_page' => 1,
            ],
        ]);
    }

    public function testCursorPaginatorResourceCanReceiveQueryParameters()
    {
        Route::get('/', function () {
            $collection = collect([new Post(['id' => 5, 'title' => 'Test Title']), new Post(['id' => 6, 'title' => 'Hello'])]);
            $paginator = new CursorPaginator(
                $collection, 1, null, ['parameters' => ['id']]
            );

            return PostCollectionResource::make($paginator)->withQuery(['author' => 'Taylor']);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/?framework=laravel&author=Otwell', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                ],
            ],
            'links' => [
                'first' => null,
                'last' => null,
                'prev' => null,
                'next' => '/?author=Taylor&cursor='.(new Cursor(['id' => 5]))->encode(),
            ],
            'meta' => [
                'path' => '/',
                'per_page' => 1,
            ],
        ]);
    }

    public function testToJsonMayBeLeftOffOfCollection()
    {
        Route::get('/', function () {
            return new EmptyPostCollectionResource(new LengthAwarePaginator(
                collect([new Post(['id' => 5, 'title' => 'Test Title'])]),
                10, 15, 1
            ));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                    'custom' => true,
                ],
            ],
            'links' => [
                'first' => '/?page=1',
                'last' => '/?page=1',
                'prev' => null,
                'next' => null,
            ],
            'meta' => [
                'current_page' => 1,
                'from' => 1,
                'last_page' => 1,
                'path' => '/',
                'per_page' => 15,
                'to' => 1,
                'total' => 10,
            ],
        ]);
    }

    public function testToJsonMayBeLeftOffOfSingleResource()
    {
        Route::get('/', function () {
            return new ReallyEmptyPostResource(new Post([
                'id' => 5,
                'title' => 'Test Title',
            ]));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                'id' => 5,
                'title' => 'Test Title',
            ],
        ]);
    }

    public function testOriginalOnResponseIsModelWhenSingleResource()
    {
        $createdPost = new Post(['id' => 5, 'title' => 'Test Title']);
        Route::get('/', function () use ($createdPost) {
            return new ReallyEmptyPostResource($createdPost);
        });
        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );
        $this->assertTrue($createdPost->is($response->getOriginalContent()));
    }

    public function testOriginalOnResponseIsCollectionOfModelWhenCollectionResource()
    {
        $createdPosts = collect([
            new Post(['id' => 5, 'title' => 'Test Title']),
            new Post(['id' => 6, 'title' => 'Test Title 2']),
        ]);
        Route::get('/', function () use ($createdPosts) {
            return new EmptyPostCollectionResource(new LengthAwarePaginator($createdPosts, 10, 15, 1));
        });
        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );
        $createdPosts->each(function ($post) use ($response) {
            $this->assertTrue($response->getOriginalContent()->contains($post));
        });
    }

    public function testCollectionResourceWithPaginationInformation()
    {
        $posts = collect([
            new Post(['id' => 5, 'title' => 'Test Title']),
        ]);

        Route::get('/', function () use ($posts) {
            return new PostCollectionResourceWithPaginationInformation(new LengthAwarePaginator($posts, 10, 1, 1));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/',
            ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                ],
            ],
            'current_page' => 1,
            'per_page' => 1,
            'total_page' => 10,
            'total' => 10,
        ]);
    }

    public function testResourceWithPaginationInformation()
    {
        $posts = collect([
            new Post(['id' => 5, 'title' => 'Test Title']),
        ]);

        Route::get('/', function () use ($posts) {
            return PostResourceWithAnonymousResourceCollectionWithPaginationInformation::collection(new LengthAwarePaginator($posts, 10, 1, 1));
        });

        $response = $this->withoutExceptionHandling()->get(
            '/',
            ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson([
            'data' => [
                [
                    'id' => 5,
                    'title' => 'Test Title',
                ],
            ],
            'current_page' => 1,
            'per_page' => 1,
            'total_page' => 10,
            'total' => 10,
        ]);
    }

    public function testCollectionResourcesAreCountable()
    {
        $posts = collect([
            new Post(['id' => 1, 'title' => 'Test title']),
            new Post(['id' => 2, 'title' => 'Test title 2']),
        ]);

        $collection = new PostCollectionResource($posts);

        $this->assertCount(2, $collection);
        $this->assertCount(2, $collection);
    }

    public function testCollectionResourcesMustCollectResources()
    {
        $posts = collect([
            new Post(['id' => 1, 'title' => 'Test title']),
            new Post(['id' => 2, 'title' => 'Test title 2']),
        ]);

        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('must collect');

        new PostModelCollectionResource($posts);
    }

    public function testKeysArePreservedIfTheResourceIsFlaggedToPreserveKeys()
    {
        $data = [
            'authorBook' => [
                'byId' => [
                    1 => [
                        'id' => 1,
                        'authorId' => 5,
                        'bookId' => 22,
                    ],
                    2 => [
                        'id' => 2,
                        'authorId' => 5,
                        'bookId' => 15,
                    ],
                    3 => [
                        'id' => 3,
                        'authorId' => 42,
                        'bookId' => 12,
                    ],
                ],
                'allIds' => [1, 2, 3],
            ],
        ];

        Route::get('/', function () use ($data) {
            return new ResourceWithPreservedKeys($data);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson(['data' => $data]);
    }

    public function testKeysArePreservedInAnAnonymousCollectionIfTheResourceIsFlaggedToPreserveKeys()
    {
        $data = Collection::make([
            [
                'id' => 1,
                'authorId' => 5,
                'bookId' => 22,
            ],
            [
                'id' => 2,
                'authorId' => 5,
                'bookId' => 15,
            ],
            [
                'id' => 3,
                'authorId' => 42,
                'bookId' => 12,
            ],
        ])->keyBy->id;

        Route::get('/', function () use ($data) {
            return ResourceWithPreservedKeys::collection($data);
        });

        $response = $this->withoutExceptionHandling()->get(
            '/', ['Accept' => 'application/json']
        );

        $response->assertStatus(200);

        $response->assertJson(['data' => $data->toArray()]);
    }

    public function testLeadingMergeKeyedValueIsMergedCorrectly()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    new MergeValue(['name' => 'mohamed', 'location' => 'hurghada']),
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'name' => 'mohamed', 'location' => 'hurghada',
        ], $results);
    }

    public function testPostTooLargeException()
    {
        $request = new Request(server: ['CONTENT_LENGTH' => '4']);
        $post = new ValidatePostSize;
        $post->handle($request, fn () => null);

        $this->expectException(PostTooLargeException::class);
        $this->expectExceptionMessage('The POST data is too large.');

        $request = new Request(server: ['CONTENT_LENGTH' => '2147483640']);
        $post = new ValidatePostSize;
        $post->handle($request, fn () => null);
    }

    public function testLeadingMergeKeyedValueIsMergedCorrectlyWhenFirstValueIsMissing()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    new MergeValue([
                        0 => new MissingValue,
                        'name' => 'mohamed',
                        'location' => 'hurghada',
                    ]),
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'name' => 'mohamed', 'location' => 'hurghada',
        ], $results);
    }

    public function testLeadingMergeValueIsMergedCorrectly()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    new MergeValue(['First', 'Second']),
                    'Taylor',
                    'Mohamed',
                    new MergeValue(['Adam', 'Matt']),
                    'Jeffrey',
                    new MergeValue(['Abigail', 'Lydia']),
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'First', 'Second', 'Taylor', 'Mohamed', 'Adam', 'Matt', 'Jeffrey', 'Abigail', 'Lydia',
        ], $results);
    }

    public function testMergeValuesMayBeMissing()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    new MergeValue(['First', 'Second']),
                    'Taylor',
                    'Mohamed',
                    $this->mergeWhen(false, ['Adam', 'Matt']),
                    'Jeffrey',
                    new MergeValue(['Abigail', 'Lydia']),
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'First', 'Second', 'Taylor', 'Mohamed', 'Jeffrey', 'Abigail', 'Lydia',
        ], $results);
    }

    public function testInitialMergeValuesMayBeMissing()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    $this->mergeWhen(false, ['First', 'Second']),
                    'Taylor',
                    'Mohamed',
                    $this->mergeWhen(true, ['Adam', 'Matt']),
                    'Jeffrey',
                    new MergeValue(['Abigail', 'Lydia']),
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'Taylor', 'Mohamed', 'Adam', 'Matt', 'Jeffrey', 'Abigail', 'Lydia',
        ], $results);
    }

    public function testMergeValueCanMergeJsonSerializable()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                $postResource = new PostResource(new Post([
                    'id' => 1,
                    'title' => 'Test Title 1',
                ]));

                return $this->filter([
                    new MergeValue($postResource),
                    'user' => 'test user',
                    'age' => 'test age',
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'id' => 1,
            'title' => 'Test Title 1',
            'custom' => true,
            'user' => 'test user',
            'age' => 'test age',
        ], $results);
    }

    public function testMergeValueCanMergeCollectionOfJsonSerializable()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                $posts = collect([
                    new Post(['id' => 1, 'title' => 'Test title 1']),
                    new Post(['id' => 2, 'title' => 'Test title 2']),
                ]);

                return $this->filter([
                    new MergeValue(PostResource::collection($posts)),
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            ['id' => 1, 'title' => 'Test title 1', 'custom' => true],
            ['id' => 2, 'title' => 'Test title 2', 'custom' => true],
        ], $results);
    }

    public function testAllMergeValuesMayBeMissing()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    $this->mergeWhen(false, ['First', 'Second']),
                    'Taylor',
                    'Mohamed',
                    $this->mergeWhen(false, ['Adam', 'Matt']),
                    'Jeffrey',
                    $this->mergeWhen(false, ['Abigail', 'Lydia']),
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'Taylor', 'Mohamed', 'Jeffrey',
        ], $results);
    }

    public function testMergeValuesMayFallbackToDefaults()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    $this->mergeUnless(false, ['Taylor', 'Mohamed'], ['First', 'Second']),
                    $this->mergeWhen(false, ['Adam', 'Matt'], ['Abigail', 'Lydia']),
                    'Jeffrey',
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            'Taylor', 'Mohamed', 'Abigail', 'Lydia', 'Jeffrey',
        ], $results);
    }

    public function testNestedMerges()
    {
        $filter = new class
        {
            use ConditionallyLoadsAttributes;

            public function work()
            {
                return $this->filter([
                    $this->mergeWhen(true, [['Something']]),
                    [
                        $this->mergeWhen(true, ['First', $this->mergeWhen(true, ['Second'])]),
                        'Third',
                    ],
                    [
                        'Fourth',
                    ],
                ]);
            }
        };

        $results = $filter->work();

        $this->assertEquals([
            [
                'Something',
            ],
            [
                'First', 'Second', 'Third',
            ],
            [
                'Fourth',
            ],
        ], $results);
    }

    public function testTheResourceCanBeAnArray()
    {
        $this->assertJsonResourceResponse([
            'user@example.com' => 'John',
            'admin@example.com' => 'Hank',
        ], [
            'data' => [
                'user@example.com' => 'John',
                'admin@example.com' => 'Hank',
            ],
        ]);
    }

    public function testItWillReturnAsAnArrayWhenStringKeysAreStripped()
    {
        $this->assertJsonResourceResponse([
            1 => 'John',
            2 => 'Hank',
            'foo' => new MissingValue,
        ], ['data' => ['John', 'Hank']]);

        $this->assertJsonResourceResponse([
            1 => 'John',
            'foo' => new MissingValue,
            3 => 'Hank',
        ], ['data' => ['John', 'Hank']]);

        $this->assertJsonResourceResponse([
            'foo' => new MissingValue,
            2 => 'John',
            3 => 'Hank',
        ], ['data' => ['John', 'Hank']]);
    }

    public function testItStripsNumericKeys()
    {
        $this->assertJsonResourceResponse([
            0 => 'John',
            1 => 'Hank',
        ], ['data' => ['John', 'Hank']]);

        $this->assertJsonResourceResponse([
            0 => 'John',
            1 => 'Hank',
            3 => 'Bill',
        ], ['data' => ['John', 'Hank', 'Bill']]);

        $this->assertJsonResourceResponse([
            5 => 'John',
            6 => 'Hank',
        ], ['data' => ['John', 'Hank']]);
    }

    public function testItWontKeysIfAnyOfThemAreStrings()
    {
        $this->assertJsonResourceResponse([
            '5' => 'John',
            '6' => 'Hank',
            'a' => 'Bill',
        ], ['data' => ['5' => 'John', '6' => 'Hank', 'a' => 'Bill']]);

        $this->assertJsonResourceResponse([
            0 => 10,
            1 => 20,
            'total' => 30,
        ], ['data' => [0 => 10, 1 => 20, 'total' => 30]]);
    }

    public function testItThrowsNoErrorInStrictModeWhenResourceIsPaginated()
    {
        $originalMode = Model::preventsAccessingMissingAttributes();
        Model::preventAccessingMissingAttributes();
        try {
            Route::get('/', function () {
                $paginator = new LengthAwarePaginator(
                    collect([new Post(['id' => 5, 'title' => 'Test Title', 'reading_time' => 3.0])]),
                    10, 15, 1
                );

                return PostResourceWithJsonOptions::collection($paginator);
            });

            $response = $this->withoutExceptionHandling()->get(
                '/', ['Accept' => 'application/json']
            );

            $response->assertStatus(200);
        } finally {
            Model::preventAccessingMissingAttributes($originalMode);
        }
    }

    public function testResourceSkipsWrappingWhenDataKeyExists()
    {
        $resource = new class(['id' => 5, 'title' => 'Test', 'data' => 'some data']) extends JsonResource
        {
            public static $wrap = 'data';
        };

        $response = $resource->toResponse(request());
        $content = json_decode($response->getContent(), true);

        $this->assertEquals([
            'id' => 5,
            'title' => 'Test',
            'data' => 'some data',
        ], $content);
    }

    public function testResourceWrapsWhenDataKeyDoesNotExist()
    {
        $resource = new class(['id' => 5, 'title' => 'Test']) extends JsonResource
        {
            public static $wrap = 'data';
        };

        $response = $resource->toResponse(request());
        $content = json_decode($response->getContent(), true);

        $this->assertEquals([
            'data' => [
                'id' => 5,
                'title' => 'Test',
            ],
        ], $content);
    }

    public function testResourceForceWrapOverridesDataKeyCheck()
    {
        $resource = new class(['id' => 5, 'title' => 'Test', 'data' => 'some data']) extends JsonResource
        {
            public static $wrap = 'data';
            public static bool $forceWrapping = true;
        };

        $response = $resource->toResponse(request());
        $content = json_decode($response->getContent(), true);

        $this->assertEquals([
            'data' => [
                'id' => 5,
                'title' => 'Test',
                'data' => 'some data',
            ],
        ], $content);
    }

    private function assertJsonResourceResponse($data, $expectedJson)
    {
        Route::get('/', function () use ($data) {
            return new JsonResource($data);
        });

        $this->withoutExceptionHandling()
            ->get('/', ['Accept' => 'application/json'])
            ->assertStatus(200)
            ->assertExactJson($expectedJson);
    }
}
