import { beforeEach, describe, expect, it, vi } from 'vitest';

const { mockDownloadMedia, mockFormatCookieHeader } = vi.hoisted(() => ({
    mockDownloadMedia: vi.fn(),
    mockFormatCookieHeader: vi.fn(() => 'sid=secret'),
}));

vi.mock('@jackwener/opencli/download/media-download', () => ({
    downloadMedia: mockDownloadMedia,
}));

vi.mock('@jackwener/opencli/download', () => ({
    formatCookieHeader: mockFormatCookieHeader,
}));

import { getRegistry } from '@jackwener/opencli/registry';
import './comments.js';
import './download.js';
import './feed.js';
import './notifications.js';
import './note.js';
import './search.js';
import './user.js';

function createPageMock(evaluateResult) {
    return {
        goto: vi.fn().mockResolvedValue(undefined),
        evaluate: vi.fn().mockResolvedValue(evaluateResult),
        wait: vi.fn().mockResolvedValue(undefined),
        autoScroll: vi.fn().mockResolvedValue(undefined),
        getCookies: vi.fn().mockResolvedValue([{ name: 'sid', value: 'secret', domain: 'www.rednote.com' }]),
    };
}
function createSearchPageMock(evaluateResults) {
    const page = createPageMock(undefined);
    page.evaluate = vi.fn();
    for (const result of evaluateResults) {
        page.evaluate.mockResolvedValueOnce(result);
    }
    return page;
}

describe('rednote note URL identity', () => {
    const download = getRegistry().get('rednote/download');
    const comments = getRegistry().get('rednote/comments');

    beforeEach(() => {
        mockDownloadMedia.mockReset();
        mockDownloadMedia.mockResolvedValue([{ index: 1, type: 'image', status: 'success', size: '1 KB' }]);
        mockFormatCookieHeader.mockClear();
    });

    it('rejects xhslink short links before browser navigation', async () => {
        const page = createPageMock({ media: [] });
        await expect(download.func(page, {
            'note-id': 'https://xhslink.com/o/4MKEjsZnhCz',
            output: './out',
        })).rejects.toMatchObject({
            code: 'ARGUMENT',
            message: expect.stringContaining('signed URL'),
            hint: expect.stringContaining('rednote.com'),
        });
        expect(page.goto).not.toHaveBeenCalled();
        expect(mockDownloadMedia).not.toHaveBeenCalled();
    });

    it('rejects signed xiaohongshu URLs before browser navigation', async () => {
        const page = createPageMock({ media: [] });
        await expect(comments.func(page, {
            'note-id': 'https://www.xiaohongshu.com/search_result/69aadbcb000000002202f131?xsec_token=abc',
            limit: 20,
        })).rejects.toMatchObject({
            code: 'ARGUMENT',
            message: expect.stringContaining('signed URL'),
            hint: expect.stringContaining('rednote.com'),
        });
        expect(page.goto).not.toHaveBeenCalled();
    });

    it('does not leak the shared xiaohongshu authorHrefRaw transport field from comments rows', async () => {
        const page = createPageMock({
            loginWall: false,
            results: [
                { author: 'Alice', authorHrefRaw: '/user/profile/alice1', text: 'Nice', likes: 1, time: 'today', is_reply: false, reply_to: '' },
            ],
        });
        const rows = await comments.func(page, {
            'note-id': 'https://www.rednote.com/search_result/69aadbcb000000002202f131?xsec_token=abc',
            limit: 20,
        });

        expect(rows).toEqual([
            { rank: 1, author: 'Alice', text: 'Nice', likes: 1, time: 'today', is_reply: false, reply_to: '' },
        ]);
        expect(rows[0]).not.toHaveProperty('authorHrefRaw');
    });

    it('uses URL-scoped rednote cookies when downloading media', async () => {
        const page = createPageMock({
            noteId: '69bc166f000000001a02069a',
            media: [{ type: 'image', url: 'https://ci.rednote.com/example.jpg' }],
        });
        await download.func(page, {
            'note-id': 'https://www.rednote.com/search_result/69bc166f000000001a02069a?xsec_token=abc',
            output: './out',
        });
        expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://www.rednote.com' });
        expect(mockDownloadMedia).toHaveBeenCalledWith([{ type: 'image', url: 'https://ci.rednote.com/example.jpg' }], expect.objectContaining({
            cookies: 'sid=secret',
            subdir: '69bc166f000000001a02069a',
        }));
    });

    it('throws empty-result instead of returning a failed success row when no media exists', async () => {
        const page = createPageMock({ noteId: '69bc166f000000001a02069a', media: [] });
        let caught;
        try {
            await download.func(page, {
                'note-id': 'https://www.rednote.com/search_result/69bc166f000000001a02069a?xsec_token=abc',
                output: './out',
            });
        }
        catch (error) {
            caught = error;
        }
        expect(caught).toMatchObject({ code: 'EMPTY_RESULT' });
        expect(caught?.hint).toContain('No downloadable media');
        expect(mockDownloadMedia).not.toHaveBeenCalled();
    });
});

