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
        when loaded via `pytest_plugins = ['nicegui.testing.user_plugin']`.
        The `user` fixture cuts away the browser and replaces it by a lightweight simulation entirely in Python.
        See [project structure](/documentation/project_structure) for a description of the setup.

        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 slow 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 is quite new and still misses 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('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
                def receive_file(e: events.UploadEventArguments):
                    content = e.content.read().decode('utf-8')
                    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
                upload = user.find(ui.upload).elements.pop()
                upload.handle_uploads([UploadFile(
                    BytesIO(b'name,age\\nAlice,30\\nBob,28'),
                    filename='data.csv',
                    headers=Headers(raw=[(b'content-type', b'text/csv')]),
                )])
                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('Autocomplete', '''
    The `UserInteraction` object returned by `user.find(...)` provides methods to trigger events on the found elements.
    This demo shows how to trigger a "keydown.tab" event to autocomplete an input field.

    *Added in version 2.7.0*
''')


@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('Test Downloads', '''
    You can verify that a download was triggered by checking `user.downloads.http_responses`.
    By awaiting `user.downloads.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:
                userA = create_user()
                await userA.open('/')
                userB = create_user()
                await userB.open('/')

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


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

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