import { describe, it, expect, beforeEach } from 'vitest';
import {
  computeChargeFillRate,
  computeCastSpeedBonus,
  computeCostMultiplier,
  computeDrainRateMultiplier,
  computeDrainPerTick,
  selectInvocationGuardian,
  getGuardianSpellbook,
  selectInvocationSpell,
  deductInvocationSpellCost,
} from '../utils/invocation-utils';
import { SPELLS_DEF } from '../constants/spells';
import { getGuardianForFloor } from '../data/guardian-encounters';
import type { DisciplineBonuses } from '../utils/mana-utils';
import type { GuardianDef } from '../types';

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

function makeBonuses(overrides: Record<string, number> = {}): DisciplineBonuses {
  return {
    bonuses: { ...overrides },
    multipliers: {},
  };
}

function makeElements(overrides: Record<string, { current: number; max: number; unlocked: boolean }> = {}) {
  return {
    fire: { current: 100, max: 100, unlocked: true },
    water: { current: 100, max: 100, unlocked: true },
    air: { current: 100, max: 100, unlocked: true },
    earth: { current: 100, max: 100, unlocked: true },
    light: { current: 100, max: 100, unlocked: true },
    dark: { current: 100, max: 100, unlocked: true },
    death: { current: 100, max: 100, unlocked: true },
    raw: { current: 1000, max: 1000, unlocked: true },
    ...overrides,
  };
}

// ─── Charge Fill Rate (§2.2) ───────────────────────────────────────────────────

describe('computeChargeFillRate', () => {
  it('should return base rate with 0 pacts and no bonus', () => {
    // 0.25 × (1 + 0 × 0.15) × (1 + 0) = 0.25
    expect(computeChargeFillRate(0, 0)).toBeCloseTo(0.25, 5);
  });

  it('should scale with pact count (AC-1)', () => {
    // 3 pacts: 0.25 × (1 + 3 × 0.15) × 1 = 0.25 × 1.45 = 0.3625
    expect(computeChargeFillRate(3, 0)).toBeCloseTo(0.3625, 5);
  });

  it('should scale with discipline bonus', () => {
    // 0 pacts, 0.10 bonus: 0.25 × 1 × 1.10 = 0.275
    expect(computeChargeFillRate(0, 0.10)).toBeCloseTo(0.275, 5);
  });

  it('should combine pact and discipline multipliers', () => {
    // 3 pacts, 0.10 bonus: 0.25 × 1.45 × 1.10 = 0.39875
    expect(computeChargeFillRate(3, 0.10)).toBeCloseTo(0.39875, 5);
  });

  it('should handle 6 pacts', () => {
    // 6 pacts: 0.25 × (1 + 6 × 0.15) = 0.25 × 1.9 = 0.475
    expect(computeChargeFillRate(6, 0)).toBeCloseTo(0.475, 5);
  });
});

// ─── Guardian Selection (§3.2) ─────────────────────────────────────────────────

describe('selectInvocationGuardian', () => {
  it('should return null for empty pacts', () => {
    expect(selectInvocationGuardian([], ['fire'])).toBeNull();
  });

  it('should select the only available guardian', () => {
    const result = selectInvocationGuardian([10], ['fire']);
    expect(result).toBe(10);
  });

  it('should prefer guardian with best elemental bonus', () => {
    // Fire guardian (floor 10) vs Water guardian (floor 20)
    // Against fire enemy: fire guardian gets 1.25 (same element), water gets 1.5 (super effective)
    // Water guardian should win due to super effective bonus
    const result = selectInvocationGuardian([10, 20], ['fire']);
    expect(result).toBe(20); // Water guardian counters fire
  });

  it('should use tier multiplier as tiebreaker', () => {
    // Both guardians have same elemental bonus, higher floor should win
    const result = selectInvocationGuardian([10, 20], ['raw']);
    // Raw has no elemental bonus, so both get 1.0
    // Floor 10: 1.0 × 1.05 = 1.05, Floor 20: 1.0 × 1.10 = 1.10
    expect(result).toBe(20);
  });

  it('should handle multi-element guardians', () => {
    // Floor 130 has elements ['metal', 'fire', 'earth']
    const result = selectInvocationGuardian([10, 130], ['fire']);
    // Floor 10 (fire): bonus 1.25, tier 1.05 → 1.3125
    // Floor 130 (metal/fire/earth): best bonus against fire is fire=1.25, tier 1.65 → 2.0625
    expect(result).toBe(130);
  });
});

