<?php



namespace Livewire\Mechanisms\PersistentMiddleware;

use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as AuthUser;
use Illuminate\Foundation\Http\Kernel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Livewire\Attributes\Modelable;
use Livewire\Attributes\Reactive;
use Livewire\Component as BaseComponent;
use Livewire\Livewire;
use Sushi\Sushi;
use Symfony\Component\HttpFoundation\Response;
use Tests\BrowserTestCase;

class BrowserTest extends BrowserTestCase
{
    protected function getEnvironmentSetUp($app) {
        parent::getEnvironmentSetUp($app);

        $app['config']->set('auth.providers.users.model', User::class);
    }

    protected function resolveApplicationHttpKernel($app)
    {
        $app->singleton(\Illuminate\Contracts\Http\Kernel::class, HttpKernel::class);
    }

    public static function tweakApplicationHook() {
        return function() {
            Livewire::addPersistentMiddleware([AllowListedMiddleware::class, IsBanned::class]);

            // Overwrite the default route for these tests, so the middleware is included
            Route::get('livewire-dusk/{component}', function ($component) {
                $class = urldecode($component);

                return app()->call(app('livewire')->new($class));
            })->middleware(['web', AllowListedMiddleware::class, BlockListedMiddleware::class]);

            Route::get('/force-login/{userId}', function ($userId) {
                Auth::login(User::find($userId));

                return 'You\'re logged in.';
            })->middleware('web');

            Route::get('/force-logout', function () {
                Auth::logout();

                return 'You\'re logged out.';
            })->middleware('web');

            Route::get('/with-authentication/livewire-dusk/{component}', function ($component) {
                $class = urldecode($component);

                return app()->call(app('livewire')->new($class));
            })->middleware(['web', 'auth']);

            Route::get('/with-redirects/livewire-dusk/{component}', function ($component) {
                $class = urldecode($component);

                return app()->call(app('livewire')->new($class));
            })->middleware(['web', 'auth', IsBanned::class]);

            Gate::policy(Post::class, PostPolicy::class);

            Route::get('/with-authorization/{post}/livewire-dusk/{component}', function (Post $post, $component) {
                $class = urldecode($component);

                return app()->call(new $class);
            })->middleware(['web', 'auth', 'can:update,post']);

            Route::get('/with-authorization/{post}/inline-auth', Component::class)
                ->middleware(['web', 'auth', 'can:update,post']);

            Livewire::component('page-with-reactive-child', PageComponentWithReactiveChild::class);
            Livewire::component('child-with-reactive', ChildComponentWithReactive::class);
            Livewire::component('page-with-modelable-child', PageComponentWithModelableChild::class);
            Livewire::component('child-with-modelable', ChildComponentWithModelable::class);
            Livewire::component('page-with-explicit-binding', PageComponentWithExplicitBinding::class);
            Livewire::component('child-for-explicit-binding', ChildForExplicitBinding::class);

            Route::get('/page-with-reactive-child/{post}', PageComponentWithReactiveChild::class)
                ->middleware('web');

            Route::get('/page-with-modelable-child/{post}', PageComponentWithModelableChild::class)
                ->middleware('web');

            Route::model('bound_model', BoundModel::class);

            Route::get('/explicit-binding/{bound_model}', PageComponentWithExplicitBinding::class)
                ->middleware('web');
        };
    }
    public function test_that_persistent_middleware_is_applied_to_subsequent_livewire_requests()
    {
        // @todo: Copy implementation from V2 for persistent middleware and ensure localisation and subdirectory hosting are supported. https://github.com/livewire/livewire/pull/5490
        Livewire::visit([Component::class, 'child' => NestedComponent::class])
            // See allow-listed middleware from original request.
            ->assertSeeIn('@middleware', json_encode([AllowListedMiddleware::class, BlockListedMiddleware::class]))

            ->waitForLivewire()->click('@refresh')

            // See that the original request middleware was re-applied.
            ->assertSeeIn('@middleware', json_encode([AllowListedMiddleware::class]))

            ->waitForLivewire()->click('@showNested')

            // Even to nested components shown AFTER the first load.
            ->assertSeeIn('@middleware', json_encode([AllowListedMiddleware::class]))
            ->assertSeeIn('@nested-middleware', json_encode([AllowListedMiddleware::class]))

            ->waitForLivewire()->click('@refreshNested')

            // Make sure they are still applied when stand-alone requests are made to that component.
            ->assertSeeIn('@middleware', json_encode([AllowListedMiddleware::class]))
            ->assertSeeIn('@nested-middleware', json_encode([AllowListedMiddleware::class]))
            ;
    }

