/* @vitest-environment jsdom */

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

import { ApiClientError } from '../../services/apiClient';
import type { EditorAssetSnapshot } from '../../services/image-editor/editorProjectClient';
import type { EditorAsset } from './ImageCanvasEditorTypes';
import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary';

const createEditorAssetFolderMock = vi.hoisted(() => vi.fn());
const deleteEditorAssetMock = vi.hoisted(() => vi.fn());
const deleteEditorAssetFolderMock = vi.hoisted(() => vi.fn());
const loadEditorAssetLibraryMock = vi.hoisted(() => vi.fn());
const updateEditorAssetMock = vi.hoisted(() => vi.fn());
const updateEditorAssetFolderMock = vi.hoisted(() => vi.fn());

const defaultOpenEditorLoginModal = () => {};
const defaultOnDeleteAssets = () => {};

vi.mock('../../services/image-editor/editorProjectClient', async () => {
  const actual = await vi.importActual<
    typeof import('../../services/image-editor/editorProjectClient')
  >('../../services/image-editor/editorProjectClient');
  return {
    ...actual,
    createEditorAssetFolder: createEditorAssetFolderMock,
    deleteEditorAsset: deleteEditorAssetMock,
    deleteEditorAssetFolder: deleteEditorAssetFolderMock,
    loadEditorAssetLibrary: loadEditorAssetLibraryMock,
    updateEditorAsset: updateEditorAssetMock,
    updateEditorAssetFolder: updateEditorAssetFolderMock,
  };
});

function createUploadedAsset(overrides: Partial<EditorAsset> = {}): EditorAsset {
  return {
    id: 'asset-a',
    label: '素材A',
    src: 'data:image/png;base64,YQ==',
    width: 320,
    height: 240,
    folderId: 'project',
    sourceKind: 'uploaded',
    sourceType: 'uploaded',
    persisted: true,
    ...overrides,
  };
}

function createAssetSnapshot(
  overrides: Partial<EditorAssetSnapshot> = {},
): EditorAssetSnapshot {
  return {
    assetId: 'asset-a',
    label: '素材A',
    imageSrc: 'data:image/png;base64,YQ==',
    width: 320,
    height: 240,
    folderId: 'project',
    sourceType: 'uploaded',
    ...overrides,
  };
}

function AssetLibraryHarness({
  canAccessProtectedData = true,
  openEditorLoginModal = defaultOpenEditorLoginModal,
  onDeleteAssets = defaultOnDeleteAssets,
}: {
  canAccessProtectedData?: boolean;
  openEditorLoginModal?: (postLoginAction?: (() => void) | null) => void;
  onDeleteAssets?: (assets: EditorAsset[]) => void;
}) {
  const assetListRef = useRef<HTMLDivElement | null>(null);
  const assetLibrary = useImageCanvasAssetLibrary({
    assetListRef,
    canAccessProtectedData,
    openEditorLoginModal,
    onDeleteAssets,
  });

  return (
    <div>
      <div ref={assetListRef}>
        {assetLibrary.groupedAssets.map((folder) => (
          <section key={folder.id} data-asset-folder-id={folder.id}>
            <h2>{folder.label}</h2>
            {folder.assets.map((asset, index) => (
              <button
                key={asset.id || `asset-${index}`}
                type="button"
                data-asset-id={asset.id}
                onClick={() => assetLibrary.toggleAssetSelected(asset.id)}
              >
                {asset.label}
              </button>
            ))}
          </section>
        ))}
      </div>
      <span data-testid="folders">
        {assetLibrary.assetFolders
          .map((folder) => `${folder.id}:${folder.label}:${folder.persisted}`)
          .join('|')}
      </span>
      <span data-testid="assets">
        {assetLibrary.assets
          .map((asset) => `${asset.id}:${asset.label}:${asset.folderId}`)
          .join('|')}
      </span>
      <span data-testid="active-upload-folder">
        {assetLibrary.activeUploadFolderId}
      </span>
      <span data-testid="all-selected">
        {String(assetLibrary.allSelectableAssetsSelected)}
      </span>
      <span data-testid="selected-assets">
        {[...assetLibrary.selectedAssetIds].join('|')}
      </span>
      <span data-testid="new-folder-name">{assetLibrary.newFolderName}</span>
      <button
        type="button"
        onClick={() => {
          assetLibrary.setNewFolderName('角色素材');
          assetLibrary.setCreatingFolder(true);
        }}
      >
        prepare folder
      </button>
      <button
        type="button"
        onClick={() => void assetLibrary.commitNewAssetFolder()}
      >
        commit folder
      </button>
      <button
        type="button"
        onClick={() => assetLibrary.moveAssetToFolder('asset-a', 'folder-role')}
      >
        move asset
      </button>
      <button
        type="button"
        onClick={() => {
          const target = assetLibrary.assets.find(
            (asset) => asset.id === 'asset-a',
          );
          if (target) {
            assetLibrary.deleteUploadedAsset(target);
          }
        }}
      >
        delete asset
      </button>
      <button type="button" onClick={assetLibrary.toggleAllAssetsSelected}>
        toggle all
      </button>
      <button type="button" onClick={assetLibrary.deleteSelectedAssets}>
        delete selected
      </button>
    </div>
  );
}

