import { EditingForm, type EditingFormData } from './editing-form';
import { I18nProvider } from '@/lib/i18n';
import {
    IMAGE_UPSTREAM_PROFILES,
    type ImageUpstreamProfile,
    type PartialImagesCount
} from '@/lib/image-upstream-profile';
import type { ImageStreamingStrategy } from '@/lib/image-upstream-strategy';
import { renderInClientDom } from '@/test-utils/react-dom';
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import * as React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

type RenderOptions = {
    backend: EditingFormData['image_backend'];
    outputFormat?: EditingFormData['output_format'];
    advancedOpen?: boolean;
    advancedTab?: 'output' | 'model' | 'stream' | 'route';
    reuseContext?: React.ComponentProps<typeof EditingForm>['reuseContext'];
    allowStreamingBatch?: boolean;
    enableParallelBatch?: boolean;
    editN?: number[];
    streamingStrategy?: EditingFormData['streaming_strategy'];
    defaultStreamingStrategy?: ImageStreamingStrategy;
    allowResponsesImageBackend?: boolean;
    hasDefaultResponsesModel?: boolean;
    editResponsesModel?: string;
    editPrompt?: string;
    editModel?: React.ComponentProps<typeof EditingForm>['editModel'];
    editSize?: React.ComponentProps<typeof EditingForm>['editSize'];
    canApplyRandomInspiration?: boolean;
    imageFiles?: File[];
    upstreamProfile?: ImageUpstreamProfile;
    upstreamProfileMixed?: boolean;
    isActive?: boolean;
    partialImages?: PartialImagesCount;
    streamMode?: React.ComponentProps<typeof EditingForm>['streamMode'];
    isLoading?: boolean;
    showLoadingState?: boolean;
};

const noop = () => {};

function readSubmitFooterClass(html: string, submitLabel: string): string {
    const footerPattern = /<div(?=[^>]*data-slot="card-footer")(?=[^>]*class="([^"]*)")[^>]*>/g;
    const footers = [...html.matchAll(footerPattern)];

    for (const [index, footer] of footers.entries()) {
        const start = footer.index;
        const end = footers[index + 1]?.index ?? html.length;
        const footerHtml = html.slice(start, end);
        if (footerHtml.includes(submitLabel)) return footer[1];
    }

    assert.fail(`missing submit footer for ${submitLabel}`);
}

function assertSubmitFooterAvailable(html: string, submitLabel: string) {
    const classNames = readSubmitFooterClass(html, submitLabel).split(/\s+/);
    const hiddenClassName = classNames.find((className) => className === 'hidden' || className.endsWith(':hidden'));

    assert.equal(hiddenClassName, undefined);
    assert.equal(classNames.includes('flex'), true);
    assert.equal(classNames.includes('border-t'), true);
}

function assertButtonDisabled(html: string, label: string) {
    const button = html.match(/<button[^>]*>[\s\S]*?<\/button>/g)?.find((candidate) => candidate.includes(label));

    assert.ok(button, `missing button for ${label}`);
    assert.match(button, /^<button[^>]*disabled=""/);
}

