<?php

namespace Livewire\Features\SupportLazyLoading;

use Illuminate\Support\Facades\Route;
use Tests\BrowserTestCase;
use Livewire\Livewire;
use Livewire\Component;
use Livewire\Attributes\On;
use Livewire\Attributes\Reactive;

class BrowserTest extends BrowserTestCase
{
    public function test_can_lazy_load_a_component()
    {
        Livewire::visit([new class extends Component {
            public function render() { return <<<HTML
            <div>
                <livewire:child lazy />
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            public function mount() {
                sleep(1);
            }

            public function render() {
                return <<<HTML
                <div id="child">
                    Child!
                </div>
                HTML;
            }
        }])
        ->assertDontSee('Child!')
        ->waitFor('#child')
        ->assertSee('Child!')
        ;
    }

    public function test_can_defer_lazy_load_a_component()
    {
        Livewire::visit([new class extends Component {
            public function render()
            {
                return <<<HTML
            <div>
                <div style="height: 200vh"></div>
                <livewire:child defer />
            </div>
            HTML;
            }
        }, 'child' => new class extends Component {
            public function mount()
            {
                sleep(1);
            }

            public function render()
            {
                return <<<HTML
                <div id="child">
                    Child!
                </div>
                HTML;
            }
        }])
            ->assertDontSee('Child!')
            ->waitFor('#child')
            ->assertSee('Child!');
    }

    public function test_cant_lazy_load_a_component_on_intersect_outside_viewport()
    {
        Livewire::visit([new class extends Component {
            public function render()
            {
                return <<<HTML
            <div>
                <div style="height: 200vh"></div>
                <livewire:child lazy />
            </div>
            HTML;
            }
        }, 'child' => new class extends Component {
            public function mount()
            {
                sleep(1);
            }

            public function render()
            {
                return <<<HTML
                <div id="child">
                    Child!
                </div>
                HTML;
            }
        }])
            ->assertDontSee('Child!')
            ->pause(2000)
            ->assertDontSee('Child!');
    }

    public function can_lazy_load_full_page_component_using_attribute()
    {
        Livewire::visit(new #[\Livewire\Attributes\Lazy] class extends Component {
            public function mount() {
                sleep(1);
            }

            public function placeholder() { return <<<HTML
                <div id="loading">
                    Loading...
                </div>
                HTML; }

            public function render() { return <<<HTML
                <div id="page">
                    Hello World
                </div>
                HTML; }
        })
        ->assertSee('Loading...')
        ->assertDontSee('Hello World')
        ->waitFor('#page')
        ->assertDontSee('Loading...')
        ->assertSee('Hello World')
        ;
    }

