import { describe, it, expect } from 'vitest';
import {
  getElementalBonus,
  getBoonBonuses,
  getIncursionStrength,
  canAffordSpellCost,
  deductSpellCost,
} from '../utils/combat-utils';
import { INCURSION_START_DAY, MAX_DAY } from '../constants';

// ─── getElementalBonus ────────────────────────────────────────────────────────

describe('getElementalBonus', () => {
  it('should return 1.0 for raw mana (no elemental bonus)', () => {
    expect(getElementalBonus('raw', 'fire')).toBe(1.0);
    expect(getElementalBonus('raw', 'water')).toBe(1.0);
    expect(getElementalBonus('raw', 'earth')).toBe(1.0);
  });

  it('should return 1.25 for same element (matching)', () => {
    expect(getElementalBonus('fire', 'fire')).toBe(1.25);
    expect(getElementalBonus('water', 'water')).toBe(1.25);
    expect(getElementalBonus('earth', 'earth')).toBe(1.25);
    expect(getElementalBonus('air', 'air')).toBe(1.25);
    expect(getElementalBonus('light', 'light')).toBe(1.25);
    expect(getElementalBonus('dark', 'dark')).toBe(1.25);
  });

  it('should return 1.5 for super effective (opposite of floor)', () => {
    // Water is opposite of fire → water spell on fire floor = super effective
    expect(getElementalBonus('water', 'fire')).toBe(1.5);
    // Fire is opposite of water → fire spell on water floor = super effective
    expect(getElementalBonus('fire', 'water')).toBe(1.5);
    // Earth is opposite of air
    expect(getElementalBonus('earth', 'air')).toBe(1.5);
    // Air is opposite of earth
    expect(getElementalBonus('air', 'earth')).toBe(1.5);
    // Dark is opposite of light
    expect(getElementalBonus('dark', 'light')).toBe(1.5);
    // Light is opposite of dark
    expect(getElementalBonus('light', 'dark')).toBe(1.5);
  });

  it('should return 0.75 for weak (spell opposite matches floor)', () => {
    // Fire spell on water floor: fire's opposite is water → weak
    expect(getElementalBonus('fire', 'water')).not.toBe(0.75); // This is super effective
    // Water spell on fire floor: water's opposite is fire → weak
    expect(getElementalBonus('water', 'fire')).not.toBe(0.75); // This is super effective
    // Let's test a truly neutral case: fire spell on earth floor
    // fire's opposite is water, water !== earth → neutral
    // earth's opposite is air, air !== fire → neutral
    expect(getElementalBonus('fire', 'earth')).toBe(1.0);
  });

  it('should return 1.0 for neutral matchups', () => {
    // Fire on earth: not same, not opposite, not weak
    expect(getElementalBonus('fire', 'earth')).toBe(1.0);
    // Water on air: not same, not opposite, not weak
    expect(getElementalBonus('water', 'air')).toBe(1.0);
    // Light on fire: not same, not opposite, not weak
    expect(getElementalBonus('light', 'fire')).toBe(1.0);
  });

  it('should handle lightning matchups', () => {
    // Lightning is weak to earth (grounding)
    expect(getElementalBonus('lightning', 'earth')).toBe(0.75);
    // Lightning on fire: neutral
    expect(getElementalBonus('lightning', 'fire')).toBe(1.0);
  });
});

// ─── getBoonBonuses ───────────────────────────────────────────────────────────

