/* @vitest-environment jsdom */

import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { useState } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { NumberInput } from './NumberInput';

const Demo = ({ onValue, min, max, allowOutOfRange }: { onValue?: (value: number) => void; min?: number; max?: number; allowOutOfRange?: boolean }) => {
    const [value, setValue] = useState(5);

    return (
        <NumberInput
            allowOutOfRange={allowOutOfRange}
            aria-label="Amount"
            className="form-control"
            max={max}
            min={min}
            value={value}
            onChange={(event) => {
                const next = Number.isNaN(event.target.valueAsNumber) ? 0 : event.target.valueAsNumber;

                setValue(next);
                onValue?.(next);
            }}
        />
    );
};

describe('NumberInput', () => {
    afterEach(() => cleanup());

    it('renders a spinbutton with the value and the octane class', () => {
        render(<Demo />);

        const input = screen.getByRole('spinbutton', { name: 'Amount' }) as HTMLInputElement;

        expect(input.value).toBe('5');
        expect(input.classList.contains('octane-number-input')).toBe(true);
        expect(input.classList.contains('form-control')).toBe(true);
    });

    it('reports a native-shaped change event while typing', () => {
        const onValue = vi.fn();

        render(<Demo onValue={onValue} />);
        fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '42' } });

        expect(onValue).toHaveBeenLastCalledWith(42);
    });

    it('keeps decimals and does not group digits', () => {
        render(<NumberInput aria-label="Ratio" step={0.01} value={1234.567} onChange={() => undefined} />);

        const expected = new Intl.NumberFormat(undefined, { useGrouping: false, maximumFractionDigits: 20 }).format(1234.567);

        expect((screen.getByRole('spinbutton') as HTMLInputElement).value).toBe(expected);
    });

    it('reports NaN when the field is cleared', () => {
        const onChange = vi.fn();

        render(<NumberInput aria-label="Empty" value={7} onChange={onChange} />);
        fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '' } });

        expect(Number.isNaN(onChange.mock.calls[0][0].target.valueAsNumber)).toBe(true);
        expect(onChange.mock.calls[0][0].target.value).toBe('');
    });

    it('steps with the arrow keys and respects max', () => {
        const onValue = vi.fn();

        render(<Demo max={6} onValue={onValue} />);

        const input = screen.getByRole('spinbutton');

        fireEvent.keyDown(input, { key: 'ArrowUp' });
        expect(onValue).toHaveBeenLastCalledWith(6);

        fireEvent.keyDown(input, { key: 'ArrowUp' });
        expect(onValue).toHaveBeenLastCalledWith(6);
    });

    it('lets typed text leave the range with allowOutOfRange, while the arrows stay inside it', () => {
        const onValue = vi.fn();

        render(<Demo allowOutOfRange max={6} min={4} onValue={onValue} />);

        const input = screen.getByRole('spinbutton');

        expect(input).toHaveAttribute('aria-valuemin', '4');
        expect(input).toHaveAttribute('aria-valuemax', '6');

        fireEvent.change(input, { target: { value: '2' } });
        expect(onValue).toHaveBeenLastCalledWith(2);

        fireEvent.keyDown(input, { key: 'ArrowDown' });
        expect(onValue).toHaveBeenLastCalledWith(4);
    });

    it('shows up / down buttons on hover and steps with them', () => {
        const onValue = vi.fn();

        render(<Demo onValue={onValue} max={7} />);

        expect(screen.queryByRole('button', { name: /increase/i })).toBeNull();

        fireEvent.mouseEnter(screen.getByRole('spinbutton'));

        const up = screen.getByRole('button', { name: /increase/i });
        const down = screen.getByRole('button', { name: /decrease/i });

        fireEvent.click(up);
        expect(onValue).toHaveBeenLastCalledWith(6);

        fireEvent.click(down);
        expect(onValue).toHaveBeenLastCalledWith(5);
    });

    it('shows the buttons while focused and never when disabled or read-only', () => {
        const { rerender } = render(<NumberInput aria-label="Focus" value={1} onChange={() => undefined} />);

        fireEvent.focus(screen.getByRole('spinbutton'));
        expect(screen.getByRole('button', { name: /increase/i })).toBeTruthy();

        fireEvent.blur(screen.getByRole('spinbutton'));
        expect(screen.queryByRole('button', { name: /increase/i })).toBeNull();

        rerender(<NumberInput disabled aria-label="Focus" value={1} onChange={() => undefined} />);
        fireEvent.mouseEnter(screen.getByRole('spinbutton'));
        expect(screen.queryByRole('button', { name: /increase/i })).toBeNull();
    });

    it('is disabled and read-only on request', () => {
        render(<NumberInput disabled aria-label="Off" value={1} onChange={() => undefined} />);

        expect((screen.getByRole('spinbutton') as HTMLInputElement).disabled).toBe(true);
    });
});