    public function test_that_authentication_middleware_is_re_applied()
    {
        Livewire::visit(Component::class)
            ->visit('/with-authentication/livewire-dusk/'.urlencode(Component::class))
            ->assertDontSee('Protected Content')
            ->visit('/force-login/1')
            ->visit('/with-authentication/livewire-dusk/'.urlencode(Component::class))
            ->waitForLivewireToLoad()
            ->assertSee('Protected Content')
            // We're going to make a fetch request, but store the request payload
            // so we can replay it from a different page.
            ->tap(function ($b) {
                $script = <<<'JS'
                    let unDecoratedFetch = window.fetch
                    let decoratedFetch = (...args) => {
                        window.localStorage.setItem(
                            'lastFetchArgs',
                            JSON.stringify(args),
                        )

                        return unDecoratedFetch(...args)
                    }
                    window.fetch = decoratedFetch
                JS;

                $b->script($script);
            })
            ->waitForLivewire()->click('@changeProtected')
            ->assertDontSee('Protected Content')
            ->assertSee('Still Secure Content')
            // Now we logout.
            ->visit('/force-logout')
            // Now we try and re-run the request payload, expecting that
            // the "auth" middleware will be applied, recognize we've
            // logged out and throw an error in the response.
            ->tap(function ($b) {
                $script = <<<'JS'
                    let args = JSON.parse(localStorage.getItem('lastFetchArgs'))

                    window.controller = new AbortController()

                    args[1].signal = window.controller.signal

                    window.fetch(...args).then(i => i.text()).then(response => {
                        document.body.textContent = 'response-ready: '+JSON.stringify(response)
                    })
JS;

                $b->script($script);
            })
            ->waitForText('response-ready: ')
            ->assertDontSee('Protected Content');
        ;
    }

    public function test_that_authorization_middleware_is_re_applied()
    {
        Livewire::visit(Component::class)
            ->visit('/with-authorization/1/livewire-dusk/'.urlencode(Component::class))
            ->assertDontSee('Protected Content')
            ->visit('/force-login/1')
            ->visit('/with-authorization/1/livewire-dusk/'.urlencode(Component::class))
            ->waitForText('Protected Content')
            ->waitForLivewireToLoad()
            ->tap(function ($b) {
                $script = <<<'JS'
                    let unDecoratedFetch = window.fetch
                    let decoratedFetch = (...args) => {
                        window.localStorage.setItem(
                            'lastFetchArgs',
                            JSON.stringify(args),
                        )

                        return unDecoratedFetch(...args)
                    }
                    window.fetch = decoratedFetch
JS;

                $b->script($script);
            })
            ->waitForLivewire()->click('@changeProtected')
            ->assertDontSee('Protected Content')
            ->assertSee('Still Secure Content')
            ->tap(function ($b) {
                // Wait until the fetch args have been stored in localStorage
                // to prevent race conditions in the test.
                $b->waitUntil("!!localStorage.getItem('lastFetchArgs')", 5);
            })
            ->visit('/force-login/2')
            ->tap(function ($b) {
                $script = <<<'JS'
                    let args = JSON.parse(localStorage.getItem('lastFetchArgs'))

                    window.controller = new AbortController()

                    args[1].signal = window.controller.signal

                    window.fetch(...args).then(i => i.text()).then(response => {
                        document.body.textContent = 'response-ready: '+JSON.stringify(response)
                    })
JS;

                $b->script($script);
            })
            ->waitForText('response-ready: ')
            ->assertDontSee('Protected Content')
        ;
    }

    public function test_that_persistent_middleware_redirects_on_subsequent_requests()
    {
        Livewire::visit(Component::class)
            ->tap(function () {
                User::where('id', 1)->update(['banned' => false]); // Reset the user. Sometimes it's cached(?.
            })
            ->visit('/force-login/1')
            ->visit('/with-redirects/livewire-dusk/' . urlencode(Component::class))
            ->assertSee('Protected Content')
            ->tap(function () {
                User::where('id', 1)->update(['banned' => true]);
            })
            ->waitForLivewire()
            ->click('@refresh')
            ->assertPathIs('/force-logout')
        ;
    }