    public function can_defer_lazy_load_full_page_component_using_attribute()
    {
        Livewire::visit(new #[\Livewire\Attributes\Defer] class extends Component {
            public function mount() {
                sleep(1);
            }

            public function placeholder() { return <<<HTML
                <div id="loading">
                    Loading...
                </div>
                HTML; }

            public function render() { return <<<HTML
                <div id="page">
                    Hello World
                </div>
                HTML; }
        })
        ->assertSee('Loading...')
        ->assertDontSee('Hello World')
        ->waitFor('#page')
        ->assertDontSee('Loading...')
        ->assertSee('Hello World')
        ;
    }

    public function test_can_lazy_load_component_using_route()
    {
        $this->beforeServingApplication(function() {
            Livewire::component('page', Page::class);
            Route::get('/', Page::class)->lazy()->middleware('web');
        });

        $this->browse(function ($browser) {
            $browser
                ->visit('/')
                ->tap(fn ($b) => $b->script('window._lw_dusk_test = true'))
                ->assertScript('return window._lw_dusk_test')
                ->assertSee('Loading...')
                ->assertDontSee('Hello World')
                ->waitFor('#page')
                ->assertDontSee('Loading...')
                ->assertSee('Hello World');
        });
    }

    public function test_can_defer_lazy_load_component_using_route()
    {
        $this->beforeServingApplication(function() {
            Livewire::component('page', Page::class);
            Route::get('/', Page::class)->defer()->middleware('web');
        });

        $this->browse(function ($browser) {
            $browser
                ->visit('/')
                ->tap(fn ($b) => $b->script('window._lw_dusk_test = true'))
                ->assertScript('return window._lw_dusk_test')
                ->assertSee('Loading...')
                ->assertDontSee('Hello World')
                ->waitFor('#page')
                ->assertDontSee('Loading...')
                ->assertSee('Hello World');
        });
    }

    public function test_can_lazy_load_component_without_mount_using_route()
    {
        $this->beforeServingApplication(function() {
            Livewire::component('page-without-mount', PageWithoutMount::class);
            Route::get('/', PageWithoutMount::class)->lazy()->middleware('web');
        });

        $this->browse(function ($browser) {
            $browser
                ->visit('/')
                ->tap(fn ($b) => $b->script('window._lw_dusk_test = true'))
                ->assertScript('return window._lw_dusk_test')
                ->assertSee('Loading...')
                ->assertDontSee('Hello World')
                ->waitFor('#page')
                ->assertDontSee('Loading...')
                ->assertSee('Hello World');
        });
    }

    public function test_can_defer_lazy_load_component_without_mount_using_route()
    {
        $this->beforeServingApplication(function() {
            Livewire::component('page-without-mount', PageWithoutMount::class);
            Route::get('/', PageWithoutMount::class)->defer()->middleware('web');
        });

        $this->browse(function ($browser) {
            $browser
                ->visit('/')
                ->tap(fn ($b) => $b->script('window._lw_dusk_test = true'))
                ->assertScript('return window._lw_dusk_test')
                ->assertSee('Loading...')
                ->assertDontSee('Hello World')
                ->waitFor('#page')
                ->assertDontSee('Loading...')
                ->assertSee('Hello World');
        });
    }

    public function test_can_lazy_load_a_component_with_a_placeholder()
    {
        Livewire::visit([new class extends Component {
            public function render() { return <<<HTML
            <div>
                <livewire:child lazy />
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            public function mount() { sleep(1); }
            public function placeholder() { return <<<HTML
                <div id="loading">
                    Loading...
                </div>
                HTML; }
            public function render() { return <<<HTML
            <div id="child">
                Child!
            </div>
            HTML; }
        }])
        ->assertSee('Loading...')
        ->assertDontSee('Child!')
        ->waitFor('#child')
        ->assertDontSee('Loading...')
        ->assertSee('Child!')
        ;
    }

    public function test_can_pass_props_to_lazyilly_loaded_component()
    {
        Livewire::visit([new class extends Component {
            public $count = 1;
            public function render() { return <<<'HTML'
            <div>
                <livewire:child :$count lazy />
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            public $count;
            public function mount() { sleep(1); }
            public function render() { return <<<'HTML'
            <div id="child">
                Count: {{ $count }}
            </div>
            HTML; }
        }])
        ->waitFor('#child')
        ->assertSee('Count: 1')
        ;
    }

    public function test_can_pass_props_to_mount_method_to_lazyilly_loaded_component()
    {
        Livewire::visit([new class extends Component {
            public $count = 1;
            public function render() { return <<<'HTML'
            <div>
                <livewire:child :$count lazy />
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            public $count;
            public function mount($count) { $this->count = $this->count + 2; }
            public function render() { return <<<'HTML'
            <div id="child">
                Count: {{ $count }}
            </div>
            HTML; }
        }])
        ->waitFor('#child')
        ->assertSee('Count: 3')
        ;
    }

    public function test_can_pass_reactive_props_to_lazyilly_loaded_component()
    {
        Livewire::visit([new class extends Component {
            public $count = 1;
            public function inc() { $this->count++; }
            public function render() { return <<<'HTML'
            <div>
                <livewire:child :$count lazy />
                <button wire:click="inc" dusk="button">+</button>
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            #[Reactive]
            public $count;
            public function mount() { sleep(1); }
            public function render() { return <<<'HTML'
            <div id="child">
                Count: {{ $count }}
            </div>
            HTML; }
        }])
        ->waitFor('#child')
        ->waitForText('Count: 1')
        ->assertSee('Count: 1')
        ->waitForLivewire()->click('@button')
        ->waitForText('Count: 2')
        ->assertSee('Count: 2')
        ->waitForLivewire()->click('@button')
        ->waitForText('Count: 3')
        ->assertSee('Count: 3')
        ;
    }

    public function test_lazy_component_outside_viewport_is_not_loaded_when_reactive_prop_changes()
    {
        Livewire::visit([new class extends Component {
            public $count = 1;
            public function inc() { $this->count++; }
            public function render() { return <<<'HTML'
            <div>
                <button wire:click="inc" dusk="button">+</button>
                <div dusk="parent-count">Parent: {{ $count }}</div>
                <div style="height: 200vh"></div>
                <livewire:child :$count lazy />
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            #[Reactive]
            public $count;
            public $config = [];
            public function mount() { $this->config = ['label' => 'Count']; }
            public function render() { return <<<'HTML'
            <div id="child">
                <div x-data="{ state: $wire.$entangle('config.label') }">
                    <span dusk="child-label" x-text="state"></span>
                </div>
            </div>
            HTML; }
        }])
        ->assertMissing('#child')
        ->waitForLivewire()->click('@button')
        ->waitForTextIn('@parent-count', 'Parent: 2')
        ->assertMissing('#child')
        ;
    }

    public function test_can_access_component_parameters_in_placeholder_view()
    {
        Livewire::visit([new class extends Component {
            public function render() { return <<<HTML
            <div>
                <livewire:child my-parameter="A Parameter Value" lazy />
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            public function mount($myParameter) {
                sleep(1);
            }
            public function placeholder(array $params = []) {
                return view('placeholder', $params);
            }
            public function render() {
                return <<<HTML
                <div id="child">
                    Child!
                </div>
                HTML;
            }
        }])
        ->waitFor('#loading')
        ->assertSee('A Parameter Value')
        ->assertDontSee('Child!')
        ->waitFor('#child')
        ->assertSee('Child!')
        ;
    }

    public function test_lazy_components_are_not_bundled_by_default()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <livewire:child1 lazy />
                        <livewire:child2 lazy />
                        <livewire:child3 lazy />
                    </div>
                    @script
                    <script>
                        window.requestCount = 0

                        Livewire.interceptRequest(({ onSend }) => {
                            onSend(() => window.requestCount++)
                        })
                    </script>
                    @endscript
                    HTML;
                }
            },
            'child1' => new class extends Component {
                public function render() {
                    return '<div>Child 1</div>';
                }
            },
            'child2' => new class extends Component {
                public function render() {
                    return '<div>Child 2</div>';
                }
            },
            'child3' => new class extends Component {
                public function render() {
                    return '<div>Child 3</div>';
                }
            }
        ])
        ->waitForLivewireToLoad()

        ->waitForText('Child 1')
        ->waitForText('Child 2')
        ->waitForText('Child 3')

        // Lazy components should be isolated by default, making separate requests
        ->assertScript('window.requestCount', 3)
        ;
    }

    public function test_lazy_bundle_bundles_multiple_lazy_components_into_single_request()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <livewire:child1 lazy.bundle />
                        <livewire:child2 lazy.bundle />
                        <livewire:child3 lazy.bundle />
                    </div>
                    @script
                    <script>
                        window.requestCount = 0

                        Livewire.interceptRequest(({ onSend }) => {
                            onSend(() => window.requestCount++)
                        })
                    </script>
                    @endscript
                    HTML;
                }
            },
            'child1' => new class extends Component {
                public function render() {
                    return '<div>Child 1</div>';
                }
            },
            'child2' => new class extends Component {
                public function render() {
                    return '<div>Child 2</div>';
                }
            },
            'child3' => new class extends Component {
                public function render() {
                    return '<div>Child 3</div>';
                }
            }
        ])
        ->waitForLivewireToLoad()

        ->waitForText('Child 1')
        ->waitForText('Child 2')
        ->waitForText('Child 3')

        // All three lazy components should be bundled into a single request
        ->assertScript('window.requestCount', 1)
        ;
    }

    public function test_defer_components_are_not_bundled_by_default()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <livewire:child1 defer />
                        <livewire:child2 defer />
                        <livewire:child3 defer />
                    </div>
                    @script
                    <script>
                        window.requestCount = 0

                        Livewire.interceptRequest(({ onSend }) => {
                            onSend(() => window.requestCount++)
                        })
                    </script>
                    @endscript
                    HTML;
                }
            },
            'child1' => new class extends Component {
                public function render() {
                    return '<div>Child 1</div>';
                }
            },
            'child2' => new class extends Component {
                public function render() {
                    return '<div>Child 2</div>';
                }
            },
            'child3' => new class extends Component {
                public function render() {
                    return '<div>Child 3</div>';
                }
            }
        ])
        ->waitForLivewireToLoad()

        ->waitForText('Child 1')
        ->waitForText('Child 2')
        ->waitForText('Child 3')

        // Defer components should be isolated by default, making separate requests
        ->assertScript('window.requestCount', 3)
        ;
    }

    public function test_defer_bundle_bundles_multiple_defer_components_into_single_request()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <livewire:child1 defer.bundle />
                        <livewire:child2 defer.bundle />
                        <livewire:child3 defer.bundle />
                    </div>
                    @script
                    <script>
                        window.requestCount = 0

                        Livewire.interceptRequest(({ onSend }) => {
                            onSend(() => window.requestCount++)
                        })
                    </script>
                    @endscript
                    HTML;
                }
            },
            'child1' => new class extends Component {
                public function render() {
                    return '<div>Child 1</div>';
                }
            },
            'child2' => new class extends Component {
                public function render() {
                    return '<div>Child 2</div>';
                }
            },
            'child3' => new class extends Component {
                public function render() {
                    return '<div>Child 3</div>';
                }
            }
        ])
        ->waitForLivewireToLoad()

        ->waitForText('Child 1')
        ->waitForText('Child 2')
        ->waitForText('Child 3')

        // All three defer components should be bundled into a single request
        ->assertScript('window.requestCount', 1)
        ;
    }

    public function test_lazy_component_outside_viewport_ignores_global_dispatch()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <button wire:click="$dispatch('refresh-child')" dusk="button">Refresh</button>
                        <div dusk="parent">Parent</div>
                        <div style="height: 200vh"></div>
                        <livewire:child lazy />
                    </div>
                    HTML;
                }
            },
            'child' => new class extends Component {
                public $title = '';

                public function mount()
                {
                    $this->title = 'Mounted';
                }

                #[On('refresh-child')]
                public function refresh() {}

                public function render()
                {
                    return <<<'HTML'
                    <div id="child">
                        Title: {{ $title }}
                    </div>
                    HTML;
                }
            },
        ])
        ->assertMissing('#child')
        ->click('@button')
        ->pause(500)
        ->assertMissing('#child')
        ;
    }

    public function test_lazy_component_outside_viewport_ignores_dispatch_to()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <button @click="window.Livewire.dispatchTo('child', 'refresh-child')" dusk="button">Refresh</button>
                        <div dusk="parent">Parent</div>
                        <div style="height: 200vh"></div>
                        <livewire:child lazy />
                    </div>
                    HTML;
                }
            },
            'child' => new class extends Component {
                public $title = '';

                public function mount()
                {
                    $this->title = 'Mounted';
                }

                #[On('refresh-child')]
                public function refresh() {}

                public function render()
                {
                    return <<<'HTML'
                    <div id="child">
                        Title: {{ $title }}
                    </div>
                    HTML;
                }
            },
        ])
        ->assertMissing('#child')
        ->click('@button')
        ->pause(500)
        ->assertMissing('#child')
        ;
    }

    public function test_lazy_component_receives_events_after_being_loaded()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <button wire:click="$dispatch('refresh-child')" dusk="button">Refresh</button>
                        <livewire:child lazy />
                    </div>
                    HTML;
                }
            },
            'child' => new class extends Component {
                public $count = 0;

                #[On('refresh-child')]
                public function refresh()
                {
                    $this->count++;
                }

                public function render()
                {
                    return <<<'HTML'
                    <div id="child">
                        <span dusk="count">{{ $count }}</span>
                    </div>
                    HTML;
                }
            },
        ])
        ->waitFor('#child')
        ->assertSeeIn('@count', '0')
        ->waitForLivewire()->click('@button')
        ->assertSeeIn('@count', '1')
        ;
    }

    public function test_lazy_component_can_use_dynamic_event_listeners_with_placeholders_set_in_mount()
    {
        Livewire::visit([
            new class extends Component {
                public function render() {
                    return <<<'HTML'
                    <div>
                        <button wire:click="$dispatch('refresh-child:abc123')" dusk="button">Refresh</button>
                        <livewire:child lazy />
                    </div>
                    HTML;
                }
            },
            'child' => new class extends Component {
                public string $parentId = '';
                public int $count = 0;

                public function mount(): void
                {
                    $this->parentId = 'abc123';
                }

                #[On('refresh-child:{parentId}')]
                public function refresh(): void
                {
                    $this->count++;
                }

                public function render()
                {
                    return <<<'HTML'
                    <div id="child">
                        <span dusk="count">{{ $count }}</span>
                    </div>
                    HTML;
                }
            },
        ])
        ->waitFor('#child')
        ->assertSeeIn('@count', '0')
        ->waitForLivewire()->click('@button')
        ->assertSeeIn('@count', '1')
        ->assertConsoleLogHasNoErrors()
        ;
    }

    public function test_can_defer_a_component_with_non_div_root()
    {
        Livewire::visit(new #[\Livewire\Attributes\Defer] class extends Component {
            public function boot() { usleep(200_000); }

            public function placeholder() { return <<<HTML
                <div id="loading">
                    Loading...
                </div>
                HTML; }

            public function render() { return <<<HTML
                <section id="page">
                    Hello World
                </section>
                HTML; }
        })
        ->assertSee('Loading...')
        ->assertDontSee('Hello World')
        ->waitFor('#page')
        ->assertDontSee('Loading...')
        ->assertSee('Hello World')
        ->assertConsoleLogHasNoErrors();
    }

    public function test_slot_content_is_preserved_on_lazy_component()
    {
        Livewire::visit([new class extends Component {
            public function render() { return <<<'HTML'
            <div>
                <livewire:child lazy>
                    <span id="slot-content">Slot works!</span>
                </livewire:child>
            </div>
            HTML; }
        }, 'child' => new class extends Component {
            public function render() { return <<<'HTML'
            <div id="child">
                {{ $slot }}
            </div>
            HTML; }
        }])
        ->waitFor('#child')
        ->waitFor('#slot-content')
        ->assertSee('Slot works!')
        ;
    }

}

class Page extends Component {
    public function mount() {
        sleep(1);
    }

    public function placeholder() { return <<<HTML
            <div id="loading">
                Loading...
            </div>
            HTML; }

    public function render() { return <<<HTML
            <div id="page">
                Hello World
            </div>
            HTML; }
}

class PageWithoutMount extends Component {
    public function boot() {
        usleep(200_000);
    }

    public function placeholder() { return <<<HTML
            <div id="loading">
                Loading...
            </div>
            HTML; }

    public function render() { return <<<HTML
            <div id="page">
                Hello World
            </div>
            HTML; }
}