// ─── Guardian Spellbook (§5.1) ─────────────────────────────────────────────────

describe('getGuardianSpellbook', () => {
  it('should return spells for a single-element guardian', () => {
    const guardian = getGuardianForFloor(10)!; // Fire guardian
    expect(guardian).not.toBeNull();
    const spellbook = getGuardianSpellbook(guardian);
    // Should include fire spells
    const fireSpells = spellbook.filter(s => s.elem === 'fire');
    expect(fireSpells.length).toBeGreaterThan(0);
  });

  it('should return spells for a multi-element guardian', () => {
    const guardian = getGuardianForFloor(130)!; // Metal/Fire/Earth guardian
    expect(guardian).not.toBeNull();
    const spellbook = getGuardianSpellbook(guardian);
    // Should include spells from all elements
    const elements = new Set(spellbook.map(s => s.elem));
    expect(elements.has('fire')).toBe(true);
  });

  it('should not have duplicate spells', () => {
    const guardian = getGuardianForFloor(10)!;
    const spellbook = getGuardianSpellbook(guardian);
    const names = spellbook.map(s => s.name);
    const uniqueNames = new Set(names);
    expect(names.length).toBe(uniqueNames.size);
  });
});

// ─── Spell Selection (§5.2) ────────────────────────────────────────────────────

describe('selectInvocationSpell', () => {
  it('should return null when no spells are affordable', () => {
    const expensiveSpells = Object.values(SPELLS_DEF).filter(
      s => s.cost.type === 'raw' && s.cost.amount > 50,
    );
    // With 0 raw mana, even at 1.0 multiplier these should be unaffordable
    const result = selectInvocationSpell(expensiveSpells, 0, makeElements(), 1.0);
    expect(result).toBeNull();
  });

  it('should select highest damage affordable spell', () => {
    const fireSpells = Object.values(SPELLS_DEF).filter(s => s.elem === 'fire');
    // With plenty of mana, should pick highest damage
    const result = selectInvocationSpell(fireSpells, 1000, makeElements(), 0.1);
    expect(result).not.toBeNull();
    // Should be the highest-damage fire spell (could be Pyroclasm etc.)
    const selectedSpell = fireSpells.find(s => s.name === result!.spellId);
    expect(selectedSpell).toBeDefined();
    expect(selectedSpell!.dmg).toBeGreaterThanOrEqual(15);
  });

  it('should step down when current spell becomes unaffordable', () => {
    const fireSpells = Object.values(SPELLS_DEF).filter(s => s.elem === 'fire');
    // With very limited mana, can only afford cheap spells
    // At 0.1 multiplier, fireball costs 0.2 fire, ember shot costs 0.1 fire
    const result = selectInvocationSpell(fireSpells, 0, makeElements({ fire: { current: 0.15, max: 100, unlocked: true } }), 0.1);
    expect(result).not.toBeNull();
    // Should pick ember shot (cost 0.1) over fireball (cost 0.2)
    expect(result!.spellId).toBe('Ember Shot');
  });

  it('should pick highest tier when damage is tied', () => {
    // This tests the tiebreaker logic
    const spellbook = Object.values(SPELLS_DEF);
    const result = selectInvocationSpell(spellbook, 1000, makeElements(), 0.1);
    expect(result).not.toBeNull();
    // Should pick the highest damage spell available
    const selectedSpell = spellbook.find(s => s.name === result!.spellId);
    expect(selectedSpell).toBeDefined();
  });

  it('should pick lowest cost when damage and tier are tied', () => {
    // Create a scenario with tied damage
    const customSpells = [
      { name: 'Expensive', elem: 'fire', dmg: 10, cost: { type: 'raw' as const, amount: 10 }, tier: 1, castSpeed: 1 },
      { name: 'Cheap', elem: 'fire', dmg: 10, cost: { type: 'raw' as const, amount: 5 }, tier: 1, castSpeed: 1 },
    ];
    const result = selectInvocationSpell(customSpells, 1000, makeElements(), 1.0);
    expect(result).not.toBeNull();
    expect(result!.spellId).toBe('Cheap');
  });
});

