import React from 'react';
import { screen } from '@testing-library/react';
import { Segment } from '@theorchard/suite-frontend';
import { renderInAppContext } from '@theorchard/suite-testing';
import { userEvent } from 'lib/test/helpers';
import { FEATURE_FLAGS } from 'src/constants';
import * as createGsrFromSpotify from 'src/data/mutations/createGlobalSoundRecordingFromSpotify/createGlobalSoundRecordingFromSpotify';
import * as globalSoundRecordingSearch from 'src/data/queries/globalSoundRecordingSearch/globalSoundRecordingSearch';
import * as spotifyTrackSearch from 'src/data/queries/spotifyTrackSearch/spotifyTrackSearch';
import * as trackSearch from 'src/data/queries/trackSearch/trackSearch';
import AssociatedSoundRecordings from '../associatedSoundRecordings';
import type { PublishingGlobalSoundRecordingFromSpotifyMutation as SpotifyGSR } from 'src/data/mutations/createGlobalSoundRecordingFromSpotify/__generated__/createGlobalSoundRecordingFromSpotify';
import type { AssociatedSoundRecording as AssociatedSoundRecordingType } from 'src/types';

const SEARCH_PLACEHOLDER = 'Search by track name';
const RESULT_ROW = '.NewSong-sound-recordings-search-row';

const owsTrack = (isrc: string, trackName: string) => ({
    analytics: { streams: { aggregate: { allTime: null } } },
    isrc,
    labelSoundRecording: {
        id: `lsr-${isrc}`,
        globalSoundRecording: { id: `gsr-${isrc}` },
    },
    participations: [
        { participant: { name: 'Kriss Norman' }, participatedAs: 'performer' },
    ],
    product: {
        imageLocation: '',
        productName: trackName,
        releaseDate: '2016-06-10',
        upc: '190374887809',
        status: 'submitted',
    },
    trackName,
});

const spotifyTrack = (id: string, isrc: string, name: string) => ({
    id,
    name,
    externalIds: { isrc },
    artists: [{ id: `artist-${id}`, name: 'Spotty Name' }],
    album: {
        id: `album-${id}`,
        images: [{ width: 20, height: 20, url: 'goodurl' }],
    },
});

const OWS_TRACKS = [
    owsTrack('OWS001', 'Track One'),
    owsTrack('OWS002', 'Track Two'),
];

const SPOTIFY_TRACKS = [
    spotifyTrack('spotify-1', 'SPOT001', 'Track Three'),
    spotifyTrack('spotify-2', 'SPOT002', 'Track Four'),
];

const SPOTIFY_ISRCS: Record<string, string> = {
    'spotify-1': 'SPOT001',
    'spotify-2': 'SPOT002',
};

