/* @vitest-environment jsdom */

import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useRef, useState } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { ApiClientError } from '../../services/apiClient';
import type {
  CanvasLayer,
  CanvasTool,
  EditorAsset,
  EditorAssetFolder,
  GenerateDialogState,
  SidebarPanel,
} from './ImageCanvasEditorTypes';
import { useImageCanvasUploadWorkflow } from './useImageCanvasUploadWorkflow';

const createEditorAssetMock = vi.hoisted(() => vi.fn());

vi.mock('../../services/image-editor/editorProjectClient', async () => {
  const actual = await vi.importActual<
    typeof import('../../services/image-editor/editorProjectClient')
  >('../../services/image-editor/editorProjectClient');
  return {
    ...actual,
    createEditorAsset: createEditorAssetMock,
  };
});

function createDefaultFolder(): EditorAssetFolder {
  return {
    id: 'project',
    label: '项目素材',
    collapsed: false,
    systemDefault: true,
    persisted: true,
  };
}

function createTestFile(name = '上传素材.png') {
  return new File(['image'], name, { type: 'image/png' });
}

function createDeferred<T>() {
  let resolve!: (value: T) => void;
  let reject!: (reason?: unknown) => void;
  const promise = new Promise<T>((promiseResolve, promiseReject) => {
    resolve = promiseResolve;
    reject = promiseReject;
  });
  return { promise, resolve, reject };
}

function UploadWorkflowHarness({
  canAccessProtectedData = true,
  openEditorLoginModal = vi.fn(),
  activeTool = 'select',
}: {
  canAccessProtectedData?: boolean;
  openEditorLoginModal?: (postLoginAction?: (() => void) | null) => void;
  activeTool?: CanvasTool;
}) {
  const [assetFolders, setAssetFolders] = useState<EditorAssetFolder[]>([
    createDefaultFolder(),
  ]);
  const [assets, setAssets] = useState<EditorAsset[]>([]);
  const [layers, setLayers] = useState<CanvasLayer[]>([]);
  const [generateDialog, setGenerateDialog] =
    useState<GenerateDialogState | null>(null);
  const [activeSidebarPanel, setActiveSidebarPanel] =
    useState<SidebarPanel | null>('assets');
  const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
  const uploadIndexRef = useRef(0);

  const workflow = useImageCanvasUploadWorkflow({
    canAccessProtectedData,
    openEditorLoginModal,
    assetFolders,
    activeUploadFolderId: 'project',
    canvasSize: { width: 900, height: 640 },
    viewport: { x: 10, y: 20, scale: 2 },
    activeTool,
    allocateUploadIndex: () => {
      uploadIndexRef.current += 1;
      return uploadIndexRef.current;
    },
    setAssetFolders,
    setAssets,
    setLayers,
    setGenerateDialog,
    setActiveSidebarPanel,
    appendCanvasLayersWithResources: (nextLayers) => {
      setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
    },
    selectSingleLayer: setSelectedLayerId,
  });

  return (
    <div>
      <input
        ref={workflow.uploadInputRef}
        type="file"
        aria-label="上传图片文件"
        multiple
        onChange={workflow.handleUploadInputChange}
      />
      <span data-testid="assets">
        {assets
          .map(
            (asset) =>
              `${asset.id}:${asset.label}:${asset.folderId}:${asset.uploadStatus ?? 'ready'}:${asset.uploadMessage ?? '-'}`,
          )
          .join('|')}
      </span>
      <span data-testid="folders">
        {assetFolders
          .map(
            (folder) =>
              `${folder.id}:${folder.collapsed ? 'collapsed' : 'open'}`,
          )
          .join('|')}
      </span>
      <span data-testid="layers">
        {layers
          .map(
            (layer) =>
              `${layer.id}:${layer.title}:${layer.sourceAssetId}:${layer.x}:${layer.y}`,
          )
          .join('|')}
      </span>
      <span data-testid="sidebar">{activeSidebarPanel ?? '-'}</span>
      <span data-testid="selected-layer">{selectedLayerId ?? '-'}</span>
      <span data-testid="dialog">
        {generateDialog
          ? `${generateDialog.mode}:${generateDialog.status}:${generateDialog.characterSpecReference?.label ?? '-'}:${generateDialog.characterReferences?.length ?? 0}:${generateDialog.iconSpecReference?.label ?? '-'}:${generateDialog.specReference?.label ?? '-'}`
          : '-'}
      </span>
      <button
        type="button"
        onClick={() => workflow.addUploadedFiles([createTestFile()])}
      >
        上传素材
      </button>
      <button type="button" onClick={() => workflow.requestUpload('asset')}>
        请求素材上传
      </button>
      <button
        type="button"
        onClick={() =>
          workflow.addUploadedFiles([createTestFile('画布素材.png')], {
            addToCanvas: true,
            canvasPoint: { x: 110, y: 120 },
          })
        }
      >
        上传到画布
      </button>
      <button
        type="button"
        onClick={() =>
          setGenerateDialog({
            mode: 'character',
            prompt: '',
            status: 'failed',
            errorMessage: '旧错误',
          })
        }
      >
        准备角色生成
      </button>
      <button
        type="button"
        onClick={() =>
          setGenerateDialog({
            mode: 'spec',
            specType: 'ui',
            prompt: '',
            status: 'failed',
            errorMessage: '旧错误',
          })
        }
      >
        准备规范生成
      </button>
      <button
        type="button"
        onClick={() => workflow.setUploadTarget('spec-reference')}
      >
        选择规范参考图
      </button>
      <button
        type="button"
        onClick={() => workflow.setUploadTarget('character-spec')}
      >
        选择角色规范
      </button>
      <button
        type="button"
        onClick={() => workflow.requestUpload('character-spec')}
      >
        请求角色规范上传
      </button>
    </div>
  );
}