function createEditingFormProps({
    backend,
    outputFormat = 'png',
    advancedOpen = true,
    advancedTab = 'route',
    reuseContext = null,
    allowStreamingBatch = false,
    enableParallelBatch = false,
    editN = [1],
    streamingStrategy = 'server-default',
    defaultStreamingStrategy = 'auto',
    allowResponsesImageBackend = true,
    hasDefaultResponsesModel = true,
    editResponsesModel = '',
    editPrompt = '',
    editModel = 'gpt-image-2',
    editSize = 'auto',
    canApplyRandomInspiration = true,
    imageFiles = [],
    upstreamProfile = IMAGE_UPSTREAM_PROFILES['openai-compatible'],
    upstreamProfileMixed = false,
    isActive = true,
    partialImages = 1,
    streamMode = 'auto',
    isLoading = false,
    showLoadingState = isLoading
}: RenderOptions): React.ComponentProps<typeof EditingForm> {
    return {
        onSubmit: noop,
        onSaveInspiration: noop,
        canApplyRandomInspiration,
        onPickRandomInspiration: () => '用户保存的编辑提示词',
        isLoading,
        showLoadingState,
        isActive,
        currentMode: 'edit',
        onModeChange: noop,
        reuseContext,
        onClearReuseContext: noop,
        isPasswordRequiredByBackend: false,
        clientPasswordHash: null,
        onOpenPasswordDialog: noop,
        editModel,
        setEditModel: noop,
        imageFiles,
        sourceImagePreviewUrls: [],
        setImageFiles: noop,
        setSourceImagePreviewUrls: noop,
        maxImages: upstreamProfile.upload.maxImages,
        editPrompt,
        setEditPrompt: noop,
        editN,
        setEditN: noop,
        editSize,
        setEditSize: noop,
        editCustomWidth: 1024,
        setEditCustomWidth: noop,
        editCustomHeight: 1024,
        setEditCustomHeight: noop,
        editQuality: 'auto',
        setEditQuality: noop,
        editOutputFormat: outputFormat,
        setEditOutputFormat: noop,
        editCompression: [85],
        setEditCompression: noop,
        upstreamProfile,
        upstreamProfileMixed,
        editModeration: 'auto',
        setEditModeration: noop,
        editBrushSize: [20],
        setEditBrushSize: noop,
        editShowMaskEditor: false,
        setEditShowMaskEditor: noop,
        editGeneratedMaskFile: null,
        setEditGeneratedMaskFile: noop,
        editIsMaskSaved: false,
        setEditIsMaskSaved: noop,
        editOriginalImageSize: null,
        setEditOriginalImageSize: noop,
        editDrawnPoints: [],
        setEditDrawnPoints: noop,
        editMaskPreviewUrl: null,
        setEditMaskPreviewUrl: noop,
        streamMode,
        setStreamMode: noop,
        allowStreamingBatch,
        enableParallelBatch,
        setEnableParallelBatch: noop,
        partialImages,
        setPartialImages: noop,
        allowResponsesImageBackend,
        hasDefaultResponsesModel,
        editImageBackend: backend,
        setEditImageBackend: noop,
        editStreamingStrategy: streamingStrategy,
        defaultStreamingStrategy,
        setEditStreamingStrategy: noop,
        editResponsesModel,
        setEditResponsesModel: noop,
        editThinking: 'server-default',
        setEditThinking: noop,
        editPromptOptimization: 'server-default',
        setEditPromptOptimization: noop,
        editForceWeb: false,
        setEditForceWeb: noop,
        requestSummaryLabel: '请求 1 张图片',
        initialAdvancedOpen: advancedOpen,
        initialAdvancedTab: advancedTab
    };
}

function renderEditingForm(options: RenderOptions): string {
    return renderToStaticMarkup(
        <I18nProvider>
            <EditingForm {...createEditingFormProps(options)} />
        </I18nProvider>
    );
}

