// ─── Golem Combat Actions Regression Tests ────────────────────────────────────
// Issue #313: Tests the 5 fixes applied to processGolemAttacks:
// 1. Spell damage uses actual SPELLS_DEF[spellId].dmg * frame.magicAffinity
// 2. Spell mana cost uses actual SPELLS_DEF[spellId].cost.amount
// 3. Elemental matchup applied to basic attacks
// 4. Enchantment effects applied to basic attacks
// 5. Armor pierce bypasses armor fraction instead of multiplying damage
//

import { describe, it, expect } from 'vitest';
import {
  CORES, FRAMES, MIND_CIRCUITS, GOLEM_ENCHANTMENTS,
} from '@/lib/game/data/golems';
import { SPELLS_DEF } from '@/lib/game/constants';
import type { GolemDesign, SerializedDesign } from '@/lib/game/data/golems/types';
import type { EnemyState, RuntimeActiveGolem } from '@/lib/game/types';
import { processGolemAttacks } from './golem-combat-actions';
import { computeBasicAttackDamage } from './golem-combat-helpers';

// ─── Helpers ──────────────────────────────────────────────────────────────────

function makeDesign(
  coreId: string,
  frameId: string,
  circuitId: string,
  enchantIds: string[] = [],
  selectedSpells: string[] = [],
): GolemDesign {
  return {
    id: `test_${coreId}_${frameId}_${circuitId}`,
    name: `Test ${coreId} ${frameId}`,
    core: CORES[coreId],
    frame: FRAMES[frameId],
    mindCircuit: MIND_CIRCUITS[circuitId],
    enchantments: enchantIds.map(id => GOLEM_ENCHANTMENTS[id]).filter(Boolean),
    selectedManaTypes: [],
    selectedSpells,
  };
}

function makeSerialized(design: GolemDesign): SerializedDesign {
  return {
    id: design.id,
    name: design.name,
    coreId: design.core.id,
    frameId: design.frame.id,
    mindCircuitId: design.mindCircuit.id,
    enchantmentIds: design.enchantments.map(e => e.id),
    selectedManaTypes: design.selectedManaTypes,
    selectedSpells: design.selectedSpells,
  };
}

function makeActiveGolem(design: GolemDesign, currentMana?: number, attackProgress = 0): RuntimeActiveGolem {
  return {
    designId: design.id,
    summonedFloor: 1,
    attackProgress,
    roomsRemaining: 3,
    currentMana: currentMana ?? design.core.manaCapacity,
    spellCastIndex: 0,
  };
}

function makeEnemy(overrides: Partial<EnemyState> = {}): EnemyState {
  return {
    id: 'enemy-1',
    name: 'Test Enemy',
    hp: 100,
    maxHP: 100,
    armor: 0.2,
    dodgeChance: 0,
    element: 'fire',
    activeEffects: [],
    effectiveArmor: 0.2,
    ...overrides,
  };
}

// Helper: run processGolemAttacks with attackProgress pre-set to trigger attacks
function runGolemAttacks(
  golem: RuntimeActiveGolem,
  serialized: SerializedDesign,
  design: GolemDesign,
  enemy: EnemyState,
  enemyElement: string,
  onDamage?: (dmg: number) => { rawMana: number; elements: Record<string, never> },
) {
  let capturedDamage = 0;
  const result = processGolemAttacks(
    [golem],
    { [design.id]: serialized },
    onDamage ?? ((dmg) => { capturedDamage = dmg; return { rawMana: 0, elements: {} }; }),
    () => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
    enemyElement,
    () => enemy,
    () => [enemy],
    () => {},
    100,
    {},
  );
  return { result, capturedDamage };
}

// ─── Fix #1 & #2: Spell damage and mana cost ─────────────────────────────────

