import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';

import {
  AnimationState,
  type Character,
  type Encounter,
  type SceneHostileNpc,
} from '../../types';
import {
  GameCanvasEntityLayer,
  getCombatFloatingNumberPresentation,
} from './GameCanvasEntityLayer';
import {
  CHARACTER_COMBAT_HP_TOP_PX,
  ENTITY_CONTAINER_REM,
  getEncounterCharacterBottomOffsetPx,
  getEncounterCharacterOpponentBottom,
  getHostileNpcSceneBottomOffsetPx,
  getMirroredStageEntityLeft,
  getMonsterWorldLeft,
  getNpcCombatHpTop,
  getSceneNpcVisualBottomOffsetPx,
  MONSTER_COMBAT_HP_TOP_PX,
} from './GameCanvasShared';

function createCharacter(): Character {
  return {
    id: 'hero',
    name: '沈行',
    title: '试剑客',
    description: '测试主角',
    backstory: '测试背景',
    avatar: '/hero.png',
    portrait: '/hero.png',
    assetFolder: 'hero',
    assetVariant: 'default',
    attributes: {
      strength: 10,
      agility: 10,
      intelligence: 8,
      spirit: 9,
    },
    personality: 'calm',
    skills: [],
    adventureOpenings: {},
  } as Character;
}

function createEncounter(overrides: Partial<Encounter> = {}): Encounter {
  return {
    id: 'npc-liu',
    kind: 'npc',
    npcName: '柳无声',
    npcDescription: '桥口旧识',
    npcAvatar: '/npc-liu.png',
    context: '断桥',
    ...overrides,
  };
}

function createHostileNpc(overrides: Partial<SceneHostileNpc> = {}): SceneHostileNpc {
  return {
    id: 'npc-liu',
    name: '柳无声',
    action: '对峙',
    description: '桥口旧识',
    animation: 'idle',
    xMeters: 3,
    yOffset: 0,
    facing: 'left',
    attackRange: 1,
    speed: 1,
    hp: 10,
    maxHp: 10,
    encounter: createEncounter(),
    ...overrides,
  };
}

function renderEntityLayer(effectNpcId: string | null) {
  return renderToStaticMarkup(
    <GameCanvasEntityLayer
      companions={[]}
      sceneActAmbientEncounters={[]}
      currentScenePreset={null}
      sceneTransitionToken={0}
      isSceneTransitionEntering={false}
      isSceneTransitionExiting={false}
      transitionSweepPx={320}
      sceneTransitionExitDurationS={0.2}
      sceneTransitionEntryDurationS={0.2}
      companionAnchorLeft="10%"
      companionAnchorBottom="20%"
      playerBottomOffsetPx={0}
      sceneTransitionPhase="idle"
      inBattle={false}
      onEntitySelect={null}
      playerLeft="20%"
      playerCharacter={createCharacter()}
      playerHp={100}
      playerMaxHp={100}
      effectivePlayerFacing="right"
      effectivePlayerAnimationState={AnimationState.IDLE}
      shouldShowPlayerDialogueIcon={false}
      dialogueIndicator={null}
      npcAffinityEffect={
        effectNpcId
          ? {
              eventId: 'effect-1',
              npcId: effectNpcId,
              delta: 3,
            }
          : null
      }
      sceneCombatants={[createHostileNpc()]}
      monsters={[]}
      getHostileNpcOuterLeft={() => '70%'}
      groundBottom="18%"
      stageLiftPx={68}
      encounter={null}
      sideAnchor="15%"
      cameraAnchorX={0}
      monsterAnchorMeters={3.2}
      playerX={0}
    />,
  );
}

