/** @vitest-environment jsdom */

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

import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import {
  executeRpgCreationAction,
  getRpgCreationOperation,
  upsertRpgWorldProfile,
} from '../../services/rpg-creation';
import { type CustomWorldProfile, WorldType } from '../../types';
import { useRpgCreationResultAutosave } from './useRpgCreationResultAutosave';
import { useRpgEntryLibraryDetail } from './useRpgEntryLibraryDetail';

vi.mock('../../services/rpg-creation', () => ({
  executeRpgCreationAction: vi.fn(),
  getRpgCreationOperation: vi.fn(),
  upsertRpgWorldProfile: vi.fn(),
}));

vi.mock('../../services/rpg-entry', () => ({
  deleteRpgEntryWorldProfile: vi.fn(),
  getRpgEntryWorldGalleryDetail: vi.fn(),
  listRpgEntryWorldLibrary: vi.fn(),
  publishRpgEntryWorldProfile: vi.fn(),
  unpublishRpgEntryWorldProfile: vi.fn(),
}));

function buildProfile(name: string): CustomWorldProfile {
  return {
    id: `profile-${name}`,
    settingText: name,
    name,
    subtitle: name,
    summary: name,
    tone: '测试',
    playerGoal: '测试',
    templateWorldType: WorldType.WUXIA,
    compatibilityTemplateWorldType: WorldType.WUXIA,
    majorFactions: [],
    coreConflicts: [],
    attributeSchema: {
      id: `schema-${name}`,
      worldId: `profile-${name}`,
      schemaVersion: 1,
      generatedFrom: {
        worldType: WorldType.CUSTOM,
        worldName: name,
        settingSummary: name,
        tone: '测试',
        conflictCore: '测试',
      },
      slots: [],
    },
    playableNpcs: [],
    storyNpcs: [],
    items: [],
    landmarks: [],
    generationMode: 'full',
    generationStatus: 'complete',
  };
}

function buildLibraryEntry(
  profile: CustomWorldProfile,
): CustomWorldLibraryEntry<CustomWorldProfile> {
  return {
    ownerUserId: 'user-1',
    profileId: profile.id,
    publicWorkCode: null,
    authorPublicUserCode: null,
    profile,
    visibility: 'published' as const,
    publishedAt: '2026-04-25T00:00:00.000Z',
    updatedAt: '2026-04-25T00:00:00.000Z',
    authorDisplayName: '测试玩家',
    worldName: profile.name,
    subtitle: profile.subtitle,
    summaryText: profile.summary,
    coverImageSrc: null,
    themeMode: 'tide' as const,
    playableNpcCount: profile.playableNpcs.length,
    landmarkCount: profile.landmarks.length,
    likeCount: 0,
  };
}

function buildSession(
  overrides: Partial<CustomWorldAgentSessionSnapshot> = {},
): CustomWorldAgentSessionSnapshot {
  return {
    sessionId: 'agent-session-1',
    currentTurn: 1,
    anchorContent: {
      worldPromise: null,
      playerFantasy: null,
      themeBoundary: null,
      playerEntryPoint: null,
      coreConflict: null,
      keyRelationships: null,
      hiddenLines: null,
      iconicElements: null,
    },
    progressPercent: 20,
    lastAssistantReply: '继续补齐世界草稿。',
    stage: 'clarifying',
    focusCardId: null,
    creatorIntent: null,
    creatorIntentReadiness: {
      isReady: false,
      completedKeys: [],
      missingKeys: [],
    },
    anchorPack: null,
    lockState: null,
    draftProfile: null,
    messages: [],
    draftCards: [],
    pendingClarifications: [],
    suggestedActions: [],
    recommendedReplies: [],
    qualityFindings: [],
    assetCoverage: {
      roleAssets: [],
      sceneAssets: [],
      allRoleAssetsReady: false,
      allSceneAssetsReady: false,
    },
    resultPreview: null,
    updatedAt: '2026-04-25T00:00:00.000Z',
    ...overrides,
  };
}