describe('processGolemAttacks - spell damage and mana cost (fixes #1, #2)', () => {
  it('uses actual spell damage (SPELLS_DEF.dmg * magicAffinity) not hardcoded 20', () => {
    const design = makeDesign('basic', 'earth', 'intermediate', [], ['fireball']);
    const serialized = makeSerialized(design);
    const spellDef = SPELLS_DEF['fireball'];
    const frame = FRAMES['earth'];
    const golem = makeActiveGolem(design, 100, 1.0);
    const enemy = makeEnemy();

    const expectedSpellDmg = spellDef.dmg * frame.magicAffinity; // 15 * 0.3 = 4.5

    const { capturedDamage } = runGolemAttacks(golem, serialized, design, enemy, 'fire');

    expect(capturedDamage).toBe(expectedSpellDmg);
    expect(capturedDamage).not.toBe(20 * frame.magicAffinity); // Old buggy value would be 6
  });

  it('uses actual spell mana cost not hardcoded 10', () => {
    const design = makeDesign('basic', 'earth', 'intermediate', [], ['fireball']);
    const serialized = makeSerialized(design);
    const spellDef = SPELLS_DEF['fireball'];
    const golem = makeActiveGolem(design, spellDef.cost.amount, 1.0);
    const enemy = makeEnemy();

    let spellCastCount = 0;
    processGolemAttacks(
      [golem],
      { [design.id]: serialized },
      () => { spellCastCount++; return { rawMana: 0, elements: {} }; },
      () => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
      'fire',
      () => enemy,
      () => [enemy],
      () => {},
    100,
    {},
  );

    expect(spellCastCount).toBeGreaterThan(0);
  });

  it('falls back to basic attack when golem mana is below actual spell cost', () => {
    const design = makeDesign('basic', 'earth', 'intermediate', [], ['fireball']);
    const serialized = makeSerialized(design);
    const spellDef = SPELLS_DEF['fireball'];
    // Give golem less mana than the spell costs (fireball costs 2)
    const golem = makeActiveGolem(design, spellDef.cost.amount - 1, 1.0);
    // Use 0 armor enemy so basic attack damage is full baseDamage (6)
    const enemy = makeEnemy({ armor: 0 });

    let damageCount = 0;
    processGolemAttacks(
      [golem],
      { [design.id]: serialized },
      () => { damageCount++; return { rawMana: 0, elements: {} }; },
      () => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
      'fire',
      () => enemy,
      () => [enemy],
      () => {},
    100,
    {},
  );

    // Should fall back to basic attack since mana is insufficient
    expect(damageCount).toBeGreaterThan(0);
  });
});

// ─── Fix #3: Elemental matchup ───────────────────────────────────────────────

describe('processGolemAttacks - elemental matchup (fix #3)', () => {
  it('applies super effective elemental bonus (1.5x) to basic attacks', () => {
    // Water is super effective vs fire (ELEMENT_OPPOSITES['fire'] = 'water')
    // Use a frame with element that is super effective vs the enemy element
    // Fire frame (Steel has element 'metal', Crystal has 'crystal')
    // Let's use computeBasicAttackDamage directly with water element vs fire enemy
    const dmg = computeBasicAttackDamage(
      { baseDamage: 10, armorPierce: 0, element: 'water' },
      0, 0, 'fire',
    );
    expect(dmg).toBe(15); // water vs fire = 1.5x
  });

  it('applies weak elemental bonus (0.75x) when frame element is weak', () => {
    // Lightning is weak to earth: ELEMENT_OPPOSITES['lightning'] = 'earth'
    const dmg = computeBasicAttackDamage(
      { baseDamage: 10, armorPierce: 0, element: 'lightning' },
      0, 0, 'earth',
    );
    expect(dmg).toBe(7.5); // 10 * 0.75
  });

  it('applies same-element bonus (1.25x) when frame element matches enemy', () => {
    const dmg = computeBasicAttackDamage(
      { baseDamage: 10, armorPierce: 0, element: 'fire' },
      0, 0, 'fire',
    );
    expect(dmg).toBe(12.5); // 10 * 1.25
  });

  it('applies neutral bonus (1.0x) for unrelated elements', () => {
    // Death element has no specific matchup with fire = neutral
    const dmg = computeBasicAttackDamage(
      { baseDamage: 10, armorPierce: 0, element: 'death' },
      0, 0, 'fire',
    );
    expect(dmg).toBe(10); // 1.0x neutral
  });

  it('applies elemental bonus through processGolemAttacks (integration)', () => {
    // Crystal frame has element 'crystal'. Test with an enemy element where crystal is neutral.
    // Crystal vs fire = neutral (1.0x)
    const design = makeDesign('basic', 'crystal', 'simple');
    const serialized = makeSerialized(design);
    const golem = makeActiveGolem(design, undefined, 1.0);
    const enemy = makeEnemy({ element: 'fire', armor: 0 }); // No armor for clean test

    const frame = FRAMES['crystal'];
    // Crystal frame: baseDamage=14, armorPierce=0.15, neutral element (1.0x)
    // New formula: 14 * 1.0 * (1 + 0.15) = 16.1
    const expectedDmg = frame.baseDamage * (1 + frame.armorPierce);

    const { capturedDamage } = runGolemAttacks(golem, serialized, design, enemy, 'fire');
    expect(capturedDamage).toBeCloseTo(expectedDmg, 5);
  });
});

// ─── Fix #4: Enchantment effects ─────────────────────────────────────────────