describe('useImageCanvasUploadWorkflow', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    createEditorAssetMock.mockImplementation(async (input) => ({
      assetId: `persisted-${input.label}`,
      folderId: input.folderId,
      label: input.label,
      imageSrc: input.imageSrc,
      width: input.width,
      height: input.height,
      sourceType: input.sourceType,
      objectKey: 'object-key-uploaded',
      assetObjectId: 'asset-object-uploaded',
    }));
  });

  it('opens login before creating placeholders and resumes the same upload after login', async () => {
    const openEditorLoginModal = vi.fn();
    const { rerender } = render(
      <UploadWorkflowHarness
        canAccessProtectedData={false}
        openEditorLoginModal={openEditorLoginModal}
      />,
    );

    fireEvent.click(screen.getByRole('button', { name: '上传素材' }));

    expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
    expect(createEditorAssetMock).not.toHaveBeenCalled();
    expect(screen.getByTestId('assets').textContent).toBe('');

    const resumeUpload = openEditorLoginModal.mock.calls[0]?.[0];
    rerender(
      <UploadWorkflowHarness
        canAccessProtectedData
        openEditorLoginModal={openEditorLoginModal}
      />,
    );
    act(() => {
      (resumeUpload as () => void)();
    });

    await waitFor(() => {
      expect(createEditorAssetMock).toHaveBeenCalledTimes(1);
    });
    await waitFor(() => {
      expect(screen.getByTestId('assets').textContent).toContain(
        'persisted-上传素材.png:上传素材.png:project:ready:-',
      );
    });
  });

  it('opens login instead of the asset file picker when protected data is unavailable', () => {
    const openEditorLoginModal = vi.fn();
    render(
      <UploadWorkflowHarness
        canAccessProtectedData={false}
        openEditorLoginModal={openEditorLoginModal}
      />,
    );
    const uploadInput = screen.getByLabelText('上传图片文件') as HTMLInputElement;
    const clickUploadInput = vi.spyOn(uploadInput, 'click');

    fireEvent.click(screen.getByRole('button', { name: '请求素材上传' }));

    expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
    expect(clickUploadInput).not.toHaveBeenCalled();
  });

  it('keeps generation reference uploads local and opens the file picker without login', () => {
    const openEditorLoginModal = vi.fn();
    render(
      <UploadWorkflowHarness
        canAccessProtectedData={false}
        openEditorLoginModal={openEditorLoginModal}
      />,
    );
    const uploadInput = screen.getByLabelText('上传图片文件') as HTMLInputElement;
    const clickUploadInput = vi.spyOn(uploadInput, 'click');

    fireEvent.click(screen.getByRole('button', { name: '请求角色规范上传' }));

    expect(openEditorLoginModal).not.toHaveBeenCalled();
    expect(clickUploadInput).toHaveBeenCalledTimes(1);
  });

  it('creates an uploading asset card, adds a canvas layer, and patches the layer with the persisted asset id', async () => {
    const deferredAsset = createDeferred<{
      assetId: string;
      folderId: string;
      label: string;
      imageSrc: string;
      width: number;
      height: number;
      sourceType: 'uploaded';
      objectKey: string;
      assetObjectId: string;
    }>();
    createEditorAssetMock.mockReturnValueOnce(deferredAsset.promise);
    render(<UploadWorkflowHarness />);

    fireEvent.click(screen.getByRole('button', { name: '上传到画布' }));

    await waitFor(() => {
      expect(screen.getByTestId('assets').textContent).toContain(
        'upload-1:画布素材.png:project:uploading:上传中',
      );
      expect(screen.getByTestId('layers').textContent).toContain(
        'layer-upload-1:画布素材.png:upload-1:-160:-107.5',
      );
    });
    expect(screen.getByTestId('sidebar').textContent).toBe('layers');
    expect(screen.getByTestId('selected-layer').textContent).toBe(
      'layer-upload-1',
    );

    deferredAsset.resolve({
      assetId: 'asset-persisted-canvas',
      folderId: 'project',
      label: '画布素材.png',
      imageSrc: 'data:image/png;base64,Y2FudmFz',
      width: 420,
      height: 315,
      sourceType: 'uploaded',
      objectKey: 'object-key-canvas',
      assetObjectId: 'asset-object-canvas',
    });

    await waitFor(() => {
      expect(screen.getByTestId('assets').textContent).toContain(
        'asset-persisted-canvas:画布素材.png:project:ready:-',
      );
      expect(screen.getByTestId('layers').textContent).toContain(
        'layer-upload-1:画布素材.png:asset-persisted-canvas:-160:-107.5',
      );
    });
  });

  it('marks upload cards as failed and reopens login on auth errors returned by asset creation', async () => {
    const openEditorLoginModal = vi.fn();
    createEditorAssetMock.mockRejectedValueOnce(
      new ApiClientError({
        message: '未授权访问',
        status: 401,
        code: 'UNAUTHORIZED',
      }),
    );
    render(
      <UploadWorkflowHarness openEditorLoginModal={openEditorLoginModal} />,
    );

    fireEvent.click(screen.getByRole('button', { name: '上传素材' }));

    await waitFor(() => {
      expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
      expect(screen.getByTestId('assets').textContent).toContain(
        'upload-1:上传素材.png:project:failed:请先登录',
      );
    });
  });

  it('dispatches file input uploads to generation references and resets failed state', async () => {
    render(<UploadWorkflowHarness />);

    fireEvent.click(screen.getByRole('button', { name: '准备角色生成' }));
    fireEvent.click(screen.getByRole('button', { name: '选择角色规范' }));
    await waitFor(() => {
      expect(screen.getByTestId('dialog').textContent).toBe(
        'character:failed:-:0:-:-',
      );
    });

    fireEvent.change(screen.getByLabelText('上传图片文件'), {
      target: {
        files: [createTestFile('角色规范.png')],
      },
    });

    await waitFor(() => {
      expect(screen.getByTestId('dialog').textContent).toContain(
        'character:idle:角色规范.png:0:-:-',
      );
    });
    expect(createEditorAssetMock).not.toHaveBeenCalled();
  });

  it('dispatches file input uploads to spec reference images', async () => {
    render(<UploadWorkflowHarness />);

    fireEvent.click(screen.getByRole('button', { name: '准备规范生成' }));
    fireEvent.click(screen.getByRole('button', { name: '选择规范参考图' }));
    fireEvent.change(screen.getByLabelText('上传图片文件'), {
      target: {
        files: [createTestFile('UI参考.png')],
      },
    });

    await waitFor(() => {
      expect(screen.getByTestId('dialog').textContent).toContain(
        'spec:idle:-:0:-:UI参考.png',
      );
    });
    expect(createEditorAssetMock).not.toHaveBeenCalled();
  });
});
