from nicegui import ui
from nicegui.testing import User, UserInteraction

from ..windows import python_window
from . import doc


@doc.part('User Fixture')
def user_fixture():
    ui.markdown('''
        We recommend utilizing the `user` fixture instead of the [`screen` fixture](/documentation/screen) wherever possible
        because execution is as fast as unit tests and it does not need Selenium as a dependency.
        The `user` fixture cuts away the browser and replaces it by a lightweight simulation.

        You can assert to "see" specific elements or content, click buttons, type into inputs and trigger events.
        We aimed for a nice API to write acceptance tests which read like a story and are easy to understand.
        Due to the fast execution, the classical [test pyramid](https://martinfowler.com/bliki/TestPyramid.html),
        where UI tests are considered to be slow, error prone and expensive, does not apply anymore 🚀.
    ''').classes('bold-links arrow-links')

    with python_window(classes='w-[600px]', title='example'):
        ui.markdown('''
            ```python
            await user.open('/')
            user.find('Username').type('user1')
            user.find('Password').type('pass1').trigger('keydown.enter')
            await user.should_see('Hello user1!')
            user.find('logout').click()
            await user.should_see('Log in')
            ```
        ''')

    ui.markdown('''
        **NOTE:** The `user` fixture might still miss some features.
        Please let us know in separate feature requests
        [over on GitHub](https://github.com/zauberzeug/nicegui/discussions/new?category=ideas-feature-requests).
    ''').classes('bold-links arrow-links')


@doc.part('Async execution')
def async_execution():
    ui.markdown('''
        The user simulation runs in the same async context as your app
        to make querying and interaction as easy as possible.
        But that also means that your tests must be `async`.
        We suggest to activate the [pytest-asyncio auto-mode](https://pytest-asyncio.readthedocs.io/en/latest/concepts.html#auto-mode)
        by either creating a `pytest.ini` file in your project root
        or adding the activation directly to your `pyproject.toml`.
    ''').classes('bold-links arrow-links')

    with ui.row(wrap=False).classes('gap-4 items-center'):
        with python_window(classes='w-[300px] h-42', title='pytest.ini'):
            ui.markdown('''
                ```ini
                [pytest]
                asyncio_mode = auto
                ```
            ''')
        ui.label('or').classes('text-2xl')
        with python_window(classes='w-[300px] h-42', title='pyproject.toml'):
            ui.markdown('''
                ```toml
                [tool.pytest.ini_options]
                asyncio_mode = "auto"
                ```
            ''')


doc.text('Querying', '''
    The querying capabilities of the `User` are built upon the [ElementFilter](/documentation/element_filter).
    The `user.should_see(...)` method and `user.find(...)` method
    provide parameters to filter for content, [markers](/documentation/element_filter#markers), types, etc.
    If you do not provide a named property, the string will match against the text content and markers.
''')


@doc.ui
def querying():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[400px]', title='some UI code'):
            ui.markdown('''
                ```python
                with ui.row():
                    ui.label('Hello World!').mark('greeting')
                    ui.icon('star')
                with ui.row():
                    ui.label('Hello Universe!')
                    ui.input(placeholder='Type here')
                ```
            ''')

        with python_window(classes='w-[600px]', title='user assertions'):
            ui.markdown('''
                ```python
                await user.should_see('greeting')
                await user.should_see('star')
                await user.should_see('Hello Universe!')
                await user.should_see('Type here')
                await user.should_see('Hello')
                await user.should_see(marker='greeting')
                await user.should_see(kind=ui.icon)
                ```
            ''')


doc.text('User Interaction', '''
    `user.find(...)` returns a `UserInteraction` object which provides methods to type text,
    clear inputs, click buttons and trigger events on the found elements.
    This demo shows how to trigger a "keydown.tab" event to autocomplete an input field after typing the first letter.

    *Added in version 2.7.0: triggering events*
''')


@doc.ui
def trigger_events():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[500px]', title='some UI code'):
            ui.markdown('''
                ```python
                fruits = ['apple', 'banana', 'cherry']
                ui.input(label='fruit', autocomplete=fruits)
                ```
            ''')
        with python_window(classes='w-[500px]', title='user assertions'):
            ui.markdown('''
                ```python
                await user.open('/')
                user.find('fruit').type('a').trigger('keydown.tab')
                await user.should_see('apple')
                ```
            ''')


