import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/svelte';
import HistoryList from './HistoryList.svelte';
import type { ReadProgressSummary } from '$lib/api/read_progress';

afterEach(() => cleanup());

function entry(over: Partial<ReadProgressSummary> = {}): ReadProgressSummary {
    return {
        manga_id: 'm1',
        manga_title: 'Berserk',
        manga_cover_image_path: null,
        chapter_id: 'c1',
        chapter_number: 5,
        chapter_page_count: 10,
        page: 1,
        updated_at: '2026-01-02T00:00:00Z',
        new_chapters_count: 0,
        ...over
    };
}

describe('HistoryList', () => {
    it('renders one row per entry with a continue link to the chapter', () => {
        render(HistoryList, {
            props: { entries: [entry({ manga_id: 'm1', chapter_id: 'c9', chapter_number: 9 })] }
        });
        const cont = screen.getByRole('link', { name: /Continue Chapter 9/i });
        expect(cont.getAttribute('href')).toBe('/manga/m1/chapter/c9');
    });

    it('renders covers with lazy loading and async decode', () => {
        render(HistoryList, {
            props: { entries: [entry({ manga_cover_image_path: 'mangas/m1/cover.jpg' })] }
        });
        const img = document.querySelector('img.cover') as HTMLImageElement;
        expect(img).not.toBeNull();
        expect(img.getAttribute('loading')).toBe('lazy');
        expect(img.getAttribute('decoding')).toBe('async');
    });

    it('appends the page number to the continue line only past page 1', () => {
        // The page suffix is a separate text node from "Continue Chapter N",
        // so assert against the link's normalized text rather than getByText.
        render(HistoryList, { props: { entries: [entry({ page: 7, chapter_number: 3 })] } });
        const link = screen.getByRole('link', { name: /Continue Chapter 3/i });
        expect(link.textContent?.replace(/\s+/g, ' ').trim()).toBe('Continue Chapter 3 — page 7');
        cleanup();
        render(HistoryList, { props: { entries: [entry({ page: 1, chapter_number: 3 })] } });
        expect(screen.queryByText(/page 1/i)).toBeNull();
    });

    it('shows a "chapter removed" fallback when the chapter id survives but its number is gone', () => {
        render(HistoryList, {
            props: { entries: [entry({ chapter_id: 'c1', chapter_number: null })] }
        });
        expect(screen.getByText(/chapter removed/i)).toBeTruthy();
        expect(screen.queryByRole('link', { name: /Continue/i })).toBeNull();
    });

    it('shows a "whole manga" fallback when there is no chapter', () => {
        render(HistoryList, {
            props: { entries: [entry({ chapter_id: null, chapter_number: null, page: 4 })] }
        });
        expect(screen.getByText(/Whole manga, page 4/i)).toBeTruthy();
    });

    it('renders the empty state with the provided copy and testid when there are no entries', () => {
        render(HistoryList, {
            props: { entries: [], emptyText: 'Nothing here yet.', testid: 'library-history' }
        });
        const empty = screen.getByTestId('library-history-empty');
        expect(empty.textContent).toContain('Nothing here yet.');
        expect(screen.queryByTestId('library-history-list')).toBeNull();
    });

    it('omits clear buttons entirely when no onClear is supplied (read-only mode)', () => {
        render(HistoryList, { props: { entries: [entry()] } });
        expect(screen.queryByRole('button', { name: /Clear/i })).toBeNull();
    });

    it('optimistically removes a row and calls onClear when the clear button is pressed', async () => {
        const onClear = vi.fn().mockResolvedValue(undefined);
        render(HistoryList, {
            props: {
                entries: [entry({ manga_id: 'm1', manga_title: 'Berserk' })],
                onClear,
                testid: 'history-reading'
            }
        });
        await fireEvent.click(screen.getByRole('button', { name: /Clear Berserk from history/i }));
        expect(onClear).toHaveBeenCalledWith(
            expect.objectContaining({ manga_id: 'm1' })
        );
        await waitFor(() => expect(screen.queryByText('Berserk')).toBeNull());
    });

    it('restores the row and surfaces an inline error when onClear rejects', async () => {
        const onClear = vi.fn().mockRejectedValue(new Error('network down'));
        render(HistoryList, {
            props: {
                entries: [entry({ manga_id: 'm1', manga_title: 'Berserk' })],
                onClear,
                testid: 'history-reading'
            }
        });
        await fireEvent.click(screen.getByRole('button', { name: /Clear Berserk from history/i }));
        await waitFor(() => {
            const err = screen.getByTestId('history-reading-error');
            expect(err.textContent).toMatch(/network down/i);
        });
        // Optimistic removal rolled back — the row is back.
        expect(screen.getByText('Berserk')).toBeTruthy();
    });
});