describe('EditingForm submit footer', { concurrency: false }, () => {
    it('keeps the submit footer available outside desktop breakpoints', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            editPrompt: '用户真实编辑要求',
            imageFiles: [new File(['x'], 'source.png', { type: 'image/png' })]
        });

        assertSubmitFooterAvailable(html, '编辑图像');
        assert.match(html, /grid grid-cols-\[minmax\(0,1fr\)_minmax\(0,1fr\)\] gap-1\.5 text-xs/);
        assert.match(html, /min-w-0 items-center justify-center rounded-md/);
        assert.doesNotMatch(html, /text-center whitespace-nowrap/);
        assert.doesNotMatch(html, /min-\[1760px\]:flex/);
        assert.match(html, /收藏提示词/);
        assert.match(html, /随机套用/);
        assert.match(html, /min-h-11 min-w-0 items-center justify-center/);
        assert.match(html, /focus-visible:ring-2 focus-visible:outline-none/);
        assert.match(html, /flex min-h-0 flex-1 flex-col overflow-hidden/);
        assert.match(html, /min-h-0 flex-1 space-y-3 overflow-y-auto p-4 pb-4/);
        assert.match(html, /border-border bg-card flex shrink-0 border-t p-4/);
    });

    it('keeps submit copy while another busy action disables the form', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            isLoading: true,
            showLoadingState: false
        });

        assert.match(html, /编辑图像/);
        assert.doesNotMatch(html, /编辑中\.\.\./);
    });

    it('disables edit inspiration actions when the prompt or saved inspirations are unavailable', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            editPrompt: '',
            canApplyRandomInspiration: false
        });

        assertButtonDisabled(html, '收藏提示词');
        assertButtonDisabled(html, '随机套用');
        assert.doesNotMatch(html, /用户保存的编辑提示词/);
    });

    it('runs edit inspiration actions through the rendered footer controls', async () => {
        const savedPrompts: string[] = [];
        const appliedPrompts: string[] = [];
        const props = createEditingFormProps({
            backend: 'server-default',
            editPrompt: '  当前编辑灵感  '
        });
        props.onSaveInspiration = (prompt) => savedPrompts.push(prompt);
        props.onPickRandomInspiration = () => '  随机编辑灵感  ';
        props.setEditPrompt = (nextPrompt) => {
            appliedPrompts.push(typeof nextPrompt === 'function' ? nextPrompt('') : nextPrompt);
        };

        const view = await renderInClientDom(
            <I18nProvider>
                <EditingForm {...props} />
            </I18nProvider>
        );

        try {
            const saveButton = [...view.container.querySelectorAll('button')].find((button) =>
                button.textContent?.includes('收藏提示词')
            );
            const randomButton = [...view.container.querySelectorAll('button')].find((button) =>
                button.textContent?.includes('随机套用')
            );

            assert.ok(saveButton, 'missing edit save inspiration button');
            assert.ok(randomButton, 'missing edit random inspiration button');

            await view.click(saveButton);
            await view.click(randomButton);

            assert.deepEqual(savedPrompts, ['  当前编辑灵感  ']);
            assert.deepEqual(appliedPrompts, ['随机编辑灵感']);
        } finally {
            await view.cleanup();
        }
    });

    it('only applies edit profile corrections after the form becomes active', async () => {
        const countCorrections: number[][] = [];
        const partialImageCorrections: number[] = [];
        const streamModeCorrections: string[] = [];
        const sizeCorrections: string[] = [];
        const props = createEditingFormProps({
            backend: 'server-default',
            isActive: false,
            editN: [0],
            editModel: 'gpt-image-1',
            editSize: 'custom',
            partialImages: 0,
            streamMode: 'auto',
            streamingStrategy: 'off'
        });
        props.setEditN = (nextCount) => {
            countCorrections.push(typeof nextCount === 'function' ? nextCount([]) : nextCount);
        };
        props.setPartialImages = (nextPartialImages) => {
            partialImageCorrections.push(
                typeof nextPartialImages === 'function' ? nextPartialImages(1) : nextPartialImages
            );
        };
        props.setStreamMode = (nextStreamMode) => {
            streamModeCorrections.push(typeof nextStreamMode === 'function' ? nextStreamMode('auto') : nextStreamMode);
        };
        props.setEditSize = (nextSize) => {
            sizeCorrections.push(typeof nextSize === 'function' ? nextSize('custom') : nextSize);
        };

        const view = await renderInClientDom(
            <I18nProvider>
                <EditingForm {...props} />
            </I18nProvider>
        );

        try {
            assert.deepEqual(countCorrections, []);
            assert.deepEqual(partialImageCorrections, []);
            assert.deepEqual(streamModeCorrections, []);
            assert.deepEqual(sizeCorrections, []);

            await view.render(
                <I18nProvider>
                    <EditingForm {...props} isActive />
                </I18nProvider>
            );

            assert.deepEqual(countCorrections, [[1]]);
            assert.deepEqual(partialImageCorrections, [1]);
            assert.deepEqual(streamModeCorrections, ['non_stream']);
            assert.deepEqual(sizeCorrections, ['auto']);
        } finally {
            await view.cleanup();
        }
    });
});