doc.text('Selecting options', '''
    To choose items in a `ui.select` simply

    - locate the `ui.select` element using `user.find()`,
    - use `click()` to open the dropdown,
    - locate the specific _option_ you want to select, again using `user.find()`, and
    - use `click()` a second time to select the desired option.

    For a multi-select element, repeat the click-and-choose steps for each item.
''')


@doc.ui
def selecting_options_in_a_select():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[500px]', title='UI code'):
            ui.markdown('''
                ```python
                ui.select(
                    ['Apple', 'Banana', 'Cherry'],
                    label='Fruits',
                    multiple=True,
                    on_change=lambda e: ui.notify(', '.join(e.value)),
                )
                ```
            ''')

        with python_window(classes='w-[500px]', title='user assertions'):
            ui.markdown('''
                ```python
                user.find('Fruits').click()
                user.find('Apple').click()
                user.find('Banana').click()
                await user.should_see('Apple, Banana')
                ```
            ''')


doc.text('Using an ElementFilter', '''
    It may be desirable to use an [`ElementFilter`](/documentation/element_filter) to

    - preserve the order of elements to check their order on the page, and
    - more granular filtering options, such as `ElementFilter(...).within(...)`.

    By entering the `user` context and iterating over `ElementFilter`,
    you can preserve the natural document order of matching elements:
''')


@doc.ui
def using_an_elementfilter():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[400px]', title='UI code'):
            ui.markdown('''
                ```python
                ui.label('1').mark('number')
                ui.label('2').mark('number')
                ui.label('3').mark('number')
                ```
            ''')

        with python_window(classes='w-[600px]', title='user assertions'):
            ui.markdown('''
                ```python
                with user:
                    elements = list(ElementFilter(marker='number'))
                    assert len(elements) == 3
                    assert elements[0].text == '1'
                    assert elements[1].text == '2'
                    assert elements[2].text == '3'
                ```
            ''')


doc.text('Complex elements', '''
    There are some elements with complex visualization and interaction behaviors (`ui.upload`, `ui.table`, ...).
    Not every aspect of these elements can be tested with `should_see` and `UserInteraction`.
    Still, you can grab them with `user.find(...)` and do the testing on the elements themselves.
''')


@doc.ui
def upload_table():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[500px]', title='some UI code'):
            ui.markdown('''
                ```python
                async def receive_file(e: events.UploadEventArguments):
                    content = await e.file.text()
                    reader = csv.DictReader(content.splitlines())
                    ui.table(
                        columns=[{
                            'name': h,
                            'label': h.capitalize(),
                            'field': h,
                        } for h in reader.fieldnames or []],
                        rows=list(reader),
                    )

                ui.upload(on_upload=receive_file)
                ```
            ''')

        with python_window(classes='w-[500px]', title='user assertions'):
            ui.markdown('''
                ```python
                from nicegui import ui

                upload = user.find(ui.upload).elements.pop()
                await upload.handle_uploads([
                    ui.upload.SmallFileUpload('data.csv', 'text/csv', b'name,age\\nAlice,30\\nBob,28')
                ])
                await user.should_see(ui.table)
                table = user.find(ui.table).elements.pop()
                assert table.columns == [
                    {'name': 'name', 'label': 'Name', 'field': 'name'},
                    {'name': 'age', 'label': 'Age', 'field': 'age'},
                ]
                assert table.rows == [
                    {'name': 'Alice', 'age': '30'},
                    {'name': 'Bob', 'age': '28'},
                ]
                ```
            ''')


doc.text('Test Downloads', '''
    You can verify that a download was triggered by checking `user.download.http_responses`.
    By awaiting `user.download.next()` you can get the next download response.

    *Added in version 2.1.0*
''')


@doc.ui
def check_outbox():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[500px]', title='some UI code'):
            ui.markdown('''
                ```python
                @ui.page('/')
                def page():
                    def download():
                        ui.download(b'Hello', filename='hello.txt')

                    ui.button('Download', on_click=download)
                ```
            ''')

        with python_window(classes='w-[500px]', title='user assertions'):
            ui.markdown('''
                ```python
                await user.open('/')
                assert len(user.download.http_responses) == 0
                user.find('Download').click()
                response = await user.download.next()
                assert response.text == 'Hello'
                ```
            ''')


