/* @vitest-environment jsdom */

import { fireEvent, render, screen, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';

import {
  AnimationState,
  type Character,
  type CompanionRenderState,
  type Encounter,
  type EquipmentLoadout,
  type GameState,
  WorldType,
} from '../types';
import { AdventureEntityModal } from './AdventureEntityModal';

vi.mock('./CharacterAnimator', () => ({
  CharacterAnimator: () => <div data-testid="character-portrait" />,
}));

vi.mock('./MedievalNpcAnimator', () => ({
  MedievalNpcAnimator: () => <div data-testid="medieval-npc-portrait" />,
}));

vi.mock('./HostileNpcAnimator', () => ({
  HostileNpcAnimator: () => <div data-testid="hostile-npc-portrait" />,
}));

function createGameState(overrides: Partial<GameState> = {}): GameState {
  return {
    worldType: WorldType.WUXIA,
    customWorldProfile: null,
    playerCharacter: null,
    runtimeStats: {
      playTimeMs: 0,
      lastPlayTickAt: null,
      hostileNpcsDefeated: 0,
      questsAccepted: 0,
      itemsUsed: 0,
      scenesTraveled: 0,
    },
    currentScene: 'test-scene',
    storyHistory: [],
    characterChats: {},
    animationState: AnimationState.IDLE,
    currentEncounter: null,
    npcInteractionActive: false,
    currentScenePreset: null,
    sceneHostileNpcs: [],
    playerX: 0,
    playerOffsetY: 0,
    playerFacing: 'right',
    playerActionMode: 'idle',
    scrollWorld: false,
    inBattle: false,
    playerHp: 100,
    playerMaxHp: 100,
    playerMana: 30,
    playerMaxMana: 30,
    playerSkillCooldowns: {},
    activeCombatEffects: [],
    playerCurrency: 0,
    playerInventory: [],
    playerEquipment: {} as EquipmentLoadout,
    npcStates: {},
    quests: [],
    roster: [],
    companions: [],
    currentBattleNpcId: null,
    currentNpcBattleMode: null,
    currentNpcBattleOutcome: null,
    sparReturnEncounter: null,
    sparPlayerHpBefore: null,
    sparPlayerMaxHpBefore: null,
    sparStoryHistoryBefore: null,
    ...overrides,
  } as GameState;
}

function createEncounter(overrides: Partial<Encounter> = {}): Encounter {
  return {
    id: 'runtime-npc',
    kind: 'npc',
    npcName: '雾中来客',
    npcDescription: '带着临时生成形象的相遇者',
    npcAvatar: '/avatar.png',
    context: '桥边试探',
    ...overrides,
  };
}

function createPlayerCharacter(): Character {
  return {
    id: 'player-1',
    name: '潮刃客',
    title: '试剑者',
    description: '测试主角',
    backstory: '测试背景',
    personality: '冷静',
    avatar: '',
    portrait: '',
    assetFolder: '',
    assetVariant: '',
    attributes: {
      strength: 5,
      agility: 5,
      intelligence: 5,
      spirit: 5,
    },
    skills: [
      {
        id: 'tide-slash',
        name: '潮刃突进',
        animation: AnimationState.ATTACK,
        damage: 16,
        manaCost: 5,
        cooldownTurns: 2,
        range: 1,
        style: 'burst',
        buildBuffs: [
          {
            id: 'wet-mark',
            sourceType: 'skill',
            sourceId: 'tide-slash',
            name: '潮湿',
            tags: ['控制', '潮汐'],
            durationTurns: 2,
          },
        ],
      },
    ],
    adventureOpenings: {},
  };
}

function createCompanionRenderState(
  character: Character,
): CompanionRenderState {
  return {
    npcId: 'companion-1',
    character,
    hp: 100,
    maxHp: 100,
    mana: 20,
    maxMana: 20,
    skillCooldowns: {},
    animationState: AnimationState.IDLE,
    slot: 'upper',
  };
}

afterEach(() => {
  vi.restoreAllMocks();
});

test('NPC 详情立绘优先展示遭遇实例形象，而不是 characterId 对应预设', () => {
  const encounter = createEncounter({
    characterId: 'sword-princess',
    imageSrc: '/runtime-npc-preview.png',
  });

  render(
    <AdventureEntityModal
      selection={{ kind: 'npc', encounter }}
      gameState={createGameState()}
      onClose={() => undefined}
    />,
  );

  const portrait = screen.getByAltText('雾中来客');

  expect(portrait.getAttribute('src')).toBe('/runtime-npc-preview.png');
  expect(screen.queryByTestId('character-portrait')).toBeNull();
});

test('NPC 背包物品空 id 会被规范成稳定渲染 id', () => {
  const consoleErrorSpy = vi
    .spyOn(console, 'error')
    .mockImplementation(() => undefined);
  const encounter = createEncounter();

  render(
    <AdventureEntityModal
      selection={{ kind: 'npc', encounter }}
      gameState={createGameState({
        npcStates: {
          'runtime-npc': {
            affinity: 0,
            relationState: { affinity: 0, stance: 'neutral' },
            helpUsed: false,
            chattedCount: 0,
            giftsGiven: 0,
            inventory: [
              {
                id: '',
                category: '材料',
                name: '裂纹石片',
                quantity: 1,
                rarity: 'common',
                tags: [],
              },
              {
                id: '',
                category: '材料',
                name: '裂纹石片',
                quantity: 2,
                rarity: 'common',
                tags: [],
              },
            ],
            recruited: false,
            revealedFacts: [],
            knownAttributeRumors: [],
            firstMeaningfulContactResolved: false,
            seenBackstoryChapterIds: [],
          },
        },
      })}
      onClose={() => undefined}
    />,
  );

  expect(screen.getAllByTitle(/裂纹石片 x/)).toHaveLength(2);
  expect(
    consoleErrorSpy.mock.calls.some((call) =>
      call.some(
        (arg) =>
          typeof arg === 'string' &&
          arg.includes('Encountered two children with the same key'),
      ),
    ),
  ).toBe(false);
});

test('物品空态复用暗色 PlatformEmptyState chrome', () => {
  render(
    <AdventureEntityModal
      selection={{ kind: 'player' }}
      gameState={createGameState({
        playerCharacter: createPlayerCharacter(),
        playerInventory: [],
      })}
      onClose={() => undefined}
    />,
  );

  const emptyState = screen.getByText('暂无物品');
  const attributeSection = screen.getByText('属性').closest('section');
  const itemSection = screen.getByText('物品').closest('section');

  expect(emptyState.className).toContain('platform-empty-state');
  expect(emptyState.className).toContain('border-dashed');
  expect(emptyState.className).toContain('bg-black/20');
  expect(attributeSection?.className).toContain('border-white/10');
  expect(attributeSection?.className).toContain('bg-black/25');
  expect(itemSection?.className).toContain('border-white/10');
  expect(itemSection?.className).toContain('bg-black/25');

  const levelPanel = screen.getByTestId('player-level-panel');

  expect(levelPanel.className).toContain('border-amber-300/18');
  expect(levelPanel.className).toContain('bg-amber-500/8');
  expect(levelPanel.className).toContain('rounded-xl');
});

test('最近回响纯展示小卡复用暗色 PlatformSubpanel chrome', () => {
  render(
    <AdventureEntityModal
      selection={{ kind: 'player' }}
      gameState={createGameState({
        playerCharacter: createPlayerCharacter(),
        playerInventory: [
          {
            id: 'echo-shell',
            category: '材料',
            name: '回声贝壳',
            quantity: 1,
            rarity: 'rare',
            tags: [],
            runtimeMetadata: {
              origin: 'procedural',
              generationChannel: 'discovery',
              seedKey: 'echo-shell-seed',
              sourceReason: '测试最近回响载体',
              storyFingerprint: {
                visibleClue: '贝壳里仍有潮声回响',
                witnessMark: '潮痕',
                unresolvedQuestion: '潮声为何未散',
                currentAppearanceReason: '被最近回响唤醒',
                relatedThreadIds: [],
                relatedScarIds: [],
                reactionHooks: [],
              },
            },
          },
        ],
        currentScenePreset: {
          narrativeResidues: [
            {
              id: 'residue-1',
              title: '墙上残痕',
              visibleClue: '刻着潮汐暗号。',
            },
          ],
        } as unknown as GameState['currentScenePreset'],
        storyEngineMemory: {
          chronicle: [
            {
              id: 'chronicle-1',
              title: '潮声编年',
              summary: '潮声把旧约刻回墙面。',
            },
          ],
          recentCarrierIds: ['echo-shell'],
          consequenceLedger: [
            {
              id: 'consequence-1',
              title: '旧约后果',
              summary: '盟约开始反噬。',
              relatedIds: ['player-1'],
            },
          ],
        } as unknown as GameState['storyEngineMemory'],
      })}
      onClose={() => undefined}
    />,
  );

  [
    'recent-consequence-echo',
    'recent-chronicle-echo',
    'recent-carrier-echo',
    'recent-scene-residue-echo',
  ].forEach((testId) => {
    const panel = screen.getByTestId(testId);

    expect(panel.className).toContain('border-white/10');
    expect(panel.className).toContain('bg-black/25');
    expect(panel.className).toContain('rounded-xl');
  });
});

test('私聊和队友收束复用暗色 tint PlatformSubpanel chrome', () => {
  const companionCharacter = createPlayerCharacter();

  render(
    <AdventureEntityModal
      selection={{
        kind: 'companion',
        companion: createCompanionRenderState(companionCharacter),
      }}
      gameState={createGameState({
        companions: [
          {
            npcId: 'companion-1',
            characterId: companionCharacter.id,
            joinedAtAffinity: 100,
            hp: 100,
            maxHp: 100,
            mana: 20,
            maxMana: 20,
            skillCooldowns: {},
          },
        ],
        npcStates: {
          'companion-1': {
            affinity: 100,
            relationState: { affinity: 100, stance: 'bonded' },
            helpUsed: false,
            chattedCount: 0,
            giftsGiven: 0,
            inventory: [],
            recruited: true,
            revealedFacts: [],
            knownAttributeRumors: [],
            firstMeaningfulContactResolved: true,
            seenBackstoryChapterIds: [],
          },
        },
        storyEngineMemory: {
          companionResolutions: [
            {
              characterId: companionCharacter.id,
              resolutionType: 'bonded',
              summary: '潮声与同行者完成誓约。',
              relatedThreadIds: ['thread-1'],
            },
          ],
        } as unknown as GameState['storyEngineMemory'],
      })}
      onClose={() => undefined}
      onOpenCharacterChat={() => undefined}
    />,
  );

  const privateChatPanel = screen.getByTestId('private-chat-panel');
  const companionResolutionEcho = screen.getByTestId(
    'companion-resolution-echo',
  );

  expect(privateChatPanel.className).toContain('border-sky-400/18');
  expect(privateChatPanel.className).toContain('bg-sky-500/8');
  expect(privateChatPanel.className).toContain('rounded-[1.35rem]');
  expect(companionResolutionEcho.className).toContain('border-emerald-400/18');
  expect(companionResolutionEcho.className).toContain('bg-emerald-500/8');
  expect(companionResolutionEcho.className).toContain('rounded-xl');
});

test('技能详情静态标签复用暗色 PlatformPillBadge chrome', () => {
  render(
    <AdventureEntityModal
      selection={{ kind: 'player' }}
      gameState={createGameState({
        playerCharacter: createPlayerCharacter(),
      })}
      onClose={() => undefined}
    />,
  );

  fireEvent.click(screen.getByRole('button', { name: /潮刃突进/u }));

  const skillPanel = screen
    .getByText('技能详情')
    .closest('.pixel-modal-shell') as HTMLElement;

  const deliveryBadge = within(skillPanel).getAllByText('近战')[0]!;
  const styleBadge = within(skillPanel).getAllByText('爆发')[0]!;
  const buffSummaryBadge = within(skillPanel).getByText('附带 1 个状态标签');
  const buffBadge = within(skillPanel).getByText('潮湿 / 控制、潮汐 / 2 回合');
  const damagePanel = within(skillPanel)
    .getByText('伤害')
    .closest('section') as HTMLElement;
  const descriptionPanel = within(skillPanel)
    .getByText(/潮刃突进 属于爆发路线/u)
    .closest('section') as HTMLElement;
  const buffPanel = within(skillPanel)
    .getByText('附带状态标签')
    .closest('section') as HTMLElement;

  expect(deliveryBadge.className).toContain('bg-white/6');
  expect(styleBadge.className).toContain('bg-sky-500/10');
  expect(buffSummaryBadge.className).toContain('bg-emerald-500/10');
  expect(buffBadge.className).toContain('rounded-full');
  expect(buffBadge.className).toContain('bg-sky-500/10');
  expect(damagePanel.className).toContain('bg-black/25');
  expect(damagePanel.className).toContain('border-white/10');
  expect(descriptionPanel.className).toContain('bg-black/25');
  expect(buffPanel.className).toContain('bg-black/25');
});