describe('GameCanvasEntityLayer', () => {
  it('keeps combat floating numbers readable on dark noisy battle backgrounds', () => {
    const damage = getCombatFloatingNumberPresentation(false);
    const healing = getCombatFloatingNumberPresentation(true);

    expect(damage.toneClass).toContain('bg-rose-950/72');
    expect(damage.toneClass).toContain('text-rose-50');
    expect(damage.textStyle.WebkitTextStroke).toContain('rgba(127, 29, 29');
    expect(damage.textStyle.textShadow).toContain('rgba(0, 0, 0');

    expect(healing.toneClass).toContain('bg-emerald-950/70');
    expect(healing.toneClass).toContain('text-emerald-50');
    expect(healing.textStyle.WebkitTextStroke).toContain('rgba(6, 78, 59');
    expect(healing.textStyle.textShadow).toContain('rgba(0, 0, 0');
  });

  it('uses mirrored stage anchors for player and opponent containers', () => {
    expect(getMirroredStageEntityLeft('15%', 'player')).toBe('15%');
    expect(getMirroredStageEntityLeft('15%', 'opponent')).toBe(`calc(100% - 15% - ${ENTITY_CONTAINER_REM}rem)`);
  });

  it('lowers large monster sprites to the shared scene ground line', () => {
    expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 62})).toBe(-78);
    expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 46})).toBe(-68);
    expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 37})).toBe(-52);
    expect(getHostileNpcSceneBottomOffsetPx({frameHeight: 23})).toBe(-28);
  });

  it('uses scene npc visual anchors instead of template character foot offsets', () => {
    const sceneNpcEncounter = createEncounter({
      characterId: 'hero',
      monsterPresetId: 'monster-20',
      imageSrc: '/generated-custom-world-npc/shark.png',
    });
    const character = createCharacter();

    expect(getEncounterCharacterOpponentBottom('18%', 68, sceneNpcEncounter, character))
      .toBe('calc(18% + 68px - 78px)');
    expect(getEncounterCharacterBottomOffsetPx(68, sceneNpcEncounter, character))
      .toBe(-10);
  });

  it('lowers scene npc custom visuals even without character ids', () => {
    const sceneNpcEncounter = createEncounter({
      visual: {
        race: 'elf',
        bodyColor: 'blue',
        headIndex: 0,
        hairColorIndex: 1,
        hairStyleFrame: 2,
        facialHairEnabled: false,
        facialHairColorIndex: 0,
        facialHairStyleFrame: 0,
      },
    });

    expect(getSceneNpcVisualBottomOffsetPx(sceneNpcEncounter)).toBe(-78);
  });

  it('keeps combat hp bars above character and monster silhouettes', () => {
    expect(getNpcCombatHpTop('hero', null)).toBe(CHARACTER_COMBAT_HP_TOP_PX);
    expect(getNpcCombatHpTop(null, 'monster-20')).toBe(MONSTER_COMBAT_HP_TOP_PX);
    expect(getNpcCombatHpTop(null, null)).toBe(CHARACTER_COMBAT_HP_TOP_PX);
  });

  it('does not apply scene visual ground offset twice for custom character enemies in battle', () => {
    const hostileNpc = createHostileNpc({
      encounter: createEncounter({
        id: 'npc-shark-elder',
        npcName: '珊瑚长老',
        characterId: 'hero',
        imageSrc: '/generated-custom-world-npc/shark-elder.png',
      }),
    });

    const html = renderToStaticMarkup(
      <GameCanvasEntityLayer
        companions={[]}
        sceneActAmbientEncounters={[]}
        currentScenePreset={null}
        sceneTransitionToken={0}
        isSceneTransitionEntering={false}
        isSceneTransitionExiting={false}
        transitionSweepPx={320}
        sceneTransitionExitDurationS={0.2}
        sceneTransitionEntryDurationS={0.2}
        companionAnchorLeft="10%"
        companionAnchorBottom="20%"
        playerBottomOffsetPx={0}
        sceneTransitionPhase="idle"
        inBattle={true}
        onEntitySelect={null}
        playerLeft="20%"
        playerCharacter={createCharacter()}
        playerHp={100}
        playerMaxHp={100}
        effectivePlayerFacing="right"
        effectivePlayerAnimationState={AnimationState.IDLE}
        shouldShowPlayerDialogueIcon={false}
        dialogueIndicator={null}
        npcAffinityEffect={null}
        sceneCombatants={[hostileNpc]}
        monsters={[]}
        getHostileNpcOuterLeft={() => '70%'}
        groundBottom="18%"
        stageLiftPx={68}
        encounter={null}
        sideAnchor="15%"
        cameraAnchorX={0}
        monsterAnchorMeters={3.2}
        playerX={0}
      />,
    );

    expect(html).toContain('bottom:calc(calc(18% + 68px) + -78px)');
    expect(html).not.toContain('bottom:calc(calc(18% + 68px - 78px) + -78px)');
  });

  it('renders affinity effect on the matching hostile npc', () => {
    const html = renderEntityLayer('npc-liu');

    expect(html).toContain('data-testid="npc-affinity-effect-npc-liu"');
    expect(html).toContain('aria-label="好感度变化 +3"');
  });

  it('keeps battle opponent visible when compat payload misses encounter context', () => {
    const hostileNpc = createHostileNpc({
      encounter: undefined,
      name: '断桥匪首',
      description: '刚进入战斗时的旧快照目标',
    });
    const html = renderToStaticMarkup(
      <GameCanvasEntityLayer
        companions={[]}
        sceneActAmbientEncounters={[]}
        currentScenePreset={null}
        sceneTransitionToken={0}
        isSceneTransitionEntering={false}
        isSceneTransitionExiting={false}
        transitionSweepPx={320}
        sceneTransitionExitDurationS={0.2}
        sceneTransitionEntryDurationS={0.2}
        companionAnchorLeft="10%"
        companionAnchorBottom="20%"
        playerBottomOffsetPx={0}
        sceneTransitionPhase="idle"
        inBattle={true}
        onEntitySelect={null}
        playerLeft="20%"
        playerCharacter={createCharacter()}
        playerHp={100}
        playerMaxHp={100}
        effectivePlayerFacing="right"
        effectivePlayerAnimationState={AnimationState.IDLE}
        shouldShowPlayerDialogueIcon={false}
        dialogueIndicator={null}
        npcAffinityEffect={null}
        sceneCombatants={[hostileNpc]}
        monsters={[]}
        getHostileNpcOuterLeft={() => '70%'}
        groundBottom="18%"
        stageLiftPx={68}
        encounter={null}
        sideAnchor="15%"
        cameraAnchorX={0}
        monsterAnchorMeters={3.2}
        playerX={0}
      />,
    );

    expect(html).toContain('查看断桥匪首详情');
    expect(html).toContain('from-rose-500 to-red-400');
  });

  it('does not render affinity effect on a different npc', () => {
    const html = renderEntityLayer('npc-other');

    expect(html).not.toContain('npc-affinity-effect-npc-liu');
    expect(html).not.toContain('好感度变化 +3');
  });

  it('keeps hostile combat hp bar visible during post-hit afterimage frames', () => {
    const html = renderToStaticMarkup(
      <GameCanvasEntityLayer
        companions={[]}
        sceneActAmbientEncounters={[]}
        currentScenePreset={null}
        sceneTransitionToken={0}
        isSceneTransitionEntering={false}
        isSceneTransitionExiting={false}
        transitionSweepPx={320}
        sceneTransitionExitDurationS={0.2}
        sceneTransitionEntryDurationS={0.2}
        companionAnchorLeft="10%"
        companionAnchorBottom="20%"
        playerBottomOffsetPx={0}
        sceneTransitionPhase="idle"
        inBattle={false}
        onEntitySelect={null}
        playerLeft="20%"
        playerCharacter={createCharacter()}
        playerHp={100}
        playerMaxHp={100}
        effectivePlayerFacing="right"
        effectivePlayerAnimationState={AnimationState.IDLE}
        shouldShowPlayerDialogueIcon={false}
        dialogueIndicator={null}
        npcAffinityEffect={null}
        sceneCombatants={[
          createHostileNpc({
            hp: 4,
            maxHp: 10,
            animation: 'die',
          }),
        ]}
        monsters={[]}
        getHostileNpcOuterLeft={() => '70%'}
        groundBottom="18%"
        stageLiftPx={68}
        encounter={null}
        sideAnchor="15%"
        cameraAnchorX={0}
        monsterAnchorMeters={3.2}
        playerX={0}
      />,
    );

    expect(html).toContain('from-rose-500 to-red-400');
  });

  it('renders scene act back-row encounters alongside the primary encounter', () => {
    const html = renderToStaticMarkup(
      <GameCanvasEntityLayer
        companions={[]}
        sceneActAmbientEncounters={[
          createEncounter({ id: 'npc-back-1', npcName: '后排甲' }),
          createEncounter({ id: 'npc-back-2', npcName: '后排乙' }),
        ]}
        currentScenePreset={null}
        sceneTransitionToken={0}
        isSceneTransitionEntering={false}
        isSceneTransitionExiting={false}
        transitionSweepPx={320}
        sceneTransitionExitDurationS={0.2}
        sceneTransitionEntryDurationS={0.2}
        companionAnchorLeft="10%"
        companionAnchorBottom="20%"
        playerBottomOffsetPx={0}
        sceneTransitionPhase="idle"
        inBattle={false}
        onEntitySelect={null}
        playerLeft="20%"
        playerCharacter={createCharacter()}
        playerHp={100}
        playerMaxHp={100}
        effectivePlayerFacing="right"
        effectivePlayerAnimationState={AnimationState.IDLE}
        shouldShowPlayerDialogueIcon={false}
        dialogueIndicator={null}
        npcAffinityEffect={null}
        sceneCombatants={[]}
        monsters={[]}
        getHostileNpcOuterLeft={() => '70%'}
        groundBottom="18%"
        stageLiftPx={68}
        encounter={createEncounter({ id: 'npc-primary', npcName: '主角色' })}
        sideAnchor="15%"
        cameraAnchorX={0}
        monsterAnchorMeters={3.2}
        playerX={0}
      />,
    );

    expect(html).toContain('查看主角色详情');
    expect(html).toContain('查看后排甲详情');
    expect(html).toContain('查看后排乙详情');
  });

  it('hides opposite scene actors while the player exits for a scene transition', () => {
    const html = renderToStaticMarkup(
      <GameCanvasEntityLayer
        companions={[]}
        sceneActAmbientEncounters={[
          createEncounter({ id: 'npc-back-1', npcName: '后排甲' }),
        ]}
        currentScenePreset={null}
        sceneTransitionToken={1}
        isSceneTransitionEntering={false}
        isSceneTransitionExiting={true}
        transitionSweepPx={320}
        sceneTransitionExitDurationS={0.2}
        sceneTransitionEntryDurationS={0.2}
        companionAnchorLeft="10%"
        companionAnchorBottom="20%"
        playerBottomOffsetPx={0}
        sceneTransitionPhase="exiting"
        inBattle={false}
        onEntitySelect={null}
        playerLeft="20%"
        playerCharacter={createCharacter()}
        playerHp={100}
        playerMaxHp={100}
        effectivePlayerFacing="right"
        effectivePlayerAnimationState={AnimationState.RUN}
        shouldShowPlayerDialogueIcon={false}
        dialogueIndicator={null}
        npcAffinityEffect={null}
        sceneCombatants={[createHostileNpc({ name: '旧场景敌人' })]}
        monsters={[]}
        getHostileNpcOuterLeft={() => '70%'}
        groundBottom="18%"
        stageLiftPx={68}
        encounter={createEncounter({ id: 'npc-primary', npcName: '主角色' })}
        sideAnchor="15%"
        cameraAnchorX={0}
        monsterAnchorMeters={3.2}
        playerX={0}
      />,
    );

    expect(html).not.toContain('查看旧场景敌人详情');
    expect(html).not.toContain('查看主角色详情');
    expect(html).not.toContain('查看后排甲详情');
  });

  it('keeps hostile combatant identity stable while attack position changes', () => {
    const sideAnchor = '15%';
    const cameraAnchorX = 0;
    const monsterAnchorMeters = 3.2;
    const attackingNpc = createHostileNpc({
      id: 'npc-attacker',
      xMeters: 0.1,
      animation: 'attack',
      combatMode: 'melee',
      encounter: createEncounter({
        id: 'npc-attacker',
        npcName: '突进敌人',
      }),
    });

    const renderedLeft = getMonsterWorldLeft(
      sideAnchor,
      attackingNpc.xMeters,
      cameraAnchorX,
      monsterAnchorMeters,
    );

    const html = renderToStaticMarkup(
      <GameCanvasEntityLayer
        companions={[]}
        sceneActAmbientEncounters={[]}
        currentScenePreset={null}
        sceneTransitionToken={0}
        isSceneTransitionEntering={false}
        isSceneTransitionExiting={false}
        transitionSweepPx={320}
        sceneTransitionExitDurationS={0.2}
        sceneTransitionEntryDurationS={0.2}
        companionAnchorLeft="10%"
        companionAnchorBottom="20%"
        playerBottomOffsetPx={0}
        sceneTransitionPhase="idle"
        inBattle={true}
        onEntitySelect={null}
        playerLeft="20%"
        playerCharacter={createCharacter()}
        playerHp={100}
        playerMaxHp={100}
        effectivePlayerFacing="right"
        effectivePlayerAnimationState={AnimationState.IDLE}
        shouldShowPlayerDialogueIcon={false}
        dialogueIndicator={null}
        npcAffinityEffect={null}
        sceneCombatants={[attackingNpc]}
        monsters={[]}
        getHostileNpcOuterLeft={(hostileNpc) =>
          getMonsterWorldLeft(
            sideAnchor,
            hostileNpc.xMeters,
            cameraAnchorX,
            monsterAnchorMeters,
          )
        }
        groundBottom="18%"
        stageLiftPx={68}
        encounter={null}
        sideAnchor={sideAnchor}
        cameraAnchorX={cameraAnchorX}
        monsterAnchorMeters={monsterAnchorMeters}
        playerX={0}
      />,
    );

    expect(html).toContain(`left:${renderedLeft}`);
  });

  it('keeps enemy formation positions when battle starts before any attack dash', () => {
    const sideAnchor = '15%';
    const cameraAnchorX = 0;
    const monsterAnchorMeters = 3.2;
    const frontNpc = createHostileNpc({
      id: 'npc-front',
      xMeters: 3.2,
      encounter: createEncounter({
        id: 'npc-front',
        npcName: '前排敌人',
      }),
    });
    const backNpc = createHostileNpc({
      id: 'npc-back',
      xMeters: 4.28,
      yOffset: 62,
      encounter: createEncounter({
        id: 'npc-back',
        npcName: '后排敌人',
      }),
    });

    const html = renderToStaticMarkup(
      <GameCanvasEntityLayer
        companions={[]}
        sceneActAmbientEncounters={[]}
        currentScenePreset={null}
        sceneTransitionToken={0}
        isSceneTransitionEntering={false}
        isSceneTransitionExiting={false}
        transitionSweepPx={320}
        sceneTransitionExitDurationS={0.2}
        sceneTransitionEntryDurationS={0.2}
        companionAnchorLeft="10%"
        companionAnchorBottom="20%"
        playerBottomOffsetPx={0}
        sceneTransitionPhase="idle"
        inBattle={true}
        onEntitySelect={null}
        playerLeft="20%"
        playerCharacter={createCharacter()}
        playerHp={100}
        playerMaxHp={100}
        effectivePlayerFacing="right"
        effectivePlayerAnimationState={AnimationState.IDLE}
        shouldShowPlayerDialogueIcon={false}
        dialogueIndicator={null}
        npcAffinityEffect={null}
        sceneCombatants={[frontNpc, backNpc]}
        monsters={[]}
        getHostileNpcOuterLeft={(hostileNpc) =>
          getMonsterWorldLeft(
            sideAnchor,
            hostileNpc.xMeters,
            cameraAnchorX,
            monsterAnchorMeters,
          )
        }
        groundBottom="18%"
        stageLiftPx={68}
        encounter={null}
        sideAnchor={sideAnchor}
        cameraAnchorX={cameraAnchorX}
        monsterAnchorMeters={monsterAnchorMeters}
        playerX={0}
      />,
    );

    const frontLeft = `left:${getMonsterWorldLeft(
      sideAnchor,
      frontNpc.xMeters,
      cameraAnchorX,
      monsterAnchorMeters,
    )}`;
    const backLeft = `left:${getMonsterWorldLeft(
      sideAnchor,
      backNpc.xMeters,
      cameraAnchorX,
      monsterAnchorMeters,
    )}`;

    expect(html).toContain(frontLeft);
    expect(html).toContain(backLeft);
  });
});
