import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
// @vitest-environment happy-dom
import { describe, expect, it, vi } from 'vitest';
import { useState } from 'react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MentionableTextarea } from '../../components/MentionableTextarea.js';
/**
 * Mount the textarea against a state-managing host so we exercise the
 * controlled-value contract. Returning the live `value` lets each
 * test inspect what the parent ends up with after a selection.
 */
function Host({ initialValue = '', search, testid = 'ta', }) {
    const [value, setValue] = useState(initialValue);
    return (_jsxs(_Fragment, { children: [_jsx(MentionableTextarea, { value: value, onChange: setValue, search: search, "data-testid": testid }), _jsx("output", { "data-testid": "value", children: value })] }));
}
function fakeSearch(rows) {
    return vi.fn(async (q) => {
        const ql = q.toLowerCase();
        return rows.filter((u) => u.name.toLowerCase().includes(ql) || u.email.toLowerCase().includes(ql));
    });
}
const ROWS = [
    { id: 'u_alice', name: 'Alice Anderson', email: 'alice@example.com' },
    { id: 'u_bob', name: 'Bob Brown', email: 'bob@example.com' },
    { id: 'u_carol', name: 'Carol Chen', email: 'carol@example.com' },
];
async function typeAt(testid, value, caret = value.length) {
    const ta = screen.getByTestId(testid);
    await act(async () => {
        // Manually drive the textarea: testing-library's
        // `fireEvent.change` resets `selectionStart` to 0 in happy-dom,
        // which would defeat the mention picker (caret 0 → no mention).
        // We bypass React's value-tracker the same way testing-library
        // does, then set the caret BEFORE dispatching `input` so the
        // React handler sees the correct selection.
        ta.focus();
        const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
        if (setter)
            setter.call(ta, value);
        else
            ta.value = value;
        ta.setSelectionRange(caret, caret);
        ta.dispatchEvent(new Event('input', { bubbles: true }));
    });
}
describe('MentionableTextarea — picker dropdown', () => {
    it('does not show the picker before @ is typed', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        await typeAt('ta', 'hello world');
        expect(screen.queryByTestId('ta-picker')).toBeNull();
    });
    it('opens the picker the instant @ is typed and shows the empty state', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        await typeAt('ta', '@');
        await waitFor(() => expect(screen.getByTestId('ta-picker')).toBeTruthy());
    });
    it('populates the dropdown with users that match the query', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        await typeAt('ta', '@ali');
        await waitFor(() => expect(screen.getByTestId('ta-picker-row-u_alice')).toBeTruthy());
        // Bob/Carol should NOT match `ali`.
        expect(screen.queryByTestId('ta-picker-row-u_bob')).toBeNull();
    });
    it('shows "No matches" when the query has no hits', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        await typeAt('ta', '@zzzz');
        await waitFor(() => expect(screen.getByText(/No matches/i)).toBeTruthy());
    });
    it('closes the picker on Escape without inserting anything', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        await typeAt('ta', '@al');
        await waitFor(() => expect(screen.getByTestId('ta-picker')).toBeTruthy());
        fireEvent.keyDown(screen.getByTestId('ta'), { key: 'Escape' });
        expect(screen.queryByTestId('ta-picker')).toBeNull();
        expect(screen.getByTestId('value').textContent).toBe('@al');
    });
    it('inserts @<userId> + trailing space when a row is clicked', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        await typeAt('ta', '@ali');
        await waitFor(() => expect(screen.getByTestId('ta-picker-row-u_alice')).toBeTruthy());
        await act(async () => {
            // We use mousedown intentionally (the component listens for
            // mousedown to avoid losing focus before commit).
            fireEvent.mouseDown(screen.getByTestId('ta-picker-row-u_alice'));
        });
        expect(screen.getByTestId('value').textContent).toBe('@u_alice ');
        expect(screen.queryByTestId('ta-picker')).toBeNull();
    });
    it('Enter commits the highlighted row', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        await typeAt('ta', '@b');
        await waitFor(() => expect(screen.getByTestId('ta-picker-row-u_bob')).toBeTruthy());
        await act(async () => {
            fireEvent.keyDown(screen.getByTestId('ta'), { key: 'Enter' });
        });
        expect(screen.getByTestId('value').textContent).toBe('@u_bob ');
    });
    it('ArrowDown then Enter commits the second result', async () => {
        const search = fakeSearch(ROWS);
        render(_jsx(Host, { search: search }));
        // Query 'e' hits Alice/Bob/Carol's emails — three matches.
        await typeAt('ta', '@e');
        await waitFor(() => expect(screen.getByTestId('ta-picker-row-u_alice')).toBeTruthy());
        fireEvent.keyDown(screen.getByTestId('ta'), { key: 'ArrowDown' });
        fireEvent.keyDown(screen.getByTestId('ta'), { key: 'ArrowDown' });
        fireEvent.keyDown(screen.getByTestId('ta'), { key: 'ArrowUp' }); // back to row 2
        await act(async () => {
            fireEvent.keyDown(screen.getByTestId('ta'), { key: 'Enter' });
        });
        // Highlight after Down/Down/Up: 0 → 1 → 2 → 1. Row index 1 should
        // be Bob (alphabetised by name? Actually fakeSearch returns in
        // ROWS order — Alice, Bob, Carol). So commit = u_bob.
        expect(screen.getByTestId('value').textContent).toBe('@u_bob ');
    });
    it('keyboard does NOT swallow Enter when the picker is closed', async () => {
        const search = fakeSearch(ROWS);
        const onKeyDown = vi.fn();
        const Wrapper = () => {
            const [v, setV] = useState('');
            return (_jsx(MentionableTextarea, { value: v, onChange: setV, search: search, onKeyDown: onKeyDown, "data-testid": "ta" }));
        };
        render(_jsx(Wrapper, {}));
        fireEvent.keyDown(screen.getByTestId('ta'), { key: 'Enter' });
        expect(onKeyDown).toHaveBeenCalledTimes(1);
    });
    it('does NOT commit a stale row when Enter fires before a new query resolves (PR #159 review)', async () => {
        // Regression for PR #159 Copilot finding: typing `@a`, getting
        // Alice in the dropdown, then quickly typing `@bo` and hitting
        // Enter before the `@bo` query resolves used to commit Alice
        // (from the previous query) instead of doing nothing or
        // committing Bob. The fix clears `users` on every new query so
        // Enter has no row to commit until the new results land.
        let resolveSlow = () => undefined;
        const search = vi.fn((q) => {
            if (q === 'bo') {
                return new Promise((resolve) => {
                    resolveSlow = resolve;
                });
            }
            return Promise.resolve(ROWS.filter((u) => u.name.toLowerCase().includes(q.toLowerCase())));
        });
        render(_jsx(Host, { search: search }));
        await typeAt('ta', '@a');
        await waitFor(() => expect(screen.getByTestId('ta-picker-row-u_alice')).toBeTruthy());
        // Now type the new query that hangs. Enter should NOT commit.
        await typeAt('ta', '@bo');
        fireEvent.keyDown(screen.getByTestId('ta'), { key: 'Enter' });
        // Value should still be `@bo` — no token was inserted.
        expect(screen.getByTestId('value').textContent).toBe('@bo');
        // Resolve the hung query and confirm Bob is then committable.
        await act(async () => {
            resolveSlow(ROWS.filter((u) => u.name.toLowerCase().includes('bo')));
        });
        await waitFor(() => expect(screen.getByTestId('ta-picker-row-u_bob')).toBeTruthy());
    });
    it('drops stale responses when typing quickly (race protection)', async () => {
        let resolveSlow = () => undefined;
        const search = vi.fn((q) => {
            if (q === 'a') {
                return new Promise((resolve) => {
                    resolveSlow = resolve;
                });
            }
            // Fast path for `ali`.
            return Promise.resolve(ROWS.filter((u) => u.name.toLowerCase().includes(q.toLowerCase())));
        });
        render(_jsx(Host, { search: search }));
        // Trigger the slow request.
        await typeAt('ta', '@a');
        // Trigger the fast request which will resolve first.
        await typeAt('ta', '@ali');
        await waitFor(() => expect(screen.getByTestId('ta-picker-row-u_alice')).toBeTruthy());
        // Now resolve the older request with the wrong rows — should NOT
        // appear in the dropdown.
        await act(async () => {
            resolveSlow(ROWS);
        });
        // Carol should still be filtered out (the fast `ali` query is
        // what won the race).
        expect(screen.queryByTestId('ta-picker-row-u_carol')).toBeNull();
    });
});
//# sourceMappingURL=mentionable-textarea.test.js.map