describe('rednote argument validation', () => {
    const comments = getRegistry().get('rednote/comments');
    const feed = getRegistry().get('rednote/feed');
    const notifications = getRegistry().get('rednote/notifications');
    const user = getRegistry().get('rednote/user');

    it.each([
        ['rednote/comments', comments, { 'note-id': 'https://www.rednote.com/search_result/69aadbcb000000002202f131?xsec_token=abc', limit: 0 }],
        ['rednote/feed', feed, { limit: 0 }],
        ['rednote/notifications', notifications, { limit: 0 }],
        ['rednote/user', user, { id: 'user123', limit: 0 }],
    ])('%s rejects invalid --limit before browser navigation', async (_name, command, kwargs) => {
        const page = createPageMock({});
        await expect(command.func(page, kwargs)).rejects.toMatchObject({ code: 'ARGUMENT' });
        expect(page.goto).not.toHaveBeenCalled();
    });

    it('rejects unknown notification types before browser navigation', async () => {
        const page = createPageMock({});
        await expect(notifications.func(page, { type: 'all', limit: 20 })).rejects.toMatchObject({
            code: 'ARGUMENT',
            message: expect.stringContaining('--type'),
        });
        expect(page.goto).not.toHaveBeenCalled();
    });
});

describe('rednote search browser-bridge envelopes', () => {
    const search = getRegistry().get('rednote/search');

    it('unwraps login-wall wait result envelopes before auth handling', async () => {
        const page = createSearchPageMock([
            { session: 'site:rednote', data: 'login_wall' },
        ]);

        await expect(search.func(page, { query: 'tesla', limit: 5 })).rejects.toMatchObject({
            code: 'AUTH_REQUIRED',
            message: expect.stringContaining('blocked behind a login wall'),
        });
        expect(page.evaluate).toHaveBeenCalledTimes(1);
    });

    it('unwraps search extraction envelopes and preserves rednote row shape', async () => {
        const url = 'https://www.rednote.com/search_result/68e90be80000000004022e66?xsec_token=test-token';
        const page = createSearchPageMock([
            'content',
            1,
            {
                session: 'site:rednote',
                data: [{
                    title: 'rednote result',
                    author: 'author',
                    likes: '12',
                    url,
                    author_url: 'https://www.rednote.com/user/profile/u1',
                }],
            },
        ]);

        await expect(search.func(page, { query: 'tesla', limit: 1 })).resolves.toEqual([{
            rank: 1,
            title: 'rednote result',
            author: 'author',
            likes: '12',
            published_at: '2025-10-10',
            url,
            author_url: 'https://www.rednote.com/user/profile/u1',
        }]);
    });

    it('fails typed instead of silently returning [] for malformed extraction payloads', async () => {
        const page = createSearchPageMock([
            'content',
            1,
            { session: 'site:rednote', data: { rows: [] } },
        ]);

        await expect(search.func(page, { query: 'tesla', limit: 1 })).rejects.toMatchObject({
            code: 'COMMAND_EXEC',
            message: expect.stringContaining('payload shape'),
        });
    });
});

describe('rednote Pinia store failures', () => {
    it('maps feed store read failure to CommandExecutionError', async () => {
        const command = getRegistry().get('rednote/feed');
        const page = createPageMock({ error: 'no_pinia' });
        await expect(command.func(page, { limit: 20 })).rejects.toMatchObject({
            code: 'COMMAND_EXEC',
            message: expect.stringContaining('no_pinia'),
        });
    });

    it('maps notification action failure to CommandExecutionError', async () => {
        const command = getRegistry().get('rednote/notifications');
        const page = createPageMock({ error: 'action_failed', detail: 'blocked' });
        await expect(command.func(page, { type: 'mentions', limit: 20 })).rejects.toMatchObject({
            code: 'COMMAND_EXEC',
            message: expect.stringContaining('action_failed'),
        });
    });

    it('allows an empty notification list after a successful store read', async () => {
        const command = getRegistry().get('rednote/notifications');
        const page = createPageMock({ items: [] });
        await expect(command.func(page, { type: 'mentions', limit: 20 })).resolves.toEqual([]);
    });
});