describe('useImageCanvasAssetLibrary', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    loadEditorAssetLibraryMock.mockResolvedValue({
      folders: [
        {
          folderId: 'project',
          label: '项目素材',
          sortOrder: 0,
          collapsed: false,
          systemDefault: true,
        },
      ],
      assets: [createAssetSnapshot()],
    });
    createEditorAssetFolderMock.mockResolvedValue({
      folderId: 'folder-role',
      label: '角色素材',
      collapsed: false,
      systemDefault: false,
    });
    updateEditorAssetMock.mockResolvedValue(createAssetSnapshot());
    deleteEditorAssetMock.mockResolvedValue(createAssetSnapshot());
    deleteEditorAssetFolderMock.mockResolvedValue({
      folders: [
        {
          folderId: 'project',
          label: '项目素材',
          sortOrder: 0,
          collapsed: false,
          systemDefault: true,
        },
      ],
      assets: [],
    });
    updateEditorAssetFolderMock.mockResolvedValue({
      folderId: 'project',
      label: '项目素材',
      collapsed: false,
      systemDefault: true,
    });
  });

  it('loads and normalizes the account asset library', async () => {
    loadEditorAssetLibraryMock.mockResolvedValueOnce({
      folders: [
        {
          folderId: 'project',
          label: '项目素材',
          sortOrder: 0,
          collapsed: false,
          systemDefault: true,
        },
        {
          folderId: 'project-duplicate',
          label: '重复默认',
          sortOrder: 1,
          collapsed: false,
          systemDefault: true,
        },
      ],
      assets: [createAssetSnapshot()],
    });

    render(<AssetLibraryHarness />);

    await waitFor(() => {
      expect(screen.getByTestId('folders').textContent).toBe(
        'project:项目素材:true',
      );
    });
    expect(screen.getByTestId('assets').textContent).toBe(
      'asset-a:素材A:project',
    );
    expect(screen.getByTestId('active-upload-folder').textContent).toBe(
      'project',
    );
  });

  it('opens login when loading the asset library is unauthorized', async () => {
    const openEditorLoginModal = vi.fn();
    loadEditorAssetLibraryMock.mockRejectedValueOnce(
      new ApiClientError({
        message: '未授权访问',
        status: 401,
        code: 'UNAUTHORIZED',
      }),
    );

    render(
      <AssetLibraryHarness openEditorLoginModal={openEditorLoginModal} />,
    );

    await waitFor(() => {
      expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
    });
  });

  it('does not request the protected asset library before login is available', () => {
    render(<AssetLibraryHarness canAccessProtectedData={false} />);

    expect(loadEditorAssetLibraryMock).not.toHaveBeenCalled();
    expect(screen.getByTestId('folders').textContent).toBe(
      'project:项目素材:false',
    );
  });

  it('creates a local folder and replaces it with the persisted folder id', async () => {
    let resolveCreateFolder: (
      folder: Awaited<ReturnType<typeof createEditorAssetFolderMock>>,
    ) => void = () => {};
    createEditorAssetFolderMock.mockReturnValueOnce(
      new Promise((resolve) => {
        resolveCreateFolder = resolve;
      }),
    );
    render(<AssetLibraryHarness />);

    await screen.findByText('素材A');
    act(() => screen.getByRole('button', { name: 'prepare folder' }).click());
    await waitFor(() => {
      expect(screen.getByTestId('new-folder-name').textContent).toBe(
        '角色素材',
      );
    });
    act(() => screen.getByRole('button', { name: 'commit folder' }).click());
    await waitFor(() => {
      expect(screen.getByTestId('folders').textContent).toContain(
        'folder-',
      );
    });
    act(() => {
      resolveCreateFolder({
        folderId: 'folder-role',
        label: '角色素材',
        collapsed: false,
        systemDefault: false,
      });
    });

    await waitFor(() => {
      expect(screen.getByTestId('folders').textContent).toContain(
        'folder-role:角色素材:true',
      );
    });
    expect(createEditorAssetFolderMock).toHaveBeenCalledWith('角色素材', 101);
    expect(screen.getByTestId('active-upload-folder').textContent).toBe(
      'folder-role',
    );
  });

  it('moves a persisted asset to another folder', async () => {
    loadEditorAssetLibraryMock.mockResolvedValueOnce({
      folders: [
        {
          folderId: 'project',
          label: '项目素材',
          sortOrder: 0,
          collapsed: false,
          systemDefault: true,
        },
        {
          folderId: 'folder-role',
          label: '角色素材',
          sortOrder: 1,
          collapsed: false,
          systemDefault: false,
        },
      ],
      assets: [createAssetSnapshot()],
    });

    render(<AssetLibraryHarness />);

    await screen.findByText('素材A');
    act(() => screen.getByRole('button', { name: 'move asset' }).click());

    expect(screen.getByTestId('assets').textContent).toBe(
      'asset-a:素材A:folder-role',
    );
    expect(updateEditorAssetMock).toHaveBeenCalledWith('asset-a', {
      folderId: 'folder-role',
    });
  });

  it('deletes uploaded assets and reports them to the canvas cleanup callback', async () => {
    const onDeleteAssets = vi.fn();
    render(<AssetLibraryHarness onDeleteAssets={onDeleteAssets} />);

    await screen.findByText('素材A');
    act(() => screen.getByRole('button', { name: 'delete asset' }).click());

    expect(screen.getByTestId('assets').textContent).toBe('');
    expect(onDeleteAssets).toHaveBeenCalledWith([
      expect.objectContaining({ id: 'asset-a' }),
    ]);
    expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-a');
  });

  it('selects and deletes selected uploaded assets', async () => {
    const onDeleteAssets = vi.fn();
    loadEditorAssetLibraryMock.mockResolvedValueOnce({
      folders: [
        {
          folderId: 'project',
          label: '项目素材',
          sortOrder: 0,
          collapsed: false,
          systemDefault: true,
        },
      ],
      assets: [
        createAssetSnapshot({ assetId: 'asset-a', label: '素材A' }),
        createAssetSnapshot({ assetId: 'asset-b', label: '素材B' }),
      ],
    });

    render(<AssetLibraryHarness onDeleteAssets={onDeleteAssets} />);

    await screen.findByText('素材A');
    act(() => screen.getByRole('button', { name: 'toggle all' }).click());
    expect(screen.getByTestId('all-selected').textContent).toBe('true');
    act(() => screen.getByRole('button', { name: '素材B' }).click());
    await waitFor(() => {
      expect(screen.getByTestId('selected-assets').textContent).toBe('asset-a');
    });
    act(() => screen.getByRole('button', { name: 'delete selected' }).click());

    expect(screen.getByTestId('assets').textContent).toBe(
      'asset-b:素材B:project',
    );
    expect(onDeleteAssets).toHaveBeenCalledWith([
      expect.objectContaining({ id: 'asset-a' }),
    ]);
    expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-a');
    expect(deleteEditorAssetMock).not.toHaveBeenCalledWith('asset-b');
  });
});
