import React from 'react';
import { render, screen, act } from '@testing-library/react';
import SpotifySelect from '..';

const SPOTIFY_URL =
    'https://open.spotify.com/artist/5JWBow4ywgKNQ5HBxY8hcz?si=omNk-4uMR8KEOFUJKOLQRw';
const ALBUM_URL = 'https://open.spotify.com/album/4aawyAB9vmqN3uQ7FjRGTy';
const PLAYLIST_URL = 'https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M';
const TRACK_URL = 'https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P';
const BARE_ID = '5JWBow4ywgKNQ5HBxY8hcz';

// Capture props that SpotifySelect passes to MultiSelect
let capturedOnLoadOptions: ((term?: string) => Promise<unknown>) | undefined;
let capturedOnChange:
    | ((value: { label: string; value: string }[]) => void)
    | undefined;
let capturedOnOpen: (() => void) | undefined;

jest.mock('@theorchard/suite-components', () => ({
    MultiSelect: (props: {
        onLoadOptions?: (term?: string) => Promise<unknown>;
        onChange?: (value: { label: string; value: string }[]) => void;
        onOpen?: () => void;
    }) => {
        capturedOnLoadOptions = props.onLoadOptions;
        capturedOnChange = props.onChange;
        capturedOnOpen = props.onOpen;
        return <div data-testid="MockMultiSelect" />;
    },
}));

/**
 * Simulate a native input event on a filter input element matching the selector
 * the component listens for on document. The filter input only exists while the
 * dropdown is open, so open it first — that is also when capture is enabled.
 */
function simulateFilterInput(value: string) {
    capturedOnOpen?.();
    const input = document.createElement('input');
    input.setAttribute('data-testid', 'SuiteListViewFilterInput');
    document.body.appendChild(input);
    input.value = value;
    input.dispatchEvent(new Event('input', { bubbles: true }));
    document.body.removeChild(input);
}