describe('processGolemAttacks - enchantment effects (fix #4)', () => {
  it('applies enchantment effects to target enemy on basic attack', () => {
    const design = makeDesign('basic', 'earth', 'simple', ['sword_fire']);
    const serialized = makeSerialized(design);
    const golem = makeActiveGolem(design, undefined, 1.0);
    const enemy = makeEnemy();

    const appliedEffects: { enemyId: string; effects: { type: string; magnitude: number }[] }[] = [];
    processGolemAttacks(
      [golem],
      { [design.id]: serialized },
      () => ({ rawMana: 0, elements: {} }),
      () => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
      'fire',
      () => enemy,
      () => [enemy],
      (enemyId, effects) => { appliedEffects.push({ enemyId, effects }); },
    100,
    {},
  );

    expect(appliedEffects.length).toBeGreaterThan(0);
    expect(appliedEffects[0].effects.some(e => e.type === 'burn')).toBe(true);
  });

  it('applies multiple enchantment effects when multiple enchantments equipped', () => {
    const design = makeDesign('basic', 'earth', 'simple', ['sword_fire', 'sword_frost', 'sword_lightning']);
    const serialized = makeSerialized(design);
    const golem = makeActiveGolem(design, undefined, 1.0);
    const enemy = makeEnemy();

    const appliedEffects: { type: string; magnitude: number }[][] = [];
    processGolemAttacks(
      [golem],
      { [design.id]: serialized },
      () => ({ rawMana: 0, elements: {} }),
      () => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
      'fire',
      () => enemy,
      () => [enemy],
      (_, effects) => { appliedEffects.push(effects); },
    100,
    {},
  );

    const allEffectTypes = appliedEffects.flat().map(e => e.type);
    expect(allEffectTypes).toContain('burn');
    expect(allEffectTypes).toContain('slow');
    expect(allEffectTypes).toContain('shock');
  });

  it('does not apply enchantment effects when no enchantments equipped', () => {
    const design = makeDesign('basic', 'earth', 'simple', []);
    const serialized = makeSerialized(design);
    const golem = makeActiveGolem(design, undefined, 1.0);
    const enemy = makeEnemy();

    let effectsCalled = false;
    processGolemAttacks(
      [golem],
      { [design.id]: serialized },
      () => ({ rawMana: 0, elements: {} }),
      () => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
      'fire',
      () => enemy,
      () => [enemy],
      () => { effectsCalled = true; },
    100,
    {},
  );

    expect(effectsCalled).toBe(false);
  });
});

// ─── Fix #5: Armor pierce (spire-combat spec §9.4) ──────────────────────────
// Formula: dmg = frame.baseDamage × (1 + frame.armorPierce)
// Enemy armor reduction is handled separately in onDamageDealt.

describe('processGolemAttacks - armor pierce (fix #5)', () => {
  it('applies armor pierce as damage multiplier per spire-combat spec §9.4', () => {
    // Steel frame: baseDamage=18, armorPierce=0.5
    // New formula: 18 * (1 + 0.5) = 27
    const design = makeDesign('basic', 'steel', 'simple');
    const serialized = makeSerialized(design);
    const golem = makeActiveGolem(design, undefined, 1.0);
    const enemy = makeEnemy({ armor: 0.4 });

    const frame = FRAMES['steel'];
    const expectedDmg = frame.baseDamage * (1 + frame.armorPierce);

    const { capturedDamage } = runGolemAttacks(golem, serialized, design, enemy, 'fire');

    expect(capturedDamage).toBe(expectedDmg);
  });

  it('doubles damage when armorPierce is 1.0', () => {
    // dmg = 10 * (1 + 1.0) = 20
    const dmg = computeBasicAttackDamage(
      { baseDamage: 10, armorPierce: 1.0, element: undefined },
      0, 0.8, 'fire',
    );
    expect(dmg).toBe(20);
  });

  it('returns base damage when armorPierce is 0', () => {
    // dmg = 10 * (1 + 0) = 10
    const dmg = computeBasicAttackDamage(
      { baseDamage: 10, armorPierce: 0, element: undefined },
      0, 0.5, 'fire',
    );
    expect(dmg).toBe(10);
  });

  it('stacks enchantment armorPierce with frame armorPierce', () => {
    const framePierce = 0.5;
    const enchantPierce = 0.15;
    const totalPierce = framePierce + enchantPierce;

    const dmg = computeBasicAttackDamage(
      { baseDamage: 20, armorPierce: framePierce, element: undefined },
      enchantPierce, 0.4, 'fire',
    );
    const expected = 20 * (1 + totalPierce);
    expect(dmg).toBe(expected);
  });
});

