import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { testComponent } from 'lib/test-utils/common';
import { Slider } from '../slider';
import type { Props } from '../types';

describe('<Slider>', () => {
    const defaultProps: Props = {
        initialValue: { min: 30, max: 70 },
    };

    testComponent(Slider);

    const renderSlider = (props?: Partial<Props>) =>
        render(<Slider {...defaultProps} {...props} />);

    test('applies style', () => {
        renderSlider({ style: { marginTop: 10 } });
        expect(screen.getByTestId('Slider')).toHaveStyle({ marginTop: '10px' });
    });

    describe('tooltips', () => {
        test('renders as percentage by default', () => {
            renderSlider();

            expect(screen.getByText('30')).toBeInTheDocument();
            expect(screen.getByText('70')).toBeInTheDocument();
        });

        test('accepts custom formatter', async () => {
            renderSlider({ tooltipFormatter: (val) => `Custom Tooltip ${val}` });

            expect(screen.getByText('Custom Tooltip 30')).toBeInTheDocument();
            expect(screen.getByText('Custom Tooltip 70')).toBeInTheDocument();
        });
    });

    describe('labels', () => {
        test('renders as integers by default', () => {
            renderSlider();

            expect(screen.getByText('0')).toBeInTheDocument();
            expect(screen.getByText('100')).toBeInTheDocument();
        });

        test('accepts custom formatter', async () => {
            renderSlider({ labelFormatter: (val) => `Custom Label ${val}` });

            expect(screen.getByText('Custom Label 0')).toBeInTheDocument();
            expect(screen.getByText('Custom Label 100')).toBeInTheDocument();
        });
    });

    describe('variant', () => {
        test('renders in range mode by default', () => {
            renderSlider();

            const inputs = screen.getAllByRole('slider');
            expect(inputs).toHaveLength(2);

            const [min, max] = inputs;
            expect(min).toBeVisible();
            expect(min).toHaveValue('30');
            expect(max).toBeVisible();
            expect(max).toHaveValue('70');
        });

        test('renders left slider in `min` mode', () => {
            renderSlider({ variant: 'min' });

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

            expect(input).toBeVisible();
            expect(input).toHaveValue('30');
            expect(input).toHaveClass('Slider-input-min');
        });

        test('renders right slider in `max` mode', () => {
            renderSlider({ variant: 'max' });

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

            expect(input).toBeVisible();
            expect(input).toHaveValue('70');
            expect(input).toHaveClass('Slider-input-max');
        });
    });

    describe('onInput', () => {
        test('is called when an input changes', () => {
            const onInput = vitest.fn();
            renderSlider({ onInput });

            const [min, max] = screen.getAllByRole('slider');
            fireEvent.change(min, { target: { value: 10 } });
            expect(onInput).toHaveBeenCalledWith({ min: 10, max: 70 }, { min: 10, max: 70 });
            fireEvent.change(max, { target: { value: 80 } });
            expect(onInput).toHaveBeenCalledWith({ min: 10, max: 80 }, { min: 10, max: 80 });
            expect(onInput).toHaveBeenCalledTimes(2);
        });

        test('is called with raw and indexed values', () => {
            const onInput = vitest.fn();
            renderSlider({ onInput, min: 0, max: 1000, step: 10 });

            const inputs = screen.getAllByRole('slider');
            fireEvent.change(inputs[1], { target: { value: 230 } });
            expect(onInput).toHaveBeenCalledWith({ min: 30, max: 230 }, { min: 3, max: 23 });
        });
    });

    describe('onChange', () => {
        test('is called when the pointer is released', () => {
            const onChange = vitest.fn();
            renderSlider({ onChange });

            const [min, max] = screen.getAllByRole('slider');
            fireEvent.change(min, { target: { value: 10 } });
            expect(onChange).not.toHaveBeenCalled();
            fireEvent.pointerUp(min);
            expect(onChange).toHaveBeenCalledWith({ min: 10, max: 70 }, { min: 10, max: 70 });
            fireEvent.change(max, { target: { value: 80 } });
            fireEvent.change(max, { target: { value: 60 } });
            fireEvent.pointerUp(min);
            expect(onChange).toHaveBeenCalledWith({ min: 10, max: 60 }, { min: 10, max: 60 });
            expect(onChange).toHaveBeenCalledTimes(2);
        });

        test('is called when the pointer action is cancelled', () => {
            const onChange = vitest.fn();
            renderSlider({ onChange });

            const [min, max] = screen.getAllByRole('slider');
            fireEvent.change(min, { target: { value: 10 } });
            expect(onChange).not.toHaveBeenCalled();
            fireEvent.pointerCancel(min);
            expect(onChange).toHaveBeenCalledWith({ min: 10, max: 70 }, { min: 10, max: 70 });
            fireEvent.change(max, { target: { value: 60 } });
            fireEvent.pointerCancel(max);
            expect(onChange).toHaveBeenCalledWith({ min: 10, max: 60 }, { min: 10, max: 60 });
            expect(onChange).toHaveBeenCalledTimes(2);
        });

        test('is called with raw and indexed values', () => {
            const onChange = vitest.fn();
            renderSlider({ onChange, min: 0, max: 1000, step: 10 });

            const inputs = screen.getAllByRole('slider');
            fireEvent.change(inputs[1], { target: { value: 230 } });
            fireEvent.pointerUp(inputs[1]);
            expect(onChange).toHaveBeenCalledWith({ min: 30, max: 230 }, { min: 3, max: 23 });
        });
    });

    describe('gap', () => {
        test('limits how close min/max can be', () => {
            renderSlider({ gap: 20 });
            const [min, max] = screen.getAllByRole('slider');

            fireEvent.change(min, { target: { value: 50 } });
            expect(min).toHaveValue('50');
            expect(max).toHaveValue('70');

            fireEvent.change(min, { target: { value: 51 } });
            expect(min).toHaveValue('50');
            expect(max).toHaveValue('70');

            fireEvent.change(min, { target: { value: 71 } });
            expect(min).toHaveValue('50');
            expect(max).toHaveValue('70');
        });
    });

    describe('initialValue', () => {
        test('sets uncontrolled starting value', () => {
            renderSlider({ initialValue: { min: 2, max: 99 } });
            const [min, max] = screen.getAllByRole('slider');
            expect(min).toHaveValue('2');
            expect(max).toHaveValue('99');

            fireEvent.change(min, { target: { value: 1 } });
            expect(min).toHaveValue('1');
            expect(max).toHaveValue('99');
        });
    });

    describe('value', () => {
        test('sets initial value', () => {
            renderSlider({ value: { min: 6, max: 7 } });
            const [min, max] = screen.getAllByRole('slider');
            expect(min).toHaveValue('6');
            expect(max).toHaveValue('7');
        });

        test('can be updated to control the inputs', () => {
            const { rerender } = renderSlider({ value: { min: 6, max: 7 } });
            const [min, max] = screen.getAllByRole('slider');
            rerender(<Slider {...defaultProps} value={{ min: 81, max: 93 }} />);
            expect(min).toHaveValue('81');
            expect(max).toHaveValue('93');
        });
    });

    describe('step', () => {
        test('is set on inputs', () => {
            renderSlider({ step: 10 });
            const [min, max] = screen.getAllByRole('slider');
            expect(min).toHaveAttribute('step', '10');
            expect(max).toHaveAttribute('step', '10');
        });
    });

    describe('min/max', () => {
        test('is set on inptus', () => {
            renderSlider({ min: 5, max: 95 });
            const [min, max] = screen.getAllByRole('slider');
            expect(min).toHaveAttribute('min', '5');
            expect(min).toHaveAttribute('max', '95');
            expect(max).toHaveAttribute('min', '5');
            expect(max).toHaveAttribute('max', '95');

            expect(screen.getByText('5')).toBeInTheDocument();
            expect(screen.getByText('95')).toBeInTheDocument();
        });
    });

    describe('active state', () => {
        test('hovering sliders sets hover class', () => {
            renderSlider();
            const slider = screen.getByTestId('Slider');
            const [min, max] = screen.getAllByRole('slider');

            fireEvent.pointerEnter(min);
            expect(slider).toHaveClass('Slider-min-hovered');
            expect(slider).not.toHaveClass('Slider-max-hovered');

            fireEvent.pointerLeave(min);
            expect(slider).not.toHaveClass('Slider-min-hovered');
            expect(slider).not.toHaveClass('Slider-max-hovered');

            fireEvent.pointerEnter(max);
            expect(slider).not.toHaveClass('Slider-min-hovered');
            expect(slider).toHaveClass('Slider-max-hovered');
        });

        test('hovering slider sets active state which persists', () => {
            renderSlider();
            const slider = screen.getByTestId('Slider');
            const [min, max] = screen.getAllByRole('slider');

            fireEvent.pointerEnter(min);
            expect(slider).toHaveClass('Slider-min-active');
            expect(slider).not.toHaveClass('Slider-max-active');

            fireEvent.pointerLeave(min);
            expect(slider).toHaveClass('Slider-min-active');
            expect(slider).not.toHaveClass('Slider-max-active');

            fireEvent.pointerEnter(max);
            expect(slider).not.toHaveClass('Slider-min-active');
            expect(slider).toHaveClass('Slider-max-active');

            fireEvent.pointerLeave(max);
            expect(slider).not.toHaveClass('Slider-min-active');
            expect(slider).toHaveClass('Slider-max-active');
        });

        test('hovering visible thumbs sets active state', () => {
            renderSlider();
            const slider = screen.getByTestId('Slider');
            const min = screen.getByTestId('Slider-thumb-min');
            const max = screen.getByTestId('Slider-thumb-max');

            fireEvent.pointerEnter(min);
            expect(slider).toHaveClass('Slider-min-active');
            expect(slider).not.toHaveClass('Slider-max-active');

            fireEvent.pointerLeave(min);
            expect(slider).toHaveClass('Slider-min-active');
            expect(slider).not.toHaveClass('Slider-max-active');

            fireEvent.pointerEnter(max);
            expect(slider).not.toHaveClass('Slider-min-active');
            expect(slider).toHaveClass('Slider-max-active');

            fireEvent.pointerLeave(max);
            expect(slider).not.toHaveClass('Slider-min-active');
            expect(slider).toHaveClass('Slider-max-active');
        });
    });

    describe('tooltip overlap', () => {
        test('min tooltip overlay gets zidx-2 only when min is hovered', () => {
            const { container } = renderSlider();
            const [minOverlay, maxOverlay] = Array.from(
                container.querySelectorAll('.TooltipOverlay')
            );
            const [min] = screen.getAllByRole('slider');

            // At rest, no zidx-2 on any (max on top strictly by DOM order)
            expect(minOverlay).not.toHaveClass('zidx-2');
            expect(maxOverlay).not.toHaveClass('zidx-2');

            // When min is hovered, min gets zidx-2, max does not
            fireEvent.pointerEnter(min);
            expect(minOverlay).toHaveClass('zidx-2');
            expect(maxOverlay).not.toHaveClass('zidx-2');

            // When hover leaves, min loses zidx-2, max still does not have it
            fireEvent.pointerLeave(min);
            expect(minOverlay).not.toHaveClass('zidx-2');
            expect(maxOverlay).not.toHaveClass('zidx-2');
        });
    });
});