doc.text('Multiple Users', '''
    Sometimes it is not enough to just interact with the UI as a single user.
    Besides the `user` fixture, we also provide the `create_user` fixture which is a factory function to create users.
    The `User` instances are independent from each other and can interact with the UI in parallel.
    See our [Chat App example](https://github.com/zauberzeug/nicegui/blob/main/examples/chat_app/test_chat_app.py)
    for a full demonstration.
''')


@doc.ui
def multiple_users():
    with python_window(classes='w-[600px]', title='example'):
        ui.markdown('''
            ```python
            async def test_chat(create_user: Callable[[], User]) -> None:
                user1 = create_user()
                await user1.open('/')
                user2 = create_user()
                await user2.open('/')

                user1.find(ui.input).type('from A').trigger('keydown.enter')
                await user2.should_see('from A')
                user2.find(ui.input).type('from B').trigger('keydown.enter')
                await user1.should_see('from A')
                await user1.should_see('from B')
            ```
        ''')


doc.text('Simulate JavasScript', '''
    The `User` class has a `javascript_rules` dictionary to simulate JavaScript execution.
    The key is a compiled regular expression and the value is a function that returns the JavaScript response.
    The function will be called with the match object of the regular expression on the JavaScript command.

    *Added in version 2.14.0*
''')


@doc.ui
def simulate_javascript():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[500px]', title='some UI code'):
            ui.markdown('''
                ```python
                @ui.page('/')
                async def page():
                    await context.client.connected()
                    date = await ui.run_javascript('Math.sqrt(1764)')
                    ui.label(date)
                ```
            ''')

        with python_window(classes='w-[500px]', title='user assertions'):
            ui.markdown('''
                ```python
                user.javascript_rules[re.compile(r'Math.sqrt\\((\\d+)\\)')] = \\
                    lambda match: int(match.group(1))**0.5
                await user.open('/')
                await user.should_see('42')
                ```
            ''')


doc.text('Comparison with the screen fixture', '''
    By cutting out the browser, test execution becomes much faster than the [`screen` fixture](/documentation/screen).
    See our [pytests example](https://github.com/zauberzeug/nicegui/tree/main/examples/pytests)
    which implements the same tests with both fixtures.
    Of course, some features like screenshots or browser-specific behavior are not available,
    but in most cases the speed of the `user` fixture makes it the first choice.
''')


doc.text('User Simulation Context', '''
    The [`user_simulation`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/testing/user_simulation.py) context
    is the low-level building block behind the `user` fixture.
    It spins up a NiceGUI app inside the same event loop, providing deterministic test control without Selenium.
    Unlike the `user` fixture, it does not rely on pytest-specific infrastructure
    and can be used with `unittest` or within plain async code.

    The context supports three testing approaches:

    - Test a `root` callable directly.
    - Test a specific main file by passing its path.
    - Define `ui.page` definitions inline within the context.

    More usage examples can be found in
    [`tests/test_user_simulation_context.py`](https://github.com/zauberzeug/nicegui/blob/main/tests/test_user_simulation_context.py).
''')


@doc.ui
def user_simulation_examples():
    with ui.row().classes('gap-4 items-stretch'):
        with python_window(classes='w-[700px]', title='script mode with root'):
            ui.markdown('''
                ```python
                from nicegui.testing import user_simulation

                async def test_click_via_root():
                    def root():
                        ui.button('Click me', on_click=lambda: ui.notify('Hello World!'))

                    async with user_simulation(root) as user:
                        await user.open('/')
                        await user.should_see('Click me')
                        user.find(ui.button).click()
                        await user.should_see('Hello World!')
                ```
            ''')

        with python_window(classes='w-[700px]', title='main file via path'):
            ui.markdown('''
                ```python
                from nicegui.testing import user_simulation

                async def test_click_via_main_file():
                    async with user_simulation(main_file='app.py') as user:
                        await user.open('/')
                        await user.should_see('Main file content')
                ```
            ''')

        with python_window(classes='w-[700px]', title='inline UI definitions'):
            ui.markdown('''
                ```python
                from nicegui.testing import user_simulation

                async def test_inline_pages():
                    async with user_simulation() as user:

                        @ui.page('/')
                        def main_page():
                            ui.label('Main page')

                        await user.open('/')
                        await user.should_see('Main page')
                ```
            ''')


doc.reference(User, title='User Reference')
doc.reference(UserInteraction, title='UserInteraction Reference')