    public function test_that_authorization_middleware_is_re_applied_on_page_components()
    {
        // This test relies on "app('router')->subsituteImplicitBindingsUsing()"...
        if (app()->version() < '10.37.1') {
            $this->markTestSkipped();
        }

        Livewire::visit(Component::class)
            ->visit('/with-authorization/1/inline-auth')
            ->assertDontSee('Protected Content')
            ->visit('/force-login/1')
            ->visit('/with-authorization/1/inline-auth')
            ->assertSee('Protected Content')
            ->waitForLivewireToLoad()
            ->tap(function ($b) {
                $script = <<<'JS'
                    let unDecoratedFetch = window.fetch
                    let decoratedFetch = (...args) => {
                        window.localStorage.setItem(
                            'lastFetchArgs',
                            JSON.stringify(args),
                        )

                        return unDecoratedFetch(...args)
                    }
                    window.fetch = decoratedFetch
JS;

                $b->script($script);
            })
            ->waitForLivewire()->click('@changeProtected')
            ->assertDontSee('Protected Content')
            ->assertSee('Still Secure Content')
            ->visit('/force-login/2')
            ->tap(function ($b) {
                $script = <<<'JS'
                    let args = JSON.parse(localStorage.getItem('lastFetchArgs'))

                    window.controller = new AbortController()

                    args[1].signal = window.controller.signal

                    window.fetch(...args).then(i => i.text()).then(response => {
                        document.body.textContent = 'response-ready: '+JSON.stringify(response)
                    })
JS;

                $b->script($script);
            })
            ->waitForText('response-ready: ')
            ->assertDontSee('Protected Content')
        ;
    }

    public function test_child_component_does_not_404_after_parent_deletes_route_bound_model()
    {
        // This test relies on "app('router')->subsituteImplicitBindingsUsing()"...
        if (app()->version() < '10.37.1') {
            $this->markTestSkipped();
        }

        Post::truncate();
        Post::insert([
            ['id' => 1, 'title' => 'First', 'user_id' => 1],
            ['id' => 2, 'title' => 'Second', 'user_id' => 2],
        ]);

        Livewire::visit(Component::class)
            ->visit('/page-with-reactive-child/1')
            ->assertSee('test')

            // Delete the route-bound model - without the fix, this would
            // 404 because PersistentMiddleware tries to resolve the route binding
            // for the deleted model when processing the child's snapshot
            ->waitForLivewire()->click('@delete')
            ->assertSee('test2')
            ->assertDontSee('404')
        ;
    }

    public function test_modelable_child_component_does_not_404_after_parent_deletes_route_bound_model()
    {
        // This test relies on "app('router')->subsituteImplicitBindingsUsing()"...
        if (app()->version() < '10.37.1') {
            $this->markTestSkipped();
        }

        Post::truncate();
        Post::insert([
            ['id' => 1, 'title' => 'First', 'user_id' => 1],
            ['id' => 2, 'title' => 'Second', 'user_id' => 2],
        ]);

        Livewire::visit(Component::class)
            ->visit('/page-with-modelable-child/1')
            ->assertSee('test')

            // Delete the route-bound model - without the fix, this would
            // 404 because PersistentMiddleware tries to resolve the route binding
            // for the deleted model when processing the child's snapshot
            ->waitForLivewire()->click('@delete')
            ->assertSee('test2')
            ->assertDontSee('404')
        ;
    }

    public function test_explicit_route_model_binding_does_not_break_with_lazy_child()
    {
        Livewire::visit(Component::class)
            ->visit('/explicit-binding/valid-slug')
            ->assertSee('original')

            // Without the fix, when the lazy child loads via a subsequent
            // Livewire request, SubstituteBindings would re-resolve the
            // explicit model binding on the cached route using the model
            // instance instead of the original route key string, causing
            // a 404 or wrong model.
            ->waitForText('child loaded')
            ->assertDontSee('404')
        ;
    }
}

class HttpKernel extends Kernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \Illuminate\Cookie\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \Orchestra\Testbench\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];

    /**
     * The priority-sorted list of middleware.
     *
     * This forces non-global middleware to always be in the given order.
     *
     * @var array
     */
    protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Illuminate\Auth\Middleware\Authenticate::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];
}

class Component extends BaseComponent
{
    public static $loggedMiddleware = [];

    public $middleware = [];
    public $showNested = false;
    public $changeProtected = false;

    public function mount(Post $post)
    {
        //
    }

    public function showNestedComponent()
    {
        $this->showNested = true;
    }

