<?php

namespace Livewire\Features\SupportJsEvaluation;

use Livewire\Livewire;

class BrowserTest extends \Tests\BrowserTestCase
{
    public static function tweakApplicationHook()
    {
        return function () {
            app('livewire.finder')->addLocation(viewPath: __DIR__ . '/fixtures');
        };
    }

    public function test_can_toggle_a_purely_js_property_with_a_purely_js_function()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $show = false;

                #[BaseJs]
                function toggle()
                {
                    return <<<'JS'
                        $wire.show = ! $wire.show;
                    JS;
                }

                public function render() { return <<<'HTML'
                <div>
                    <button @click="$wire.toggle" dusk="toggle">Toggle</button>

                    <div dusk="target" x-show="$wire.show">
                        Toggle Me!
                    </div>
                </div>
                HTML; }
        })
        ->waitUntilMissingText('Toggle Me!')
        ->assertDontSee('Toggle Me!')
        ->click('@toggle')
        ->waitForText('Toggle Me!')
        ->assertSee('Toggle Me!')
        ->click('@toggle')
        ->waitUntilMissingText('Toggle Me!')
        ->assertDontSee('Toggle Me!')
        ;
    }

    public function test_can_evaluate_js_code_after_an_action_is_performed()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $show = false;

                function toggle()
                {
                    $this->js('$wire.show = true');
                }

                public function render() { return <<<'HTML'
                <div>
                    <button wire:click="toggle" dusk="toggle">Toggle</button>

                    <div dusk="target" x-show="$wire.show">
                        Toggle Me!
                    </div>
                </div>
                HTML; }
        })
        ->assertDontSee('Toggle Me!')
        ->waitForLivewire()->click('@toggle')
        ->waitForText('Toggle Me!')
        ;
    }

    public function test_can_evaluate_js_on_mount_that_calls_a_livewire_action()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $count = 0;

                public function mount()
                {
                    $this->js('$wire.increment()');
                }

                public function increment()
                {
                    $this->count++;
                }

                public function render() { return <<<'HTML'
                <div>
                    <span dusk="count">{{ $count }}</span>
                </div>
                HTML; }
        })
        ->waitForText('1')
        ->assertSeeIn('@count', '1')
        ;
    }

    public function test_a_broken_js_expression_does_not_wedge_the_component()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $count = 0;

                // Easy mistake: `record` here is a PHP action, not a registered
                // `$js` action, so the expression blows up client-side...
                public function breakIt()
                {
                    $this->js('increment');
                }

                public function increment()
                {
                    $this->count++;
                }

                public function render() { return <<<'HTML'
                <div>
                    <span dusk="count">{{ $count }}</span>
                    <span wire:loading dusk="loading">Loading...</span>
                    <button wire:click="breakIt" dusk="break">Break</button>
                    <button wire:click="increment" dusk="increment">Increment</button>
                </div>
                HTML; }
        })
        ->waitForLivewire()->click('@break')
        // The loading state must not stick around after the response...
        ->assertDontSee('Loading...')
        // ...and the component must still respond to further actions...
        ->waitForLivewire()->click('@increment')
        ->assertSeeIn('@count', '1')
        ;
    }

    public function test_a_throwing_effect_hook_does_not_fire_the_render_hook()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $count = 0;

                public function increment()
                {
                    $this->count++;
                }

                public function render() { return <<<'HTML'
                <div>
                    <span dusk="count">{{ $count }}</span>
                    <button dusk="arm" x-on:click="window.armed = true">Arm</button>
                    <button wire:click="increment" dusk="increment">Increment</button>
                </div>

                @script
                <script>
                    window.hooks = []

                    Livewire.hook('effect', () => {
                        if (window.armed) throw new Error('boom')
                    })

                    $wire.interceptMessage(({ onSuccess, onFinish }) => {
                        onSuccess(({ onRender }) => onRender(() => window.hooks.push('onRender')))
                        onFinish(() => window.hooks.push('onFinish'))
                    })
                </script>
                @endscript
                HTML; }
        })
        ->click('@arm')
        ->waitForLivewire()->click('@increment')
        ->pause(300)
        // The morph never completed, so nothing was painted. `onFinish` still
        // runs (it's documented to run on error), but `onRender` must not...
        ->assertScript('window.hooks.includes("onFinish")')
        ->assertScript('window.hooks.includes("onRender") === false')
        ;
    }

    public function test_can_define_js_actions_though_dollar_wire_on_a_component()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public function render() { return <<<'HTML'
                <div>
                    <button wire:click="$js.test" dusk="test">Test</button>
                </div>

                @script
                <script>
                    $wire.$js('test', () => {
                        window.test = 'through dollar wire'
                    })
                </script>
                @endscript
                HTML; }
            }
        )
        ->click('@test')
        ->assertScript('window.test === "through dollar wire"')
        ;
    }

    public function test_can_define_js_actions_though_dollar_wire_on_a_component_using_direct_propert_assignment()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public function render() { return <<<'HTML'
                <div>
                    <button wire:click="$js.test" dusk="test">Test</button>
                </div>

                @script
                <script>
                    $wire.$js.test = () => {
                        window.test = 'through dollar wire'
                    }
                </script>
                @endscript
                HTML; }
            }
        )
        ->click('@test')
        ->assertScript('window.test === "through dollar wire"')
        ;
    }

    public function test_can_define_js_actions_though_dollar_js_magic_in_a_script()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public function render() { return <<<'HTML'
                <div>
                    <button wire:click="$js.test" dusk="test">Test</button>
                </div>

                @script
                <script>
                    $js('test', () => {
                        window.test = 'through dollar js'
                    })
                </script>
                @endscript
                HTML; }
            }
        )
        ->click('@test')
        ->assertScript('window.test === "through dollar js"')
        ;
    }

    public function test_can_define_js_actions_though_dollar_js_magic_in_a_sfc_script()
    {
        Livewire::visit('sfc-component-with-dollar-js-magic')
            ->waitForLivewireToLoad()
            // Pause for a moment to allow the script to be loaded...
            ->pause(100)
            ->click('@test')
            ->assertScript('window.test === "through dollar js"')
        ;
    }

    public function test_can_define_js_actions_though_dollar_js_magic_on_a_mfc_script()
    {
        Livewire::visit('mfc-component-with-dollar-js-magic')
            ->waitForLivewireToLoad()
            // Pause for a moment to allow the script to be loaded...
            ->pause(100)
            ->click('@test')
            ->assertScript('window.test === "through dollar js"')
        ;
    }

    public function test_can_call_a_defined_js_action_from_wire_click_without_params()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public function render() {
                    return <<<'HTML'
                        <div>
                            <button wire:click="$js.test" dusk="test">Test</button>
                        </div>

                        @script
                        <script>
                            this.$js('test', () => {
                                window.test = 'through wire:click'
                            })
                        </script>
                        @endscript
                    HTML;
                }
            }
        )
        ->click('@test')
        ->assertScript('window.test === "through wire:click"')
        ;
    }

    public function test_can_call_a_defined_js_action_from_wire_click_with_params()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public function render() {
                    return <<<'HTML'
                        <div>
                            <button wire:click="$js.test('foo','bar')" dusk="test">Test</button>
                        </div>

                        @script
                        <script>
                            this.$js.test = (param1, param2) => {
                                console.log('test', param1, param2);
                                window.test = `through wire:click with params: ${param1}, ${param2}`
                            }
                        </script>
                        @endscript
                    HTML;
                }
            }
        )
        ->click('@test')
        ->assertScript('window.test === "through wire:click with params: foo, bar"')
        ;
    }

    public function test_can_call_a_defined_js_action_from_the_backend_using_the_js_method_without_params()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public function save() {
                    $this->js('test');
                }
                public function render() {
                    return <<<'HTML'
                        <div>
                            <button wire:click="save" dusk="save">Save</button>
                        </div>

                        @script
                        <script>
                            this.$js('test', () => {
                                window.test = 'through backend js method'
                            })
                        </script>
                        @endscript
                    HTML;
                }
            }
        )
        ->waitForLivewire()->click('@save')
        ->assertScript('window.test === "through backend js method"')
        ;
    }

    public function test_can_call_a_defined_js_action_from_the_backend_using_the_js_method_with_params()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public function save() {
                    $this->js('test', 'foo', 'bar');
                }
                public function render() {
                    return <<<'HTML'
                        <div>
                            <button wire:click="save" dusk="save">Save</button>
                        </div>

                        @script
                        <script>
                            this.$js('test', (param1, param2) => {
                                window.test = `through backend js method with params: ${param1}, ${param2}`
                            })
                        </script>
                        @endscript
                    HTML;
                }
            }
        )
        ->waitForLivewire()->click('@save')
        ->assertScript('window.test === "through backend js method with params: foo, bar"')
        ;
    }

    public function test_can_call_js_action_with_alpine_x_for_loop_variable()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $users = [
                    ['id' => 1, 'name' => 'Alice'],
                    ['id' => 2, 'name' => 'Bob'],
                ];

                public function render() {
                    return <<<'HTML'
                        <div>
                            <template x-for="user in $wire.users" :key="user.id">
                                <div>
                                    <button wire:click="$js.selectUser(user)" x-text="user.name" :dusk="'select-' + user.id"></button>
                                </div>
                            </template>
                        </div>

                        @script
                        <script>
                            $wire.$js.selectUser = (user) => {
                                window.selectedUser = JSON.stringify(user)
                            }
                        </script>
                        @endscript
                    HTML;
                }
            }
        )
        ->waitForText('Alice')
        ->click('@select-1')
        ->assertScript('JSON.parse(window.selectedUser).name === "Alice"')
        ->click('@select-2')
        ->assertScript('JSON.parse(window.selectedUser).name === "Bob"')
        ;
    }

    public function test_dollar_prefixed_wire_methods_work_inside_alpine_scope()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $value = 'initial';

                public function render() {
                    return <<<'HTML'
                        <div>
                            <div x-data="{ $set: () => {}, $get: () => {} }">
                                <button wire:click="$set('value', 'updated')" dusk="button">Click</button>
                            </div>
                            <span dusk="output" x-text="$wire.value"></span>
                        </div>
                    HTML;
                }
            }
        )
        ->assertSeeIn('@output', 'initial')
        ->waitForLivewire()->click('@button')
        ->assertSeeIn('@output', 'updated')
        ;
    }

    public function test_js_action_called_from_wire_click_has_this_bound_to_wire()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $value = 'from wire';

                public function render() {
                    return <<<'HTML'
                        <div>
                            <button wire:click="$js.test" dusk="test">Test</button>
                        </div>

                        @script
                        <script>
                            this.$js('test', function () {
                                window.test = this.$get('value')
                            })
                        </script>
                        @endscript
                    HTML;
                }
            }
        )
        ->click('@test')
        ->assertScript('window.test === "from wire"')
        ;
    }

    public function test_js_action_called_from_php_has_this_bound_to_wire()
    {
        Livewire::visit(
            new class extends \Livewire\Component {
                public $value = 'from wire';

                public function save() {
                    $this->js('test');
                }

                public function render() {
                    return <<<'HTML'
                        <div>
                            <button wire:click="save" dusk="save">Save</button>
                        </div>

                        @script
                        <script>
                            this.$js('test', function () {
                                window.test = this.$get('value')
                            })
                        </script>
                        @endscript
                    HTML;
                }
            }
        )
        ->waitForLivewire()->click('@save')
        ->assertScript('window.test === "from wire"')
        ;
    }

    public function test_parent_alpine_scope_does_not_leak_into_child_wire_click()
    {
        Livewire::visit([
            new class extends \Livewire\Component {
                public function render() {
                    return <<<'HTML'
                        <div x-data="{ handleClick() { $el.querySelector('[dusk=output]').textContent = 'alpine' } }">
                            <livewire:child />
                        </div>
                    HTML;
                }
            },
            'child' => new class extends \Livewire\Component {
                public $result = '';

                public function handleClick() {
                    $this->result = 'livewire';
                }

                public function render() {
                    return <<<'HTML'
                        <div x-data="{}">
                            <button wire:click="handleClick" dusk="button">Click</button>
                            <span dusk="output">{{ $result }}</span>
                        </div>
                    HTML;
                }
            },
        ])
        ->waitForLivewire()->click('@button')
        ->assertSeeIn('@output', 'livewire')
        ;
    }
}