describe('<AssociatedSoundRecordings>', () => {
    let spotifyMutation: jest.Mock;

    const Harness: React.FC = () => {
        const [associatedSounds, setAssociatedSounds] = React.useState<
            AssociatedSoundRecordingType[]
        >([]);

        return (
            <>
                <AssociatedSoundRecordings
                    associatedSounds={associatedSounds}
                    setAssociatedSounds={setAssociatedSounds}
                    debounceTime={0}
                    category="New Song"
                />
                <div data-testid="associated-isrcs">
                    {associatedSounds.map(sound => sound.isrc).join('|')}
                </div>
            </>
        );
    };

    const renderComponent = (multiSelectEnabled = false) =>
        renderInAppContext(<Harness />, {
            identity: {
                features: {
                    [FEATURE_FLAGS.ADMIN_UX_IMPROVEMENTS]: multiSelectEnabled,
                },
            },
        } as Parameters<typeof renderInAppContext>[1]);

    const search = async () =>
        userEvent.type(
            screen.getByPlaceholderText(SEARCH_PLACEHOLDER),
            'track'
        );

    const resultRows = (container: HTMLElement) =>
        Array.from(container.querySelectorAll(RESULT_ROW));

    const resultIsrcs = (container: HTMLElement) =>
        resultRows(container).map(row => row.lastElementChild?.textContent);

    const clickResult = async (container: HTMLElement, isrc: string) => {
        const row = resultRows(container).find(r =>
            r.textContent?.includes(isrc)
        );
        if (!row) throw new Error(`No result row for ${isrc}`);

        await userEvent.click(row);
    };

    // the ISRCs that ended up associated with the song
    const associatedIsrcs = () =>
        screen.getByTestId('associated-isrcs').textContent?.split('|') ?? [];

    beforeEach(() => {
        spotifyMutation = jest.fn();

        jest.spyOn(trackSearch, 'useLazyTrackSearch').mockReturnValue([
            jest.fn(),
            {
                loading: false,
                called: true,
                error: undefined,
                songs: OWS_TRACKS,
            },
        ]);
        jest.spyOn(
            globalSoundRecordingSearch,
            'useLazyGlobalSoundRecordingSearch'
        ).mockReturnValue([
            jest.fn(),
            { loading: false, called: true, error: undefined, songs: [] },
        ]);
        jest.spyOn(
            spotifyTrackSearch,
            'useLazySpotifyTrackSearch'
        ).mockReturnValue([
            jest.fn(),
            {
                loading: false,
                called: true,
                error: undefined,
                data: SPOTIFY_TRACKS,
            },
        ]);

        // every Spotify mutation completes on its own, the way Apollo does it
        jest.spyOn(
            createGsrFromSpotify,
            'useCreateGlobalSoundRecordingFromSpotify'
        ).mockImplementation((onCompleted: (data: SpotifyGSR) => void) => {
            const mutation = ({ variables }: { variables: { id: string } }) => {
                spotifyMutation(variables.id);
                onCompleted({
                    createGlobalSoundRecordingFromSpotify: {
                        id: `gsr-${variables.id}`,
                        isrc: SPOTIFY_ISRCS[variables.id],
                        name: 'Spotify Track',
                        globalParticipants: [{ name: 'Spotty Name' }],
                    },
                } as SpotifyGSR);
            };

            return mutation as unknown as ReturnType<
                typeof createGsrFromSpotify.useCreateGlobalSoundRecordingFromSpotify
            >;
        });

        jest.spyOn(Segment, 'trackEvent');
    });

    afterEach(() => {
        jest.restoreAllMocks();
    });

    describe('with the admin ux improvements flag off', () => {
        test('adds a single recording on click and clears the search term', async () => {
            const { container } = renderComponent();

            await search();
            await clickResult(container, 'OWS001');

            expect(associatedIsrcs()).toEqual(['OWS001']);
            expect(screen.getByPlaceholderText(SEARCH_PLACEHOLDER)).toHaveValue(
                ''
            );
            expect(Segment.trackEvent).toHaveBeenCalledTimes(1);
        });

        test('creates a global sound recording when a Spotify result is clicked', async () => {
            const { container } = renderComponent();

            await search();
            await clickResult(container, 'SPOT001');

            expect(spotifyMutation).toHaveBeenCalledTimes(1);
            expect(spotifyMutation).toHaveBeenCalledWith('spotify-1');
            expect(associatedIsrcs()).toEqual(['SPOT001']);
        });

        test('does not render any selection controls', async () => {
            const { container } = renderComponent();

            await search();

            expect(
                container.querySelectorAll(
                    `${RESULT_ROW} input[type="checkbox"]`
                )
            ).toHaveLength(0);
            expect(screen.queryByText(/Add Selected/)).toBeNull();
            expect(screen.queryByText('Select All')).toBeNull();
        });
    });

    describe('with the admin ux improvements flag on', () => {
        test('adds several selected recordings at once and keeps the search term', async () => {
            const { container } = renderComponent(true);

            await search();
            await clickResult(container, 'OWS001');
            await clickResult(container, 'OWS002');
            await userEvent.click(screen.getByText('Add Selected (2)'));

            expect(associatedIsrcs()).toEqual(['OWS001', 'OWS002']);
            expect(screen.getByPlaceholderText(SEARCH_PLACEHOLDER)).toHaveValue(
                'track'
            );
            expect(Segment.trackEvent).toHaveBeenCalledTimes(2);
        });

        test('creates a global sound recording for every selected Spotify result', async () => {
            const { container } = renderComponent(true);

            await search();
            await clickResult(container, 'SPOT001');
            await clickResult(container, 'SPOT002');
            await userEvent.click(screen.getByText('Add Selected (2)'));

            expect(spotifyMutation).toHaveBeenCalledTimes(2);
            expect(spotifyMutation).toHaveBeenCalledWith('spotify-1');
            expect(spotifyMutation).toHaveBeenCalledWith('spotify-2');
            // neither mutation result may overwrite the other
            expect(associatedIsrcs()).toEqual(['SPOT001', 'SPOT002']);
        });

        test('adds a mix of Spotify and non-Spotify results in one action', async () => {
            const { container } = renderComponent(true);

            await search();
            await clickResult(container, 'OWS001');
            await clickResult(container, 'SPOT002');
            await clickResult(container, 'OWS002');
            await userEvent.click(screen.getByText('Add Selected (3)'));

            expect(spotifyMutation).toHaveBeenCalledTimes(1);
            expect(spotifyMutation).toHaveBeenCalledWith('spotify-2');
            expect(associatedIsrcs().sort()).toEqual([
                'OWS001',
                'OWS002',
                'SPOT002',
            ]);
        });

        test('select all selects every result', async () => {
            renderComponent(true);

            await search();
            await userEvent.click(screen.getByText('Select All'));
            await userEvent.click(screen.getByText('Add Selected (4)'));

            expect(associatedIsrcs().sort()).toEqual([
                'OWS001',
                'OWS002',
                'SPOT001',
                'SPOT002',
            ]);
        });

        test('a selected recording can be unselected', async () => {
            const { container } = renderComponent(true);

            await search();
            await clickResult(container, 'OWS001');
            await clickResult(container, 'OWS002');
            await clickResult(container, 'OWS001');

            expect(screen.getByText('Add Selected (1)')).toBeTruthy();

            await userEvent.click(screen.getByText('Add Selected (1)'));

            expect(associatedIsrcs()).toEqual(['OWS002']);
        });

        test('drops already associated recordings from the results', async () => {
            const { container } = renderComponent(true);

            await search();
            await clickResult(container, 'OWS001');
            await clickResult(container, 'SPOT001');
            await userEvent.click(screen.getByText('Add Selected (2)'));

            expect(associatedIsrcs().sort()).toEqual(['OWS001', 'SPOT001']);
            // the term is kept, so what is left of the results stays on screen
            expect(resultIsrcs(container)).toEqual(['OWS002', 'SPOT002']);
        });
    });
});