    public function toggleProtected()
    {
        $this->changeProtected = ! $this->changeProtected;
    }

    public function render()
    {
        $this->middleware = static::$loggedMiddleware;

        return <<<'HTML'
<div>
    <span dusk="middleware">@json($middleware)</span>

    <button wire:click="$refresh" dusk="refresh">Refresh</button>
    <button wire:click="toggleProtected" dusk="changeProtected">Change Protected</button>
    <button wire:click="showNestedComponent" dusk="showNested">Show Nested</button>

    <h1>
        @unless($changeProtected)
            Protected Content
        @else
            Still Secure Content
        @endunless
    </h1>

    @if ($showNested)
        @livewire(\Livewire\Mechanisms\PersistentMiddleware\NestedComponent::class)
    @endif
</div>
HTML;
    }
}

class NestedComponent extends BaseComponent
{
    public $middleware = [];

    public function render()
    {
        $this->middleware = Component::$loggedMiddleware;

        return <<<'HTML'
<div>
    <span dusk="nested-middleware">@json($middleware)</span>

    <button wire:click="$refresh" dusk="refreshNested">Refresh</button>
</div>
HTML;
    }
}

class AllowListedMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        Component::$loggedMiddleware[] = static::class;

        return $next($request);
    }
}

class BlockListedMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        Component::$loggedMiddleware[] = static::class;

        return $next($request);
    }
}

class User extends AuthUser
{
    use Sushi;

    protected $fillable = ['banned'];

    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    protected $rows = [
        [
            'id' => 1,
            'name' => 'First User',
            'email' => 'first@laravel-livewire.com',
            'password' => '',
            'banned' => false,
        ],
        [
            'id' => 2,
            'name' => 'Second user',
            'email' => 'second@laravel-livewire.com',
            'password' => '',
            'banned' => false,
        ],
    ];
}

class Post extends Model
{
    use Sushi;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    protected $rows = [
        ['title' => 'First', 'user_id' => 1],
        ['title' => 'Second', 'user_id' => 2],
    ];
}

class PostPolicy
{
    public function update(User $user, Post $post)
    {
        return (int) $post->user_id === (int) $user->id;
    }
}

class IsBanned
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->user()->banned) {
            return redirect('/force-logout');
        }

        return $next($request);
    }
}

class PageComponentWithReactiveChild extends BaseComponent
{
    public Post $post;

    public $test = 'test';

    public function delete()
    {
        $this->post->delete();
        $this->test = 'test2';
    }

    public function render()
    {
        return <<<'HTML'
        <div>
            <livewire:child-with-reactive :$test />

            <button wire:click="delete" dusk="delete">Delete</button>
        </div>
        HTML;
    }
}

class ChildComponentWithReactive extends BaseComponent
{
    #[Reactive]
    public $test;

    public function render()
    {
        return <<<'HTML'
        <div>
            {{ $test }}
        </div>
        HTML;
    }
}

class PageComponentWithModelableChild extends BaseComponent
{
    public Post $post;

    public $test = 'test';

    public function delete()
    {
        $this->post->delete();
        $this->test = 'test2';
    }

    public function render()
    {
        return <<<'HTML'
        <div>
            <livewire:child-with-modelable wire:model="test" />

            <button wire:click="delete" dusk="delete">Delete</button>
        </div>
        HTML;
    }
}

class ChildComponentWithModelable extends BaseComponent
{
    #[Modelable]
    public $value;

    public function render()
    {
        return <<<'HTML'
        <div>
            {{ $value }}
        </div>
        HTML;
    }
}

class BoundModel extends Model
{
    protected $guarded = [];

    public function __toString()
    {
        return 'string-representation';
    }

    public function getRouteKeyName()
    {
        return 'slug';
    }

    public function resolveRouteBinding($value, $field = null)
    {
        if ($value !== 'valid-slug') {
            return null;
        }

        return new static(['slug' => 'valid-slug']);
    }
}

class PageComponentWithExplicitBinding extends BaseComponent
{
    public function render()
    {
        return <<<'HTML'
        <div>
            <span dusk="output">original</span>

            <livewire:child-for-explicit-binding lazy />
        </div>
        HTML;
    }
}

class ChildForExplicitBinding extends BaseComponent
{
    public function render()
    {
        return <<<'HTML'
        <div>
            <span dusk="child-output">child loaded</span>
        </div>
        HTML;
    }
}