// ─── Cost Multiplier (§5.5) ────────────────────────────────────────────────────

describe('computeCostMultiplier', () => {
  it('should return base 0.1 with no discipline bonuses', () => {
    expect(computeCostMultiplier(makeBonuses())).toBeCloseTo(0.1, 5);
  });

  it('should reduce cost by invocationCostReduction', () => {
    // invocation-efficiency perk: -0.02
    expect(computeCostMultiplier(makeBonuses({ invocationCostReduction: 0.02 }))).toBeCloseTo(0.08, 5);
  });

  it('should have minimum of 0.05 (AC-19)', () => {
    // Even with huge reduction, should not go below 0.05
    expect(computeCostMultiplier(makeBonuses({ invocationCostReduction: 0.1 }))).toBeCloseTo(0.05, 5);
  });

  it('should handle combined efficiency + mastery perks', () => {
    // efficiency: -0.02, mastery (3 tiers): -0.03 = total -0.05
    expect(computeCostMultiplier(makeBonuses({ invocationCostReduction: 0.05 }))).toBeCloseTo(0.05, 5);
  });
});

// ─── Drain Rate Multiplier (§4) ────────────────────────────────────────────────

describe('computeDrainRateMultiplier', () => {
  it('should return base 1.0 with no discipline bonuses', () => {
    expect(computeDrainRateMultiplier(makeBonuses())).toBeCloseTo(1.0, 5);
  });

  it('should reduce drain by sustain perk (AC-17)', () => {
    // sustain perk: drainRateMultiplier = -0.1 per tier
    expect(computeDrainRateMultiplier(makeBonuses({ drainRateMultiplier: -0.1 }))).toBeCloseTo(0.9, 5);
  });

  it('should have minimum of 0.7 (AC-20)', () => {
    // 3 tiers of sustain: -0.3, so 1.0 - 0.3 = 0.7
    expect(computeDrainRateMultiplier(makeBonuses({ drainRateMultiplier: -0.3 }))).toBeCloseTo(0.7, 5);
    // Even with more reduction, should not go below 0.7
    expect(computeDrainRateMultiplier(makeBonuses({ drainRateMultiplier: -0.5 }))).toBeCloseTo(0.7, 5);
  });
});

// ─── Drain Per Tick ────────────────────────────────────────────────────────────

describe('computeDrainPerTick', () => {
  it('should compute drain based on spell cost', () => {
    // spell cost 10, drain mult 1.0: 1.0 × (10/10) × 1.0 = 1.0
    expect(computeDrainPerTick(10, 1.0)).toBeCloseTo(1.0, 5);
  });

  it('should scale with spell cost', () => {
    // spell cost 20, drain mult 1.0: 1.0 × (20/10) × 1.0 = 2.0
    expect(computeDrainPerTick(20, 1.0)).toBeCloseTo(2.0, 5);
  });

  it('should scale with drain multiplier', () => {
    // spell cost 10, drain mult 0.7: 1.0 × 1.0 × 0.7 = 0.7
    expect(computeDrainPerTick(10, 0.7)).toBeCloseTo(0.7, 5);
  });
});

// ─── Pact Affinity Cast Speed (§6.1) ───────────────────────────────────────────

