import {
    addXtreamPortal,
    channelItemByTitle,
    clickCategoryByNameExact,
    closeElectronApp,
    expect,
    launchElectronApp,
    openWorkspaceSection,
    resetMockServers,
    test,
    waitForXtreamWorkspaceReady,
} from './electron-test-fixtures';
import {
    fetchXtreamEpgFixture,
    XtreamNormalizedEpgListing,
} from './portal-mock-fixtures';

const epgPortalName = 'Xtream EPG Fixture';
const epgCredentials = {
    username: 'epg',
    password: 'epg',
};

for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
    test(`renders Xtream EPG previews and selected-channel schedule in ${timeZone}`, async ({
        dataDir,
        request,
    }) => {
        await resetMockServers(request, ['xtream']);
        const fixture = await fetchXtreamEpgFixture(request, epgCredentials);
        const currentProgram = fixture.shortEpg[0];
        if (!currentProgram) {
            throw new Error(
                'Expected the Xtream EPG fixture to include a current program.'
            );
        }
        const app = await launchElectronApp(dataDir, {
            env: { TZ: timeZone },
        });

        try {
            await addXtreamPortal(app.mainWindow, {
                name: `${epgPortalName} ${timeZone}`,
                username: epgCredentials.username,
                password: epgCredentials.password,
            });
            await waitForXtreamWorkspaceReady(app.mainWindow);
            await openWorkspaceSection(app.mainWindow, 'Live TV');
            await clickCategoryByNameExact(
                app.mainWindow,
                fixture.categoryName
            );

            const channelRow = channelItemByTitle(
                app.mainWindow,
                fixture.stream.name ?? ''
            ).first();
            await expect(channelRow).toBeVisible({ timeout: 20000 });

            await expect
                .poll(async () =>
                    (
                        (await channelRow
                            .locator('.epg-title')
                            .textContent()) ?? ''
                    ).trim()
                )
                .toBe(currentProgram.title);
            await expect
                .poll(async () => {
                    return (
                        await channelRow.locator('.epg-time').allInnerTexts()
                    ).map((value) => value.trim());
                })
                .toEqual([
                    formatTimeInZone(currentProgram.startTimestamp, timeZone),
                    formatTimeInZone(currentProgram.stopTimestamp, timeZone),
                ]);
            await expect
                .poll(async () => getProgressWidthPercent(channelRow))
                .toBeGreaterThan(0);

            await channelRow.click();
            await expect(app.mainWindow.locator('app-epg-list')).toBeVisible({
                timeout: 20000,
            });

            const currentDateKey = formatDateKeyInZone(Date.now(), timeZone);
            const currentDayTitles = titlesForDate(
                fixture.fullEpg,
                currentDateKey,
                timeZone
            );

            await expect
                .poll(() => visibleProgramTitles(app.mainWindow))
                .toEqual(currentDayTitles);

            await expectCurrentProgram(
                app.mainWindow,
                currentProgram,
                timeZone
            );

            const firstFutureDateKey = getFirstFutureDateKey(
                fixture.fullEpg,
                currentDateKey,
                timeZone
            );
            expect(firstFutureDateKey).not.toBeNull();

            const futureDayTitles = titlesForDate(
                fixture.fullEpg,
                firstFutureDateKey!,
                timeZone
            );
            const nextDayClicks = dayDifference(
                currentDateKey,
                firstFutureDateKey!
            );

            for (let index = 0; index < nextDayClicks; index += 1) {
                await app.mainWindow
                    .locator(
                        'app-live-epg-panel .next-day, app-epg-list .next-day'
                    )
                    .click();
            }

            await expect
                .poll(() => visibleProgramTitles(app.mainWindow))
                .toEqual(futureDayTitles);
        } finally {
            await closeElectronApp(app);
        }
    });
}

function formatTimeInZone(timestampSeconds: number, timeZone: string): string {
    return new Intl.DateTimeFormat('en-US', {
        timeZone,
        hour: '2-digit',
        minute: '2-digit',
        hour12: false,
    }).format(new Date(timestampSeconds * 1000));
}

function formatDateKeyInZone(
    timestampMilliseconds: number,
    timeZone: string
): string {
    const parts = new Intl.DateTimeFormat('en-CA', {
        timeZone,
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
    }).formatToParts(new Date(timestampMilliseconds));

    const year = parts.find((part) => part.type === 'year')?.value;
    const month = parts.find((part) => part.type === 'month')?.value;
    const day = parts.find((part) => part.type === 'day')?.value;

    if (!year || !month || !day) {
        throw new Error(
            `Failed to format a date key for timezone ${timeZone}.`
        );
    }

    return `${year}-${month}-${day}`;
}

function titlesForDate(
    listings: XtreamNormalizedEpgListing[],
    dateKey: string,
    timeZone: string
): string[] {
    return listings
        .filter(
            (listing) =>
                formatDateKeyInZone(listing.startTimestamp * 1000, timeZone) ===
                dateKey
        )
        .map((listing) => listing.title);
}

function getFirstFutureDateKey(
    listings: XtreamNormalizedEpgListing[],
    currentDateKey: string,
    timeZone: string
): string | null {
    const uniqueDateKeys = Array.from(
        new Set(
            listings.map((listing) =>
                formatDateKeyInZone(listing.startTimestamp * 1000, timeZone)
            )
        )
    ).sort();

    return uniqueDateKeys.find((dateKey) => dateKey > currentDateKey) ?? null;
}

function dayDifference(fromDateKey: string, toDateKey: string): number {
    const from = Date.parse(`${fromDateKey}T00:00:00.000Z`);
    const to = Date.parse(`${toDateKey}T00:00:00.000Z`);
    return Math.round((to - from) / (24 * 60 * 60 * 1000));
}

async function visibleProgramTitles(
    page: Parameters<typeof channelItemByTitle>[0]
) {
    return page
        .locator('app-epg-list .program-title')
        .allInnerTexts()
        .then((titles) => titles.map((title) => title.trim()).filter(Boolean));
}

async function expectCurrentProgram(
    page: Parameters<typeof channelItemByTitle>[0],
    currentProgram: XtreamNormalizedEpgListing,
    timeZone: string
): Promise<void> {
    const startTime = formatTimeInZone(currentProgram.startTimestamp, timeZone);
    const stopTime = formatTimeInZone(currentProgram.stopTimestamp, timeZone);
    const currentProgramRow = page
        .locator('app-epg-list .program-item.current-program')
        .first();

    if (await currentProgramRow.isVisible().catch(() => false)) {
        await expect(currentProgramRow.locator('.program-title')).toHaveText(
            currentProgram.title
        );
        await expect(currentProgramRow.locator('.time')).toHaveText(
            `${startTime}–${stopTime}`
        );
        return;
    }

    const summary = page.locator('app-live-epg-panel').first();
    await expect(summary.locator('.live-epg-panel__title')).toHaveText(
        currentProgram.title
    );
    await expect(summary.locator('.live-epg-panel__time')).toHaveText(
        `${startTime} - ${stopTime}`
    );
}

async function getProgressWidthPercent(
    row: ReturnType<typeof channelItemByTitle>
) {
    const style = await row.locator('.epg-progress-fill').getAttribute('style');
    const width = style?.match(/width:\s*([\d.]+)%/)?.[1] ?? '0';
    return Number.parseFloat(width);
}
