import '@testing-library/jest-dom/extend-expect';
import React from 'react';
import { render, waitForElement, wait } from '@testing-library/react';
import { flatten } from 'lodash';

import renderWithProvider from 'lib/testing/provider';
import { createSoundRecordingStreams, createSoundRecordingDownloads } from 'lib/factories/soundRecording';
import SoundRecordingStreamsQuery from 'src/queries/soundRecordingStreams.gql';
import SoundRecordingDownloadsQuery from 'src/queries/soundRecordingDownloads.gql';
import { SoundRecordingStreams, SoundRecordingDownloads } from 'src/definitions';
import { formatNumber, formatPercentage } from 'src/utils/formatNumber';
import { HIGHWATER_MARK, PAST28, FIRST365 } from 'src/constants/periods';
import { EMPTY_CHAR } from 'src/components/metric';

import { TERM_TRIGGER_LABEL as DATASOURCES_TERM_TRIGGER } from '../../sourcesStatus/SourcesStatus';
import { TEST_ID as CHART_TEST_ID } from '../chart/Chart';
import {
    CLASSNAME as TABLE_CLASSNAME,
    METRIC_TEST_ID as TABLE_METRIC_TEST_ID
} from '../metricsTable/MetricsTable';
import {
    CLASSNAME as HERO_CLASSNAME,
    METRIC_TEST_ID as HERO_METRIC_TEST_ID
} from '../metricsHero/MetricsHero';

import PerformanceOverTime, {
    Content, EmptyState,
    TERM_EMPTY_TEXT, TERM_HEADER
} from '../PerformanceOverTime';

const zeroPad = (num:number, places:number) => String(num).padStart(places, '0');

describe('<PerformanceOverTime>', () => {
    test('shows empty state by default', async () => {
        const container = await renderWithProvider(PerformanceOverTime);

        const emptyText = container.getByText(TERM_EMPTY_TEXT);
        expect(emptyText).toBeVisible();
    });

    test('shows header by default', async () => {
        const container = await renderWithProvider(PerformanceOverTime);

        const headerTitle = container.getByText(TERM_HEADER);
        expect(headerTitle).toBeVisible();
    });

    test('does not show content by default', async () => {
        const container = await renderWithProvider(PerformanceOverTime);

        const content = container.queryByTestId('content');
        expect(content).toBeNull();
    });

    test('does not show pot-metric dropdown by default', async () => {
        const { container } = await renderWithProvider(PerformanceOverTime);

        const dropdown = container.getElementsByClassName('pot-metric-select').item(0);
        expect(dropdown).toBeNull();
    });

    test('does show empty div by default', async () => {
        const { container } = await renderWithProvider(PerformanceOverTime);

        const dropdown = container.getElementsByClassName('pot-metric-select-placeholder').item(0);
        expect(dropdown).not.toBeNull();
    });
});

