/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import assert from 'node:assert';
import fs from 'node:fs/promises';
import path from 'node:path';
import { describe, it } from 'node:test';
import { click, hover, fill, drag, fillForm, uploadFile, pressKey, } from '../../src/tools/input.js';
import { parseKey } from '../../src/utils/keyboard.js';
import { serverHooks } from '../server.js';
import { html, withMcpContext } from '../utils.js';
describe('input', () => {
    const server = serverHooks();
    describe('click', () => {
        it('clicks', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<button onclick="this.innerText = 'clicked';">test</button>`);
                await context.createTextSnapshot();
                await click.handler({
                    params: {
                        uid: '1_1',
                    },
                }, response, context);
                assert.strictEqual(response.responseLines[0], 'Successfully clicked on the element');
                assert.ok(response.includeSnapshot);
                assert.ok(await page.$('text/clicked'));
            });
        });
        it('double clicks', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<button ondblclick="this.innerText = 'dblclicked';"
            >test</button
          >`);
                await context.createTextSnapshot();
                await click.handler({
                    params: {
                        uid: '1_1',
                        dblClick: true,
                    },
                }, response, context);
                assert.strictEqual(response.responseLines[0], 'Successfully double clicked on the element');
                assert.ok(response.includeSnapshot);
                assert.ok(await page.$('text/dblclicked'));
            });
        });
        it('waits for navigation', async () => {
            const resolveNavigation = Promise.withResolvers();
            server.addHtmlRoute('/link', html `<a href="/navigated">Navigate page</a>`);
            server.addRoute('/navigated', async (_req, res) => {
                await resolveNavigation.promise;
                res.write(html `<main>I was navigated</main>`);
                res.end();
            });
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.goto(server.getRoute('/link'));
                await context.createTextSnapshot();
                const clickPromise = click.handler({
                    params: {
                        uid: '1_1',
                    },
                }, response, context);
                const [t1, t2] = await Promise.all([
                    clickPromise.then(() => Date.now()),
                    new Promise(res => {
                        setTimeout(() => {
                            resolveNavigation.resolve();
                            res(Date.now());
                        }, 300);
                    }),
                ]);
                assert(t1 > t2, 'Waited for navigation');
            });
        });
        it('waits for stable DOM', async () => {
            server.addHtmlRoute('/unstable', html `
          <button>Click to change to see time</button>
          <script>
            const button = document.querySelector('button');
            button.addEventListener('click', () => {
              setTimeout(() => {
                button.textContent = Date.now();
              }, 50);
            });
          </script>
        `);
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.goto(server.getRoute('/unstable'));
                await context.createTextSnapshot();
                const handlerResolveTime = await click
                    .handler({
                    params: {
                        uid: '1_1',
                    },
                }, response, context)
                    .then(() => Date.now());
                const buttonChangeTime = await page.evaluate(() => {
                    const button = document.querySelector('button');
                    return Number(button?.textContent);
                });
                assert(handlerResolveTime > buttonChangeTime, 'Waited for navigation');
            });
        });
    });
    describe('hover', () => {
        it('hovers', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<button onmouseover="this.innerText = 'hovered';">test</button>`);
                await context.createTextSnapshot();
                await hover.handler({
                    params: {
                        uid: '1_1',
                    },
                }, response, context);
                assert.strictEqual(response.responseLines[0], 'Successfully hovered over the element');
                assert.ok(response.includeSnapshot);
                assert.ok(await page.$('text/hovered'));
            });
        });
    });
    describe('fill', () => {
        it('fills out an input', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<input />`);
                await context.createTextSnapshot();
                await fill.handler({
                    params: {
                        uid: '1_1',
                        value: 'test',
                    },
                }, response, context);
                assert.strictEqual(response.responseLines[0], 'Successfully filled out the element');
                assert.ok(response.includeSnapshot);
                assert.ok(await page.$('text/test'));
            });
        });
        it('fills out a select by text', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<select
            ><option value="v1">one</option
            ><option value="v2">two</option></select
          >`);
                await context.createTextSnapshot();
                await fill.handler({
                    params: {
                        uid: '1_1',
                        value: 'two',
                    },
                }, response, context);
                assert.strictEqual(response.responseLines[0], 'Successfully filled out the element');
                assert.ok(response.includeSnapshot);
                const selectedValue = await page.evaluate(() => document.querySelector('select').value);
                assert.strictEqual(selectedValue, 'v2');
            });
        });
    });
    describe('drags', () => {
        it('drags one element onto another', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<div
              role="button"
              id="drag"
              draggable="true"
              >drag me</div
            >
            <div
              id="drop"
              aria-label="drop"
              style="width: 100px; height: 100px; border: 1px solid black;"
              ondrop="this.innerText = 'dropped';"
            >
            </div>
            <script>
              drag.addEventListener('dragstart', event => {
                event.dataTransfer.setData('text/plain', event.target.id);
              });
              drop.addEventListener('dragover', event => {
                event.preventDefault();
                event.dataTransfer.dropEffect = 'move';
              });
              drop.addEventListener('drop', event => {
                event.preventDefault();
                const data = event.dataTransfer.getData('text/plain');
                event.target.appendChild(document.getElementById(data));
              });
            </script>`);
                await context.createTextSnapshot();
                await drag.handler({
                    params: {
                        from_uid: '1_1',
                        to_uid: '1_2',
                    },
                }, response, context);
                assert.ok(response.includeSnapshot);
                assert.strictEqual(response.responseLines[0], 'Successfully dragged an element');
                assert.ok(await page.$('text/dropped'));
            });
        });
    });
    describe('fill form', () => {
        it('successfully fills out the form', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<form>
            <label
              >username<input
                name="username"
                type="text"
            /></label>
            <label
              >email<input
                name="email"
                type="text"
            /></label>
            <input
              type="submit"
              value="Submit"
            />
          </form>`);
                await context.createTextSnapshot();
                await fillForm.handler({
                    params: {
                        elements: [
                            {
                                uid: '1_2',
                                value: 'test',
                            },
                            {
                                uid: '1_4',
                                value: 'test2',
                            },
                        ],
                    },
                }, response, context);
                assert.ok(response.includeSnapshot);
                assert.strictEqual(response.responseLines[0], 'Successfully filled out the form');
                assert.deepStrictEqual(await page.evaluate(() => {
                    return [
                        // @ts-expect-error missing types
                        document.querySelector('input[name=username]').value,
                        // @ts-expect-error missing types
                        document.querySelector('input[name=email]').value,
                    ];
                }), ['test', 'test2']);
            });
        });
    });
    describe('uploadFile', () => {
        it('uploads a file to a file input', async () => {
            const testFilePath = path.join(process.cwd(), 'test.txt');
            await fs.writeFile(testFilePath, 'test file content');
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<form>
            <input
              type="file"
              id="file-input"
            />
          </form>`);
                await context.createTextSnapshot();
                await uploadFile.handler({
                    params: {
                        uid: '1_1',
                        filePath: testFilePath,
                    },
                }, response, context);
                assert.ok(response.includeSnapshot);
                assert.strictEqual(response.responseLines[0], `File uploaded from ${testFilePath}.`);
            });
            await fs.unlink(testFilePath);
        });
        it('uploads a file when clicking an element opens a file uploader', async () => {
            const testFilePath = path.join(process.cwd(), 'test.txt');
            await fs.writeFile(testFilePath, 'test file content');
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<button id="file-chooser-button">Upload file</button>
            <input
              type="file"
              id="file-input"
              style="display: none;"
            />
            <script>
              document
                .getElementById('file-chooser-button')
                .addEventListener('click', () => {
                  document.getElementById('file-input').click();
                });
            </script>`);
                await context.createTextSnapshot();
                await uploadFile.handler({
                    params: {
                        uid: '1_1',
                        filePath: testFilePath,
                    },
                }, response, context);
                assert.ok(response.includeSnapshot);
                assert.strictEqual(response.responseLines[0], `File uploaded from ${testFilePath}.`);
                const uploadedFileName = await page.$eval('#file-input', el => {
                    const input = el;
                    return input.files?.[0]?.name;
                });
                assert.strictEqual(uploadedFileName, 'test.txt');
                await fs.unlink(testFilePath);
            });
        });
        it('throws an error if the element is not a file input and does not open a file chooser', async () => {
            const testFilePath = path.join(process.cwd(), 'test.txt');
            await fs.writeFile(testFilePath, 'test file content');
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<div>Not a file input</div>`);
                await context.createTextSnapshot();
                await assert.rejects(uploadFile.handler({
                    params: {
                        uid: '1_1',
                        filePath: testFilePath,
                    },
                }, response, context), {
                    message: 'Failed to upload file. The element could not accept the file directly, and clicking it did not trigger a file chooser.',
                });
                assert.strictEqual(response.responseLines.length, 0);
                assert.strictEqual(response.snapshotParams, undefined);
                await fs.unlink(testFilePath);
            });
        });
    });
    describe('press_key', () => {
        it('parses keys', () => {
            assert.deepStrictEqual(parseKey('Shift+A'), ['A', 'Shift']);
            assert.deepStrictEqual(parseKey('Shift++'), ['+', 'Shift']);
            assert.deepStrictEqual(parseKey('Control+Shift++'), [
                '+',
                'Control',
                'Shift',
            ]);
            assert.deepStrictEqual(parseKey('Shift'), ['Shift']);
            assert.deepStrictEqual(parseKey('KeyA'), ['KeyA']);
        });
        it('throws on empty key', () => {
            assert.throws(() => {
                parseKey('');
            });
        });
        it('throws on invalid key', () => {
            assert.throws(() => {
                parseKey('aaaaa');
            });
        });
        it('throws on multiple keys', () => {
            assert.throws(() => {
                parseKey('Shift+Shift');
            });
        });
        it('processes press_key', async () => {
            await withMcpContext(async (response, context) => {
                const page = context.getSelectedPage();
                await page.setContent(html `<script>
            logs = [];
            document.addEventListener('keydown', e => logs.push('d' + e.key));
            document.addEventListener('keyup', e => logs.push('u' + e.key));
          </script>`);
                await context.createTextSnapshot();
                await pressKey.handler({
                    params: {
                        key: 'Control+Shift+C',
                    },
                }, response, context);
                assert.deepStrictEqual(await page.evaluate('logs'), [
                    'dControl',
                    'dShift',
                    'dC',
                    'uC',
                    'uShift',
                    'uControl',
                ]);
            });
        });
    });
});