describe('getBoonBonuses', () => {
  it('should return zero bonuses for empty pacts', () => {
    const result = getBoonBonuses([]);
    expect(result).toEqual({
      maxMana: 0,
      manaRegen: 0,
      castingSpeed: 0,
      elementalDamage: 0,
      rawDamage: 0,
      critChance: 0,
      critDamage: 0,
      spellEfficiency: 0,
      manaGain: 0,
      insightGain: 0,
      studySpeed: 0,
      prestigeInsight: 0,
    });
  });

  it('should return bonuses from a single pact (floor 10)', () => {
    const result = getBoonBonuses([10]);
    expect(result.elementalDamage).toBe(5);
    expect(result.maxMana).toBe(50);
  });

  it('should stack bonuses from multiple pacts', () => {
    const result = getBoonBonuses([10, 20]);
    // Floor 10: elementalDamage=5, maxMana=50
    // Floor 20: elementalDamage=5, manaRegen=0.5
    expect(result.elementalDamage).toBe(10);
    expect(result.maxMana).toBe(50);
    expect(result.manaRegen).toBe(0.5);
  });

  it('should handle all boon types from floor 100', () => {
    const result = getBoonBonuses([100]);
    // Floor 100 (sand): elementalDamage=15, manaRegen=1.5
    expect(result.elementalDamage).toBe(15);
    expect(result.manaRegen).toBe(1.5);
    expect(result.maxMana).toBe(0);
    expect(result.insightGain).toBe(0);
  });

  it('should ignore non-guardian floors', () => {
    const result = getBoonBonuses([5, 15, 25]);
    expect(result).toEqual({
      maxMana: 0,
      manaRegen: 0,
      castingSpeed: 0,
      elementalDamage: 0,
      rawDamage: 0,
      critChance: 0,
      critDamage: 0,
      spellEfficiency: 0,
      manaGain: 0,
      insightGain: 0,
      studySpeed: 0,
      prestigeInsight: 0,
    });
  });

  it('should handle mix of guardian and non-guardian floors', () => {
    const result = getBoonBonuses([5, 10, 15]);
    // Only floor 10 counts
    expect(result.elementalDamage).toBe(5);
    expect(result.maxMana).toBe(50);
  });

  it('should stack all bonus types from multiple pacts', () => {
    // Include floor 70 (death) which has rawDamage=10
    const result = getBoonBonuses([10, 20, 30, 40, 50, 60, 70, 80, 90, 100]);
    expect(result.elementalDamage).toBeGreaterThan(0);
    expect(result.maxMana).toBeGreaterThan(0);
    expect(result.manaRegen).toBeGreaterThan(0);
    expect(result.castingSpeed).toBeGreaterThan(0);
    expect(result.insightGain).toBeGreaterThan(0);
    expect(result.critDamage).toBeGreaterThan(0);
    expect(result.rawDamage).toBeGreaterThan(0);
  });
});

// ─── getIncursionStrength ─────────────────────────────────────────────────────

describe('getIncursionStrength', () => {
  it('should return 0 before incursion start day', () => {
    expect(getIncursionStrength(1, 0)).toBe(0);
    expect(getIncursionStrength(10, 12)).toBe(0);
    expect(getIncursionStrength(INCURSION_START_DAY - 1, 23)).toBe(0);
  });

  it('should return 0 at incursion start day hour 0', () => {
    const result = getIncursionStrength(INCURSION_START_DAY, 0);
    expect(result).toBe(0);
  });

  it('should return positive value during incursion', () => {
    const result = getIncursionStrength(INCURSION_START_DAY, 12);
    expect(result).toBeGreaterThan(0);
  });

  it('should increase with later days', () => {
    const early = getIncursionStrength(INCURSION_START_DAY, 12);
    const late = getIncursionStrength(MAX_DAY, 12);
    expect(late).toBeGreaterThan(early);
  });

  it('should increase with later hours on same day', () => {
    const morning = getIncursionStrength(INCURSION_START_DAY + 1, 6);
    const evening = getIncursionStrength(INCURSION_START_DAY + 1, 18);
    expect(evening).toBeGreaterThan(morning);
  });

  it('should not exceed 0.95', () => {
    expect(getIncursionStrength(MAX_DAY, 23)).toBeLessThanOrEqual(0.95);
    expect(getIncursionStrength(MAX_DAY + 100, 23)).toBeLessThanOrEqual(0.95);
  });

  it('should approach 0.95 at end of max day', () => {
    const result = getIncursionStrength(MAX_DAY, 23);
    expect(result).toBeCloseTo(0.95, 1);
  });
});

// ─── canAffordSpellCost ───────────────────────────────────────────────────────