describe('Content', () => {
    const ISRC1 = 'ISRCCODE1';
    const ISRC2 = 'ISRCCODE2';
    const ISRC3 = 'ISRCCODE3';
    const TOTAL1 = 1;
    const TOTAL2 = 1000;
    const TOTAL3 = 0;

    const records = (multiplier: number): Record<string, SoundRecordingStreams> => ({
        [ISRC1]: createSoundRecordingStreams({
            aggregate: {
                allTime: 0,
                skipRate: 0.5,
                items: [
                    {
                        date: '2019-01-01',
                        value: TOTAL1 * multiplier,
                        skipRate: TOTAL1 * multiplier,
                        saves: TOTAL1 * multiplier
                    }
                ]
            }
        }, ISRC1),
        [ISRC2]: createSoundRecordingStreams({
            aggregate: {
                allTime: 0,
                skipRate: 0.5,
                items: [
                    {
                        date: '2019-01-01',
                        value: TOTAL2 * multiplier,
                        skipRate: TOTAL2 * multiplier,
                        saves: TOTAL2 * multiplier
                    }
                ]
            }
        }, ISRC2),
        [ISRC3]: createSoundRecordingStreams({
            aggregate: {
                allTime: 0,
                skipRate: 0.5,
                items: [
                    {
                        date: '2019-01-01',
                        value: TOTAL3 * multiplier,
                        skipRate: TOTAL3 * multiplier,
                        saves: TOTAL3 * multiplier
                    }
                ]
            }
        }, ISRC3)
    });

    const downloads = (): Record<string, SoundRecordingDownloads> => ({
        [ISRC1]: createSoundRecordingDownloads({
            aggregate: {
                items: [
                    {
                        date: '2019-01-01',
                        value: TOTAL1,
                    }
                ]
            }
        }, ISRC1),
        [ISRC2]: createSoundRecordingDownloads({
            aggregate: {
                items: [
                    {
                        date: '2019-01-01',
                        value: TOTAL2,
                    }
                ]
            }
        }, ISRC2),
        [ISRC3]: createSoundRecordingDownloads({
            aggregate: {
                items: [
                    {
                        date: '2019-01-01',
                        value: TOTAL3,
                    }
                ]
            }
        }, ISRC3)
    });

    const renderMocked = async ({
        ids,
        countries = [],
        stores = [],
        datePeriod = PAST28,
        waitDuration,
        potMetric = 'streams',
    }: {
        ids: string[];
        countries?: string[];
        stores?: number[];
        datePeriod?: string;
        waitDuration?: number;
        potMetric?: string;
    }) => {
        const mocks = flatten(ids.map((isrc, index) => ([{
            request: {
                query: SoundRecordingStreamsQuery,
                variables: { isrc, countries, startDate: HIGHWATER_MARK, days: -28, storeIds: stores }
            },
            result: {
                data: records(1)[isrc]
            }
        }, {
            request: {
                query: SoundRecordingStreamsQuery,
                variables: { isrc, countries, startDate: `2017-02-${zeroPad(index + 1, 2)}`, days: 365, storeIds: stores }
            },
            result: {
                data: records(2)[isrc]
            }
        }, {
            request: {
                query: SoundRecordingDownloadsQuery,
                variables: { isrc, countries, startDate: HIGHWATER_MARK, days: -28, storeIds: stores }
            },
            result: {
                data: downloads()[isrc]
            }
        }])));

        const songs = ids.map((id, index) => ({
            loading: false,
            id,
            error: false,
            data: {
                isrc: id,
                name: '',
                imageLocation: '',
                primaryArtists: [{
                    artistName: '',
                    artistType: 'performer'
                }],
                products: [
                    { genreId: null, releaseDate: `2017-02-${zeroPad(index + 5, 2)}`, deletions: 'N' },
                    { genreId: null, releaseDate: `2017-02-${zeroPad(index + 1, 2)}`, deletions: 'N' },
                    { genreId: null, releaseDate: `2017-02-${zeroPad(index + 8, 2)}`, deletions: 'N' },
                ]
            }
        }));

        return renderWithProvider(() => (
            <Content
                selectedSongIds={ids}
                countryFilter={countries}
                storeFilter={stores}
                dateFilter={datePeriod}
                songs={songs}
                potMetric={potMetric}
            />
        ), { mocks, wait: waitDuration });
    };

    const getByClassName = (container: HTMLElement, className: string) => {
        const element = container.getElementsByClassName(className).item(0);
        if (!element) throw new Error(`${className} element not found`);
        return element;
    };

    describe('is loading', () => {
        test('renders loading hero metrics', async () => {
            const ids = [ISRC1, ISRC2];
            const { container } = await renderMocked({ ids, waitDuration: 0 });

            const metricsHero = getByClassName(container, HERO_CLASSNAME);
            const loaders = metricsHero.getElementsByClassName('LoaderWrapper-item');

            expect(loaders.item(0)).toBeVisible();
            expect(loaders.item(1)).toBeVisible();

            // wait so the rendering can enter loaded state
            // jest does not like state changing outside test scope
            await wait();
        });

        test('renders loading table metrics', async () => {
            const ids = [ISRC1, ISRC2];
            const { container } = await renderMocked({ ids, waitDuration: 0 });

            const metricsTable = getByClassName(container, TABLE_CLASSNAME);
            const loaders = metricsTable.getElementsByClassName('LoaderWrapper-item');

            expect(loaders.item(0)).toBeVisible();
            expect(loaders.item(1)).toBeVisible();

            // wait so the rendering can enter loaded state
            // jest does not like state changing outside test scope
            await wait();
        });
    });

    describe('is loaded', () => {
        test('renders hero metrics', async () => {
            const ids = [ISRC1, ISRC2];
            const container = await renderMocked({ ids });

            await waitForElement(() =>
                container.getByTestId(HERO_METRIC_TEST_ID(0)));

            const metric1 = container.getByTestId(HERO_METRIC_TEST_ID(0));
            const metric2 = container.getByTestId(HERO_METRIC_TEST_ID(1));

            expect(metric1).toHaveTextContent(formatNumber(TOTAL1));
            expect(metric2).toHaveTextContent(formatNumber(TOTAL2));
        });

        test('renders performance chart', async () => {
            const ids = [ISRC1, ISRC2];
            const container = await renderMocked({ ids });

            const chart = await waitForElement(() =>
                container.getByTestId(CHART_TEST_ID));

            expect(chart).toBeVisible();
        });

        test('renders table metrics', async () => {
            const ids = [ISRC1, ISRC2, ISRC3];
            const container = await renderMocked({ ids });

            await waitForElement(() =>
                container.getByTestId(TABLE_METRIC_TEST_ID(0, 'streams')));

            await waitForElement(() =>
                container.getByTestId(TABLE_METRIC_TEST_ID(0, 'skips')));

            await waitForElement(() =>
                container.getByTestId(TABLE_METRIC_TEST_ID(0, 'saves')));

            const metric1 = container.getByTestId(TABLE_METRIC_TEST_ID(0, 'streams'));
            const metric2 = container.getByTestId(TABLE_METRIC_TEST_ID(1, 'streams'));
            const metric3 = container.getByTestId(TABLE_METRIC_TEST_ID(2, 'streams'));

            const metric4 = container.getByTestId(TABLE_METRIC_TEST_ID(0, 'skips'));
            const metric5 = container.getByTestId(TABLE_METRIC_TEST_ID(1, 'skips'));
            const metric6 = container.getByTestId(TABLE_METRIC_TEST_ID(2, 'skips'));

            const metric7 = container.getByTestId(TABLE_METRIC_TEST_ID(0, 'saves'));
            const metric8 = container.getByTestId(TABLE_METRIC_TEST_ID(1, 'saves'));
            const metric9 = container.getByTestId(TABLE_METRIC_TEST_ID(2, 'skips'));

            expect(metric1).toHaveTextContent(formatNumber(TOTAL1));
            expect(metric2).toHaveTextContent(formatNumber(TOTAL2));
            expect(metric3).toHaveTextContent(EMPTY_CHAR);

            expect(metric4).toHaveTextContent(formatPercentage(TOTAL1 / (TOTAL1 * 2)));
            expect(metric5).toHaveTextContent(formatPercentage(TOTAL2 / (TOTAL2 * 2)));
            expect(metric6).toHaveTextContent(EMPTY_CHAR);

            expect(metric7).toHaveTextContent(formatNumber(TOTAL1));
            expect(metric8).toHaveTextContent(formatNumber(TOTAL2));
            expect(metric9).toHaveTextContent(EMPTY_CHAR);
        });

        test('renders table downloads metrics', async () => {
            const ids = [ISRC1, ISRC2, ISRC3];
            const container = await renderMocked({ ids, potMetric: 'downloads' });

            await waitForElement(() =>
                container.getByTestId(TABLE_METRIC_TEST_ID(0, 'downloads')));

            const metric1 = container.getByTestId(TABLE_METRIC_TEST_ID(0, 'downloads'));
            const metric2 = container.getByTestId(TABLE_METRIC_TEST_ID(1, 'downloads'));
            const metric3 = container.getByTestId(TABLE_METRIC_TEST_ID(2, 'downloads'));

            expect(metric1).toHaveTextContent(formatNumber(TOTAL1));
            expect(metric2).toHaveTextContent(formatNumber(TOTAL2));
            expect(metric3).toHaveTextContent(EMPTY_CHAR);
        });

        test('shows sources', async () => {
            const ids = [ISRC1, ISRC2];
            const container = await renderMocked({ ids });

            const sourcePopoverTrigger = container.getByText(DATASOURCES_TERM_TRIGGER);
            expect(sourcePopoverTrigger).toBeVisible();
        });

        test('fetches data based on product date', async () => {
            const ids = [ISRC1, ISRC2];
            const container = await renderMocked({ ids, datePeriod: FIRST365 });

            await waitForElement(() =>
                container.getByTestId(HERO_METRIC_TEST_ID(0)));

            const metric1 = container.getByTestId(HERO_METRIC_TEST_ID(0));
            const metric2 = container.getByTestId(HERO_METRIC_TEST_ID(1));
            expect(metric1).toHaveTextContent(formatNumber(TOTAL1 * 2));
            expect(metric2).toHaveTextContent(formatNumber(TOTAL2 * 2));
        });
    });
});

describe('EmptyState', () => {
    test('renders the empty icon', () => {
        const { container } = render(<EmptyState />);

        const icon = container.getElementsByClassName('Outline Products').item(0);
        expect(icon).toBeDefined();
    });

    test('renders the empty text', () => {
        const container = render(<EmptyState />);

        const text = container.getByText(TERM_EMPTY_TEXT);
        expect(text).toBeVisible();
    });
});