function buildResultView(
  overrides: Partial<RpgCreationResultView> = {},
): RpgCreationResultView {
  const session = overrides.session ?? buildSession();
  return {
    session,
    profile: null,
    profileSource: 'none',
    targetStage: 'agent-workspace',
    generationViewSource: null,
    resultViewSource: null,
    canAutosaveLibrary: false,
    canSyncResultProfile: false,
    publishReady: false,
    canEnterWorld: false,
    blockerCount: 0,
    recoveryAction: 'continue_agent',
    recoveryReason: null,
    ...overrides,
  };
}

describe('RPG Agent 草稿恢复', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('作品摘要已有对象数量但 session 没有 draftProfile 时恢复 Agent 页面', async () => {
    const syncAgentCreationResultView = vi.fn(async () =>
      buildResultView({
        session: buildSession({
          stage: 'clarifying',
          draftProfile: null,
        }),
        targetStage: 'agent-workspace',
        recoveryAction: 'continue_agent',
      }),
    );
    const setSelectionStage = vi.fn();
    const persistAgentUiState = vi.fn();
    const setGeneratedCustomWorldProfile = vi.fn();
    const setCustomWorldResultViewSource = vi.fn();
    const suppressAgentDraftResultAutoOpen = vi.fn();
    let openWork:
      | ((work: CustomWorldWorkSummary) => Promise<void>)
      | null = null;

    function Harness() {
      openWork = useRpgEntryLibraryDetail({
        userId: 'user-1',
        selectedDetailEntry: null,
        setSelectedDetailEntry: vi.fn(),
        savedCustomWorldEntries: [],
        setSavedCustomWorldEntries: vi.fn(),
        setGeneratedCustomWorldProfile,
        setCustomWorldError: vi.fn(),
        setCustomWorldAutoSaveError: vi.fn(),
        setCustomWorldAutoSaveState: vi.fn(),
        setCustomWorldGenerationViewSource: vi.fn(),
        setCustomWorldResultViewSource,
        setSelectionStage,
        setPlatformTabToCreate: vi.fn(),
        setPlatformTabToDraft: vi.fn(),
        setPlatformError: vi.fn(),
        appendBrowseHistoryEntry: vi.fn(async () => {}),
        refreshCustomWorldWorks: vi.fn(async () => []),
        refreshPublishedGallery: vi.fn(async () => []),
        persistAgentUiState,
        syncAgentCreationResultView,
        buildDraftResultProfile: (view) =>
          (view?.profile as CustomWorldProfile | null) ?? null,
        suppressAgentDraftResultAutoOpen,
        releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
        resetAutoSaveTrackingToIdle: vi.fn(),
        markAutoSavedProfile: vi.fn(),
      }).handleOpenCreationWork;

      return null;
    }

    render(<Harness />);

    await act(async () => {
      await openWork?.({
        workId: 'draft:agent-session-1',
        sourceType: 'agent_session',
        status: 'draft',
        title: '未生成草稿作品',
        subtitle: '',
        summary: '',
        updatedAt: '2026-04-25T00:00:00.000Z',
        stage: 'clarifying',
        stageLabel: '澄清中',
        playableNpcCount: 2,
        landmarkCount: 3,
        sessionId: 'agent-session-1',
        canResume: true,
        canEnterWorld: false,
      });
    });

    expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1');
    expect(suppressAgentDraftResultAutoOpen).toHaveBeenCalled();
    expect(persistAgentUiState).toHaveBeenCalledWith('agent-session-1', null);
    expect(setGeneratedCustomWorldProfile).toHaveBeenLastCalledWith(null);
    expect(setCustomWorldResultViewSource).toHaveBeenLastCalledWith(null);
    expect(setSelectionStage).toHaveBeenLastCalledWith('agent-workspace');
    expect(setSelectionStage).not.toHaveBeenCalledWith('custom-world-result');
  });

  it('作品详情已加载完整编辑资产时列表摘要不能覆盖 selectedDetailEntry', async () => {
    const fullProfile: CustomWorldProfile = {
      ...buildProfile('星砂废都'),
      id: 'profile-stardust',
      cover: {
        sourceType: 'default',
        imageSrc: null,
        characterRoleIds: ['playable-shamian'],
      },
      playableNpcs: [
        {
          id: 'playable-shamian',
          name: '砂眠',
          title: '废都引路人',
          role: '主角代理',
          description: '追查旧约的人。',
          backstory: '从星砂潮汐里醒来。',
          personality: '冷静。',
          motivation: '找到旧约。',
          combatStyle: '踏砂突进。',
          initialAffinity: 45,
          relationshipHooks: [],
          tags: [],
          relations: [],
          backstoryReveal: {
            publicSummary: '废都引路人。',
            privateChatUnlockAffinity: 60,
            chapters: [],
          },
          skills: [
            {
              id: 'skill-star-step',
              name: '星砂步',
              summary: '踏砂突进。',
              style: 'mobility',
              actionPreviewConfig: {
                folder: 'characters/shamian',
                prefix: 'skill_',
                frames: 8,
                basePath: '/assets/custom-world/shamian/skill',
                previewVideoPath: '/assets/custom-world/shamian/skill.mp4',
              },
            },
          ],
          initialItems: [
            {
              id: 'item-sand-compass',
              name: '星砂罗盘',
              category: '专属物品',
              quantity: 1,
              rarity: 'rare',
              description: '能指出旧约埋藏方向。',
              tags: [],
              iconSrc: '/assets/custom-world/items/sand-compass.png',
            },
          ],
          imageSrc: '/assets/custom-world/playable-shamian.png',
          attributeProfile: {
            schemaId: 'schema-星砂废都',
            values: { axis_a: 8 },
            topTraits: ['星砂共鸣'],
            evidence: [
              { slotId: 'axis_a', reason: '能听见星砂潮汐。' },
            ],
          },
        },
      ],
      sceneChapterBlueprints: [
        {
          id: 'scene-chapter-clocktower',
          sceneId: 'landmark-clocktower',
          title: '钟楼第一夜',
          summary: '钟楼第一夜。',
          sceneTaskDescription: '进入钟楼。',
          linkedThreadIds: [],
          linkedLandmarkIds: ['landmark-clocktower'],
          acts: [
            {
              id: 'act-clocktower-opening',
              sceneId: 'landmark-clocktower',
              title: '第一幕',
              summary: '砂眠带玩家进入坠星钟楼。',
              stageCoverage: ['opening'],
              backgroundImageSrc:
                '/assets/custom-world/act-clocktower-opening.png',
              backgroundAssetId: 'asset-act-clocktower-opening',
              encounterNpcIds: ['playable-shamian'],
              primaryNpcId: 'playable-shamian',
              oppositeNpcId: 'playable-shamian',
              eventDescription: '钟楼旧铃忽然自鸣。',
              linkedThreadIds: ['thread-old-vow'],
              advanceRule: 'after_primary_contact',
              actGoal: '进入钟楼。',
              transitionHook: '星砂开始倒流。',
            },
          ],
        },
      ],
    };
    const summaryProfile: CustomWorldProfile = {
      ...fullProfile,
      cover: null,
      playableNpcs: [
        {
          ...fullProfile.playableNpcs[0]!,
          skills: [
            {
              id: 'skill-star-step',
              name: '星砂步',
              summary: '踏砂突进。',
              style: 'mobility',
            },
          ],
          initialItems: [
            {
              ...fullProfile.playableNpcs[0]!.initialItems[0]!,
              iconSrc: undefined,
            },
          ],
          imageSrc: undefined,
          attributeProfile: undefined,
        },
      ],
      sceneChapterBlueprints: [
        {
          ...fullProfile.sceneChapterBlueprints![0]!,
          acts: [
            {
              ...fullProfile.sceneChapterBlueprints![0]!.acts[0]!,
              backgroundImageSrc: undefined,
              backgroundAssetId: undefined,
              linkedThreadIds: [],
              actGoal: '',
              transitionHook: '',
            },
          ],
        },
      ],
    };
    const detailEntry = buildLibraryEntry(fullProfile);
    const summaryEntry = buildLibraryEntry(summaryProfile);
    const selectedEntries: CustomWorldLibraryEntry<CustomWorldProfile>[] = [];

    function Harness() {
      const [selectedDetailEntry, setSelectedDetailEntry] = useState<
        CustomWorldLibraryEntry<CustomWorldProfile> | null
      >(detailEntry);
      useEffect(() => {
        if (selectedDetailEntry) {
          selectedEntries.push(selectedDetailEntry);
        }
      }, [selectedDetailEntry]);
      useRpgEntryLibraryDetail({
        userId: 'user-1',
        selectedDetailEntry,
        setSelectedDetailEntry,
        savedCustomWorldEntries: [summaryEntry],
        setSavedCustomWorldEntries: vi.fn(),
        setGeneratedCustomWorldProfile: vi.fn(),
        setCustomWorldError: vi.fn(),
        setCustomWorldAutoSaveError: vi.fn(),
        setCustomWorldAutoSaveState: vi.fn(),
        setCustomWorldGenerationViewSource: vi.fn(),
        setCustomWorldResultViewSource: vi.fn(),
        setSelectionStage: vi.fn(),
        setPlatformTabToCreate: vi.fn(),
        setPlatformTabToDraft: vi.fn(),
        setPlatformError: vi.fn(),
        appendBrowseHistoryEntry: vi.fn(async () => {}),
        refreshCustomWorldWorks: vi.fn(async () => []),
        refreshPublishedGallery: vi.fn(async () => []),
        persistAgentUiState: vi.fn(),
        syncAgentCreationResultView: vi.fn(),
        buildDraftResultProfile: () => null,
        suppressAgentDraftResultAutoOpen: vi.fn(),
        releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
        resetAutoSaveTrackingToIdle: vi.fn(),
        markAutoSavedProfile: vi.fn(),
      });
      return null;
    }

    render(<Harness />);

    await act(async () => {});
    const lastSelected = selectedEntries.at(-1);
    expect(lastSelected?.profile.cover?.characterRoleIds).toEqual([
      'playable-shamian',
    ]);
    expect(
      lastSelected?.profile.playableNpcs[0]?.skills[0]?.actionPreviewConfig
        ?.previewVideoPath,
    ).toBe('/assets/custom-world/shamian/skill.mp4');
    expect(lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc).toBe(
      '/assets/custom-world/items/sand-compass.png',
    );
    expect(lastSelected?.profile.playableNpcs[0]?.attributeProfile).toBeTruthy();
    expect(
      lastSelected?.profile.sceneChapterBlueprints?.[0]?.acts[0]
        ?.backgroundImageSrc,
    ).toBe('/assets/custom-world/act-clocktower-opening.png');
  });

  it('默认封面和角色编辑结构差异也不能被列表摘要覆盖', async () => {
    const fullRole = {
      id: 'playable-shamian',
      name: '砂眠',
      title: '废都引路人',
      role: '主角代理',
      description: '追查旧约的人。',
      backstory: '从星砂潮汐里醒来。',
      personality: '冷静。',
      motivation: '找到旧约。',
      combatStyle: '踏砂突进。',
      initialAffinity: 45,
      relationshipHooks: [],
      tags: [],
      relations: [],
      backstoryReveal: {
        publicSummary: '废都引路人。',
        privateChatUnlockAffinity: 60,
        chapters: [],
      },
      skills: [
        {
          id: 'skill-star-step',
          name: '星砂步',
          summary: '踏砂突进。',
          style: 'mobility',
          actionPreviewConfig: {
            folder: 'characters/shamian',
            prefix: 'skill_',
            frames: 8,
            basePath: '/assets/custom-world/shamian/skill',
            previewVideoPath: '/assets/custom-world/shamian/skill.mp4',
          },
        },
      ],
      initialItems: [
        {
          id: 'item-sand-compass',
          name: '星砂罗盘',
          category: '专属物品',
          quantity: 1,
          rarity: 'rare',
          description: '能指出旧约埋藏方向。',
          tags: [],
          iconSrc: '/assets/custom-world/items/sand-compass.png',
        },
      ],
      attributeProfile: {
        schemaId: 'schema-星砂废都',
        values: { axis_a: 8 },
        topTraits: ['星砂共鸣'],
        evidence: [
          { slotId: 'axis_a', reason: '能听见星砂潮汐。' },
        ],
      },
    } satisfies CustomWorldProfile['playableNpcs'][number];
    const fullProfile: CustomWorldProfile = {
      ...buildProfile('星砂废都'),
      id: 'profile-stardust-structure',
      cover: {
        sourceType: 'default',
        imageSrc: null,
        characterRoleIds: ['playable-shamian'],
      },
      playableNpcs: [fullRole],
    };
    const summaryProfile: CustomWorldProfile = {
      ...fullProfile,
      cover: null,
      playableNpcs: [
        {
          ...fullRole,
          skills: [
            {
              id: 'skill-star-step',
              name: '星砂步',
              summary: '踏砂突进。',
              style: 'mobility',
            },
          ],
          initialItems: [
            {
              ...fullRole.initialItems[0]!,
              iconSrc: undefined,
            },
          ],
          attributeProfile: undefined,
        },
      ],
    };
    const detailEntry = buildLibraryEntry(fullProfile);
    const summaryEntry = buildLibraryEntry(summaryProfile);
    const selectedEntries: CustomWorldLibraryEntry<CustomWorldProfile>[] = [];

    function Harness() {
      const [selectedDetailEntry, setSelectedDetailEntry] = useState<
        CustomWorldLibraryEntry<CustomWorldProfile> | null
      >(detailEntry);
      useEffect(() => {
        if (selectedDetailEntry) {
          selectedEntries.push(selectedDetailEntry);
        }
      }, [selectedDetailEntry]);
      useRpgEntryLibraryDetail({
        userId: 'user-1',
        selectedDetailEntry,
        setSelectedDetailEntry,
        savedCustomWorldEntries: [summaryEntry],
        setSavedCustomWorldEntries: vi.fn(),
        setGeneratedCustomWorldProfile: vi.fn(),
        setCustomWorldError: vi.fn(),
        setCustomWorldAutoSaveError: vi.fn(),
        setCustomWorldAutoSaveState: vi.fn(),
        setCustomWorldGenerationViewSource: vi.fn(),
        setCustomWorldResultViewSource: vi.fn(),
        setSelectionStage: vi.fn(),
        setPlatformTabToCreate: vi.fn(),
        setPlatformTabToDraft: vi.fn(),
        setPlatformError: vi.fn(),
        appendBrowseHistoryEntry: vi.fn(async () => {}),
        refreshCustomWorldWorks: vi.fn(async () => []),
        refreshPublishedGallery: vi.fn(async () => []),
        persistAgentUiState: vi.fn(),
        syncAgentCreationResultView: vi.fn(),
        buildDraftResultProfile: () => null,
        suppressAgentDraftResultAutoOpen: vi.fn(),
        releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
        resetAutoSaveTrackingToIdle: vi.fn(),
        markAutoSavedProfile: vi.fn(),
      });
      return null;
    }

    render(<Harness />);

    await act(async () => {});
    const lastSelected = selectedEntries.at(-1);
    expect(lastSelected?.profile.cover?.characterRoleIds).toEqual([
      'playable-shamian',
    ]);
    expect(
      lastSelected?.profile.playableNpcs[0]?.skills[0]?.actionPreviewConfig
        ?.previewVideoPath,
    ).toBe('/assets/custom-world/shamian/skill.mp4');
    expect(lastSelected?.profile.playableNpcs[0]?.initialItems[0]?.iconSrc).toBe(
      '/assets/custom-world/items/sand-compass.png',
    );
    expect(lastSelected?.profile.playableNpcs[0]?.attributeProfile?.values).toEqual(
      { axis_a: 8 },
    );
  });

  it('Agent 结果页自动保存先回写 session，再保存后端 result-view profile', async () => {
    const oldProfile = buildProfile('旧前端快照');
    const latestProfile = {
      ...buildProfile('服务端草稿快照'),
      summary: '自动保存应保存这份 session 最新草稿。',
    };
    const latestSession = buildSession({
      stage: 'object_refining',
      draftProfile: latestProfile as unknown as Record<string, unknown>,
    });
    const syncAgentSessionSnapshot = vi.fn(async () => latestSession);
    const syncAgentCreationResultView = vi.fn(async () =>
      buildResultView({
        session: latestSession,
        profile: latestProfile,
        profileSource: 'result_preview',
        targetStage: 'custom-world-result',
        resultViewSource: 'agent-draft',
        canAutosaveLibrary: true,
        canSyncResultProfile: true,
        recoveryAction: 'open_result',
      }),
    );
    vi.mocked(executeRpgCreationAction).mockResolvedValue({
      operation: {
        operationId: 'operation-sync-result',
        type: 'sync_result_profile',
        status: 'running',
        phaseLabel: '结果页同步中',
        phaseDetail: '正在同步结果页。',
        progress: 50,
      },
    });
    vi.mocked(getRpgCreationOperation).mockResolvedValue({
      operationId: 'operation-sync-result',
      type: 'sync_result_profile',
      status: 'completed',
      phaseLabel: '结果页已同步',
      phaseDetail: '结果页已同步。',
      progress: 100,
    });

    vi.mocked(upsertRpgWorldProfile).mockResolvedValue({
      entry: {
        ownerUserId: 'user-1',
        profileId: latestProfile.id,
        publicWorkCode: null,
        authorPublicUserCode: null,
        profile: latestProfile,
        visibility: 'draft',
        publishedAt: null,
        updatedAt: '2026-04-25T00:00:00.000Z',
        authorDisplayName: '测试玩家',
        worldName: latestProfile.name,
        subtitle: latestProfile.subtitle,
        summaryText: latestProfile.summary,
        coverImageSrc: null,
        themeMode: 'tide',
        playableNpcCount: 0,
        landmarkCount: 0,
        likeCount: 0,
      },
      entries: [],
    });

    function Harness() {
      useRpgCreationResultAutosave({
        selectionStage: 'custom-world-result',
        activeAgentSessionId: 'agent-session-1',
        generatedCustomWorldProfile: oldProfile,
        isAgentDraftResultView: true,
        userId: 'user-1',
        setGeneratedCustomWorldProfile: vi.fn(),
        setAgentOperation: vi.fn(),
        setSavedCustomWorldEntries: vi.fn(),
        setSelectedDetailEntry: vi.fn(),
        refreshCustomWorldWorks: vi.fn(async () => []),
        persistAgentUiState: vi.fn(),
        syncAgentSessionSnapshot,
        syncAgentCreationResultView,
        buildDraftResultProfile: (view) =>
          (view?.profile as CustomWorldProfile | null) ?? null,
      });

      return null;
    }

    vi.useFakeTimers();
    render(<Harness />);

    await act(async () => {
      await vi.advanceTimersByTimeAsync(650);
    });
    vi.useRealTimers();

    expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1');
    expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1');
    expect(executeRpgCreationAction).toHaveBeenCalledWith('agent-session-1', {
      action: 'sync_result_profile',
      profile: expect.objectContaining({
        id: oldProfile.id,
        name: oldProfile.name,
      }),
    });
    expect(upsertRpgWorldProfile).toHaveBeenCalledWith(
      expect.objectContaining({
        id: latestProfile.id,
        name: latestProfile.name,
        summary: latestProfile.summary,
      }),
      {
        sourceAgentSessionId: 'agent-session-1',
      },
    );
  });
});