describe('canAffordSpellCost', () => {
  it('should return true for raw cost when enough mana', () => {
    expect(canAffordSpellCost({ type: 'raw', amount: 10 }, 50, {})).toBe(true);
  });

  it('should return false for raw cost when not enough mana', () => {
    expect(canAffordSpellCost({ type: 'raw', amount: 100 }, 50, {})).toBe(false);
  });

  it('should return true for zero raw cost', () => {
    expect(canAffordSpellCost({ type: 'raw', amount: 0 }, 0, {})).toBe(true);
  });

  it('should return true for elemental cost when element is unlocked and has enough', () => {
    const elements = {
      fire: { current: 10, max: 50, unlocked: true },
    };
    expect(canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements)).toBe(true);
  });

  it('should return false for elemental cost when not enough mana', () => {
    const elements = {
      fire: { current: 3, max: 50, unlocked: true },
    };
    expect(canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements)).toBe(false);
  });

  it('should return false for locked element', () => {
    const elements = {
      fire: { current: 10, max: 50, unlocked: false },
    };
    expect(canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements)).toBe(false);
  });

  it('should return falsy for missing element', () => {
    expect(canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, {})).toBeFalsy();
  });

  it('should return true for zero elemental cost', () => {
    const elements = {
      fire: { current: 0, max: 50, unlocked: true },
    };
    expect(canAffordSpellCost({ type: 'element', element: 'fire', amount: 0 }, 0, elements)).toBe(true);
  });

  it('should handle exact amount', () => {
    const elements = {
      fire: { current: 5, max: 50, unlocked: true },
    };
    expect(canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements)).toBe(true);
  });
});

// ─── deductSpellCost ──────────────────────────────────────────────────────────

describe('deductSpellCost', () => {
  it('should deduct raw mana correctly', () => {
    const result = deductSpellCost({ type: 'raw', amount: 10 }, 50, {});
    expect(result.rawMana).toBe(40);
  });

  it('should not let raw mana go below zero', () => {
    const result = deductSpellCost({ type: 'raw', amount: 100 }, 50, {});
    expect(result.rawMana).toBe(0);
  });

  it('should deduct elemental mana correctly', () => {
    const elements = {
      fire: { current: 10, max: 50, unlocked: true },
    };
    const result = deductSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
    expect(result.elements.fire.current).toBe(5);
    expect(result.rawMana).toBe(0); // unchanged
  });

  it('should not let elemental mana go below zero', () => {
    const elements = {
      fire: { current: 3, max: 50, unlocked: true },
    };
    const result = deductSpellCost({ type: 'element', element: 'fire', amount: 10 }, 0, elements);
    expect(result.elements.fire.current).toBe(0);
  });

  it('should not modify raw mana when deducting elemental cost', () => {
    const elements = {
      fire: { current: 10, max: 50, unlocked: true },
    };
    const result = deductSpellCost({ type: 'element', element: 'fire', amount: 5 }, 100, elements);
    expect(result.rawMana).toBe(100);
  });

  it('should return unchanged state for zero cost', () => {
    const elements = {
      fire: { current: 10, max: 50, unlocked: true },
    };
    const result = deductSpellCost({ type: 'raw', amount: 0 }, 50, elements);
    expect(result.rawMana).toBe(50);
    expect(result.elements.fire.current).toBe(10);
  });

  it('should not modify other elements when deducting', () => {
    const elements = {
      fire: { current: 10, max: 50, unlocked: true },
      water: { current: 20, max: 50, unlocked: true },
    };
    const result = deductSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
    expect(result.elements.fire.current).toBe(5);
    expect(result.elements.water.current).toBe(20);
  });

  it('should handle missing element gracefully', () => {
    const elements = {
      fire: { current: 10, max: 50, unlocked: true },
    };
    const result = deductSpellCost({ type: 'element', element: 'water', amount: 5 }, 100, elements);
    expect(result.rawMana).toBe(100);
    expect(result.elements).toEqual(elements);
  });
});