describe('computeCastSpeedBonus', () => {
  it('should return 0 with no pact affinity', () => {
    expect(computeCastSpeedBonus(0)).toBeCloseTo(0, 5);
  });

  it('should return ~5.7% at 0.1 affinity (AC-12)', () => {
    // 0.5 × (1 - 1 / (1 + 0.1 × 1.5)) = 0.5 × (1 - 1/1.15) ≈ 0.0652
    expect(computeCastSpeedBonus(0.1)).toBeCloseTo(0.0652, 2);
  });

  it('should return ~21.4% at 0.5 affinity', () => {
    // 0.5 × (1 - 1 / (1 + 0.5 × 1.5)) = 0.5 × (1 - 1/1.75) ≈ 0.2143
    expect(computeCastSpeedBonus(0.5)).toBeCloseTo(0.2143, 2);
  });

  it('should approach 50% asymptotically (AC-12)', () => {
    // At very high affinity, should approach but never reach 0.5
    const bonus = computeCastSpeedBonus(100);
    expect(bonus).toBeLessThan(0.5);
    expect(bonus).toBeGreaterThan(0.49);
  });

  it('should give diminishing returns', () => {
    const low = computeCastSpeedBonus(0.1);
    const high = computeCastSpeedBonus(0.2);
    // Doubling affinity should less than double the bonus
    expect(high).toBeLessThan(low * 2);
  });
});

// ─── Deduct Invocation Spell Cost ──────────────────────────────────────────────

describe('deductInvocationSpellCost', () => {
  it('should deduct raw mana cost at multiplier', () => {
    const spell = SPELLS_DEF['manaBolt'];
    if (!spell) {
      // If manaBolt doesn't exist, skip
      return;
    }
    const result = deductInvocationSpellCost('manaBolt', 0.1, 100, makeElements());
    // Cost should be deducted from raw mana
    expect(result.rawMana).toBeLessThan(100);
  });

  it('should deduct element mana cost at multiplier', () => {
    const result = deductInvocationSpellCost('fireball', 0.1, 1000, makeElements());
    // Fireball costs 2 fire at full price, at 0.1 multiplier = 0.2 fire
    expect(result.elements.fire.current).toBeLessThan(100);
  });

  it('should handle unaffordable spells gracefully', () => {
    const result = deductInvocationSpellCost('Fireball', 0.1, 0, makeElements({ fire: { current: 0, max: 100, unlocked: true } }));
    // Should not deduct anything if can't afford
    expect(result.rawMana).toBe(0);
    expect(result.elements.fire.current).toBe(0);
  });
});

// ─── Integration: Full Invocation Flow ─────────────────────────────────────────

describe('invocation system integration', () => {
  it('should compute full charge-to-drain cycle', () => {
    // Simulate charge fill with 3 pacts
    const fillRate = computeChargeFillRate(3, 0);
    expect(fillRate).toBeCloseTo(0.3625, 5);

    // Time to 100: 100 / 0.3625 ≈ 276 ticks
    const ticksToFull = 100 / fillRate;
    expect(ticksToFull).toBeCloseTo(275.86, 1);
  });

  it('should compute drain during invocation with discipline perks', () => {
    const disciplineEffects = makeBonuses({
      invocationCostReduction: 0.05, // efficiency + mastery
      drainRateMultiplier: -0.3, // max sustain
    });

    const costMult = computeCostMultiplier(disciplineEffects);
    expect(costMult).toBeCloseTo(0.05, 5);

    const drainMult = computeDrainRateMultiplier(disciplineEffects);
    expect(drainMult).toBeCloseTo(0.7, 5);

    // Fireball costs 2 fire, drain = 1.0 × (2/10) × 0.7 = 0.14 per tick
    const drainPerTick = computeDrainPerTick(2, drainMult);
    expect(drainPerTick).toBeCloseTo(0.14, 5);

    // Invocation lasts: 100 / 0.14 ≈ 714 ticks
    const invocationDuration = 100 / drainPerTick;
    expect(invocationDuration).toBeCloseTo(714.29, 1);
  });

  it('should verify spell selection from guardian spellbook', () => {
    const guardian = getGuardianForFloor(10)!; // Fire guardian
    const spellbook = getGuardianSpellbook(guardian);

    // With full mana, should select highest damage fire spell
    const selection = selectInvocationSpell(spellbook, 1000, makeElements(), 0.1);
    expect(selection).not.toBeNull();
    expect(selection!.element).toBe('fire');
  });
});