describe('EditingForm advanced upstream controls', () => {
    it('keeps translated preset labels inside responsive control cells', () => {
        const html = renderEditingForm({ backend: 'server-default' });

        assert.match(html, /radio-group-item-content\]\]:min-w-0/);
        assert.match(html, /radio-group-item-content\]\]:overflow-hidden/);
        assert.match(html, /max-w-full min-w-0 text-center leading-4 break-words whitespace-normal/);
        assert.doesNotMatch(html, /radio-group-item-content\]\]:overflow-visible/);
        assert.doesNotMatch(html, /2xl:grid-cols-5/);
        assert.doesNotMatch(html, /lg:hidden 2xl:block/);
    });

    it('keeps the full professional accordion available on desktop and mobile', () => {
        const html = renderEditingForm({ backend: 'server-default', advancedTab: 'route' });

        assert.match(
            html,
            /<div class="border-border bg-muted\/20 rounded-md border"><button[^>]*aria-controls="editing-advanced-panel"/
        );
        assert.doesNotMatch(
            html,
            /<div class="[^"]*lg:hidden[^"]*"><button[^>]*aria-controls="editing-advanced-panel"/
        );
    });

    it('keeps model and streaming controls out of the default edit form surface', () => {
        const html = renderEditingForm({ backend: 'server-default', advancedOpen: false });

        assert.match(html, /参考图/);
        assert.match(html, /修改想法/);
        assert.match(html, /专业模式/);
        assert.doesNotMatch(html, /block truncate text-xs font-normal/);
        assert.doesNotMatch(html, /edit-model-select/);
        assert.doesNotMatch(html, /edit-stream-mode-select/);
    });

    it('translates the default backend into a user-facing route label near submit', () => {
        const html = renderEditingForm({ backend: 'server-default', advancedOpen: false });

        assert.match(html, /默认线路/);
        assert.match(html, /请求 1 张图片/);
    });

    it('renders edit model controls only in the professional model tab', () => {
        const html = renderEditingForm({ backend: 'server-default', advancedTab: 'model' });

        assert.match(html, /edit-model-select/);
        assert.match(html, /gpt-image-2 始终以高保真方式处理参考图/);
        assert.doesNotMatch(html, /edit-image-backend-select/);
    });

    it('renders edit stream controls only in the professional stream tab', () => {
        const html = renderEditingForm({ backend: 'server-default', advancedTab: 'stream' });

        assert.match(html, /edit-stream-mode-select/);
        assert.match(html, /edit-partial-1/);
        assert.doesNotMatch(html, /edit-model-select/);
    });

    it('hides Matsca-only edit stream controls for the default OpenAI-compatible profile', () => {
        const html = renderEditingForm({ backend: 'server-default', advancedTab: 'stream' });

        assert.doesNotMatch(html, /edit-partial-0/);
        assert.match(html, /edit-partial-1/);
        assert.match(html, /edit-partial-3/);
        assert.doesNotMatch(html, /edit-partial-4/);
    });

    it('renders Matsca edit stream controls when the active upstream profile allows them', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'stream',
            upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca
        });

        assert.match(html, /edit-partial-0/);
        assert.match(html, /edit-partial-4/);
    });

    it('intersects Matsca edit partial image options with the Responses backend contract', () => {
        const html = renderEditingForm({
            backend: 'responses-image-generation',
            advancedTab: 'stream',
            upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca
        });

        assert.doesNotMatch(html, /edit-partial-0/);
        assert.match(html, /edit-partial-1/);
        assert.match(html, /edit-partial-3/);
        assert.doesNotMatch(html, /edit-partial-4/);
    });

    it('uses Matsca edit upload limits when the active upstream profile requires them', () => {
        const imageFiles = Array.from(
            { length: 9 },
            (_, index) => new File(['x'], `source-${index}.png`, { type: 'image/png' })
        );
        const html = renderEditingForm({
            backend: 'server-default',
            advancedOpen: false,
            editPrompt: '用户真实编辑要求',
            imageFiles,
            upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca
        });

        assert.match(html, /最多 8 张/);
        assert.match(html, /9 \/ 8 张/);
        assert.match(html, /最多只能选择 8 张图片。/);
        assert.match(html, /<button[^>]*disabled=""[^>]*>[\s\S]*编辑图像[\s\S]*<\/button>/);
    });

    it('renders profile-aware high resolution edit size presets', () => {
        const openAiHtml = renderEditingForm({ backend: 'server-default' });
        const matscaHtml = renderEditingForm({
            backend: 'server-default',
            upstreamProfile: IMAGE_UPSTREAM_PROFILES.matsca
        });

        assert.match(openAiHtml, /id="edit-size-wide-4k"/);
        assert.doesNotMatch(openAiHtml, /id="edit-size-square-4k"/);
        assert.match(matscaHtml, /id="edit-size-square-4k"/);
    });

    it('renders an explicit parallel batch toggle in edit stream settings', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'stream',
            allowStreamingBatch: true,
            enableParallelBatch: true,
            editN: [2]
        });

        assert.match(html, /并发批量/);
        assert.match(html, /多张图或多条提示词会按当前渠道容量并发执行/);
        assert.match(html, /id="edit-parallel-batch-enabled"/);
        assert.match(html, /aria-checked="true"/);
    });

    it('keeps edit parallel batch disabled for a single output image', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'stream',
            allowStreamingBatch: true,
            enableParallelBatch: true
        });

        assert.match(html, /选择至少 2 张图片或 2 条提示词后可启用并发/);
        assert.match(html, /id="edit-parallel-batch-enabled"/);
        assert.match(html, /aria-checked="false"/);
        assert.match(html, /disabled=""/);
    });

    it('keeps edit parallel batch disabled when streaming strategy is off', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'stream',
            allowStreamingBatch: true,
            enableParallelBatch: true,
            editN: [2],
            streamingStrategy: 'off'
        });

        assert.match(html, /并发批量需要流式模式；非流式会保持顺序执行。/);
        assert.match(html, /id="edit-parallel-batch-enabled"/);
        assert.match(html, /aria-checked="false"/);
        assert.match(html, /disabled=""/);
    });

    it('keeps edit parallel batch disabled when the server default streaming strategy is off', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'stream',
            allowStreamingBatch: true,
            enableParallelBatch: true,
            editN: [2],
            defaultStreamingStrategy: 'off'
        });

        assert.match(html, /并发批量需要流式模式；非流式会保持顺序执行。/);
        assert.match(html, /id="edit-parallel-batch-enabled"/);
        assert.match(html, /aria-checked="false"/);
        assert.match(html, /disabled=""/);
    });

    it('disables the edit stream mode selector when the server default streaming strategy is off', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'stream',
            defaultStreamingStrategy: 'off'
        });

        assert.match(
            html,
            /<button[^>]*(?:disabled=""[^>]*id="edit-stream-mode-select"|id="edit-stream-mode-select"[^>]*disabled="")/
        );
    });

    it('renders Responses-specific edit controls when the Responses backend is selected', () => {
        const html = renderEditingForm({ backend: 'responses-image-generation', upstreamProfileMixed: true });

        assert.match(html, /图片生成后端/);
        assert.match(html, /影响说明/);
        assert.match(html, /Responses image_generation 需要实验开关和顶层模型/);
        assert.match(html, /当前服务端渠道包含不同上游模式/);
        assert.match(html, /自动或服务端默认会优先使用当前推荐的流式策略/);
        assert.match(html, /GPT 顶层模型/);
        assert.match(html, /思考强度/);
        assert.match(html, /提示词优化/);
        assert.doesNotMatch(html, /优先 Web 账号/);
    });

    it('explains the resolved edit server default streaming strategy', () => {
        const offHtml = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'route',
            streamingStrategy: 'server-default',
            defaultStreamingStrategy: 'off'
        });
        const forceHtml = renderEditingForm({
            backend: 'server-default',
            advancedTab: 'route',
            streamingStrategy: 'server-default',
            defaultStreamingStrategy: 'force-sse'
        });

        assert.match(offHtml, /关闭流式会减少长连接不稳定因素/);
        assert.doesNotMatch(offHtml, /自动或服务端默认会优先使用当前推荐的流式策略/);
        assert.match(forceHtml, /强制 SSE 会跳过自动判断/);
        assert.doesNotMatch(forceHtml, /自动或服务端默认会优先使用当前推荐的流式策略/);
    });

    it('disables the experimental Responses backend when runtime capabilities do not allow it', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            allowResponsesImageBackend: false
        });

        assert.match(html, /当前运行时或默认服务器渠道未开放 Responses image_generation/);
        assert.doesNotMatch(html, /GPT 顶层模型/);
    });

    it('blocks Responses edits until a top-level model is available', () => {
        const html = renderEditingForm({
            backend: 'responses-image-generation',
            hasDefaultResponsesModel: false,
            editResponsesModel: '',
            editPrompt: '用户真实编辑要求',
            imageFiles: [new File(['x'], 'source.png', { type: 'image/png' })]
        });

        assert.match(html, /Responses image_generation 需要填写 GPT 顶层模型/);
        assert.match(html, /<button[^>]*disabled=""[^>]*>[\s\S]*编辑图像[\s\S]*<\/button>/);
    });

    it('renders Images API edit controls and compression when JPEG output is selected', () => {
        const html = renderEditingForm({ backend: 'images-api', outputFormat: 'jpeg', advancedTab: 'output' });

        assert.match(html, /Images API/);
        assert.match(html, /输出格式/);
        assert.match(html, /压缩：85%/);
        assert.match(html, /内容审核级别/);
        assert.doesNotMatch(html, /GPT 顶层模型/);
    });
});

describe('EditingForm reused history context', () => {
    it('shows which history values were carried into edit mode', () => {
        const html = renderEditingForm({
            backend: 'server-default',
            reuseContext: {
                sourceLabel: '最近生成：2026/6/2 12:00:00',
                restoredFields: ['参考图', '提示词', '模型', '尺寸', '数量'],
                promptPreview: '用户真实编辑提示词'
            }
        });

        assert.match(html, /已带入内容/);
        assert.match(html, /最近生成：2026\/6\/2 12:00:00/);
        assert.match(html, /参考图/);
        assert.match(html, /提示词/);
        assert.match(html, /模型/);
        assert.match(html, /尺寸/);
        assert.match(html, /数量/);
        assert.match(html, /用户真实编辑提示词/);
        assert.match(html, /这些内容已经写入编辑单，可以修改后再生成。/);
    });
});