describe('SpotifySelect', () => {
    const defaultProps = {
        lookup: jest
            .fn()
            .mockResolvedValue({ name: 'Artist', url: SPOTIFY_URL }),
        onChange: jest.fn(),
        expectedType: 'artist' as const,
        invalidTypeMessage: (pastedType: string) => `wrong type: ${pastedType}`,
        invalidLinkMessage: () => 'not a valid spotify link',
    };

    beforeEach(() => {
        capturedOnLoadOptions = undefined;
        capturedOnChange = undefined;
        capturedOnOpen = undefined;
        jest.clearAllMocks();
    });

    it('renders the MultiSelect component', () => {
        render(<SpotifySelect {...defaultProps} />);
        expect(screen.getByTestId('MockMultiSelect')).toBeInTheDocument();
    });

    it('passes a wrapped onLoadOptions to MultiSelect', () => {
        render(<SpotifySelect {...defaultProps} />);
        expect(capturedOnLoadOptions).toBeDefined();
    });

    it('calls lookup with the case-preserved input value', async () => {
        const lookup = jest
            .fn()
            .mockResolvedValue({ name: 'Artist Name', url: SPOTIFY_URL });
        render(<SpotifySelect {...defaultProps} lookup={lookup} />);

        simulateFilterInput(SPOTIFY_URL);

        const result = await act(() => capturedOnLoadOptions!());

        expect(lookup).toHaveBeenCalledWith(SPOTIFY_URL);
        expect(result).toEqual({
            data: [{ label: 'Artist Name', value: SPOTIFY_URL }],
        });
    });

    it('returns empty data when lookup returns null', async () => {
        const lookup = jest.fn().mockResolvedValue(null);
        render(<SpotifySelect {...defaultProps} lookup={lookup} />);

        simulateFilterInput(SPOTIFY_URL);

        const result = await act(() => capturedOnLoadOptions!());

        expect(lookup).toHaveBeenCalledWith(SPOTIFY_URL);
        expect(result).toEqual({ data: [] });
    });

    it('returns empty data when no input has been entered', async () => {
        const lookup = jest.fn();
        render(<SpotifySelect {...defaultProps} lookup={lookup} />);

        const result = await act(() => capturedOnLoadOptions!());

        expect(lookup).not.toHaveBeenCalled();
        expect(result).toEqual({ data: [] });
    });

    it('trims whitespace from the captured input', async () => {
        const lookup = jest
            .fn()
            .mockResolvedValue({ name: 'Artist', url: SPOTIFY_URL });
        render(<SpotifySelect {...defaultProps} lookup={lookup} />);

        simulateFilterInput(`  ${SPOTIFY_URL}  `);

        await act(() => capturedOnLoadOptions!());

        expect(lookup).toHaveBeenCalledWith(SPOTIFY_URL);
    });

    it('returns empty data for whitespace-only input', async () => {
        const lookup = jest.fn();
        render(<SpotifySelect {...defaultProps} lookup={lookup} />);

        simulateFilterInput('   ');

        const result = await act(() => capturedOnLoadOptions!());

        expect(lookup).not.toHaveBeenCalled();
        expect(result).toEqual({ data: [] });
    });

    it('maps onChange values from label/value to name/url', () => {
        const onChange = jest.fn();
        render(<SpotifySelect {...defaultProps} onChange={onChange} />);

        capturedOnChange!([
            { label: 'Artist One', value: 'https://spotify.com/1' },
            { label: 'Artist Two', value: 'https://spotify.com/2' },
        ]);

        expect(onChange).toHaveBeenCalledWith([
            { name: 'Artist One', url: 'https://spotify.com/1' },
            { name: 'Artist Two', url: 'https://spotify.com/2' },
        ]);
    });

    it('cleans up the document event listener on unmount', () => {
        const removeSpy = jest.spyOn(document, 'removeEventListener');

        const { unmount } = render(<SpotifySelect {...defaultProps} />);

        unmount();

        expect(removeSpy).toHaveBeenCalledWith(
            'input',
            expect.any(Function),
            true
        );
        removeSpy.mockRestore();
    });

    describe('wrong-type link validation in onLoadOptions', () => {
        it('throws the invalidTypeMessage when a playlist link is pasted into an album field', async () => {
            const lookup = jest.fn();
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="album"
                    invalidTypeMessage={type => `paste ${type} elsewhere`}
                />
            );

            simulateFilterInput(PLAYLIST_URL);

            await expect(act(() => capturedOnLoadOptions!())).rejects.toThrow(
                'paste playlist elsewhere'
            );
            expect(lookup).not.toHaveBeenCalled();
        });

        it('throws the invalidTypeMessage when a track link is pasted into an artist field', async () => {
            const lookup = jest.fn();
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="artist"
                    invalidTypeMessage={type => `paste ${type} elsewhere`}
                />
            );

            simulateFilterInput(TRACK_URL);

            await expect(act(() => capturedOnLoadOptions!())).rejects.toThrow(
                'paste track elsewhere'
            );
            expect(lookup).not.toHaveBeenCalled();
        });

        it('does not throw for a matching album link in an album field', async () => {
            const lookup = jest
                .fn()
                .mockResolvedValue({ name: 'Album', url: ALBUM_URL });
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="album"
                />
            );

            simulateFilterInput(ALBUM_URL);

            const result = await act(() => capturedOnLoadOptions!());

            expect(lookup).toHaveBeenCalledWith(ALBUM_URL);
            expect(result).toEqual({
                data: [{ label: 'Album', value: ALBUM_URL }],
            });
        });

        it('throws using the MultiSelect-provided term when rawInputRef is empty', async () => {
            const lookup = jest.fn();
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="artist"
                    invalidTypeMessage={type => `paste ${type} elsewhere`}
                />
            );

            // Do NOT call simulateFilterInput — rawInputRef stays empty.
            // MultiSelect passes its own (lowercased) term as first arg.
            await expect(
                act(() => capturedOnLoadOptions!(PLAYLIST_URL.toLowerCase()))
            ).rejects.toThrow('paste playlist elsewhere');
            expect(lookup).not.toHaveBeenCalled();
        });

        it('does not throw for a bare Spotify ID (passes through to lookup)', async () => {
            const lookup = jest
                .fn()
                .mockResolvedValue({ name: 'Artist', url: SPOTIFY_URL });
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="artist"
                />
            );

            simulateFilterInput(BARE_ID);

            await act(() => capturedOnLoadOptions!());

            expect(lookup).toHaveBeenCalledWith(BARE_ID);
        });

        it('throws the invalidLinkMessage for a non-Spotify URL', async () => {
            const lookup = jest.fn();
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="artist"
                    invalidLinkMessage={() => 'not a valid spotify link'}
                />
            );

            simulateFilterInput(
                'http://localhost:8080/create/digital-audio/bulk-getting-started'
            );

            await expect(act(() => capturedOnLoadOptions!())).rejects.toThrow(
                'not a valid spotify link'
            );
            expect(lookup).not.toHaveBeenCalled();
        });

        it('throws the invalidLinkMessage for an https non-Spotify URL', async () => {
            const lookup = jest.fn();
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="album"
                    invalidLinkMessage={() => 'not a valid spotify link'}
                />
            );

            simulateFilterInput('https://example.com/foo');

            await expect(act(() => capturedOnLoadOptions!())).rejects.toThrow(
                'not a valid spotify link'
            );
            expect(lookup).not.toHaveBeenCalled();
        });

        it('throws the invalidLinkMessage for random text that is not a valid ID', async () => {
            const lookup = jest.fn();
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="artist"
                    invalidLinkMessage={() => 'not a valid spotify link'}
                />
            );

            simulateFilterInput('not-an-id');

            await expect(act(() => capturedOnLoadOptions!())).rejects.toThrow(
                'not a valid spotify link'
            );
            expect(lookup).not.toHaveBeenCalled();
        });

        it('throws the invalidLinkMessage for an ID that is the wrong length', async () => {
            const lookup = jest.fn();
            render(
                <SpotifySelect
                    {...defaultProps}
                    lookup={lookup}
                    expectedType="artist"
                    invalidLinkMessage={() => 'not a valid spotify link'}
                />
            );

            // 10 chars: too short to be a spotify id
            simulateFilterInput('abc1234567');

            await expect(act(() => capturedOnLoadOptions!())).rejects.toThrow(
                'not a valid spotify link'
            );
            expect(lookup).not.toHaveBeenCalled();
        });
    });

    it('ignores input while its dropdown is closed, so a value typed in another field never searches here', async () => {
        const lookup = jest.fn();
        render(
            <SpotifySelect
                {...defaultProps}
                lookup={lookup}
                expectedType="album"
            />
        );

        // A value typed into another field's open dropdown reaches the shared
        // document listener while THIS dropdown is closed. The open-gate must
        // ignore it, so nothing is looked up here.
        const input = document.createElement('input');
        input.setAttribute('data-testid', 'SuiteListViewFilterInput');
        document.body.appendChild(input);
        input.value = ALBUM_URL;
        input.dispatchEvent(new Event('input', { bubbles: true }));
        document.body.removeChild(input);

        const result = await act(() => capturedOnLoadOptions!());

        expect(result).toEqual({ data: [] });
        expect(lookup).not.toHaveBeenCalled();
    });
});
